19 Commits
Author SHA1 Message Date
gisle c2d029f918 fix posts not being logged after logbot reconnect.
continuous-integration/drone/push Build is passing
2020-08-09 13:01:06 +02:00
gisle 293102c1c6 add more logging and update irc code.
continuous-integration/drone/push Build is passing
2020-07-20 21:14:49 +02:00
gisle de04d6069b simply and hopefully fix channel leave/join
continuous-integration/drone/push Build is passing
2020-07-20 20:40:55 +02:00
gisle b249a495a4 add more debug stuffs.
continuous-integration/drone/push Build is passing
2020-07-20 20:15:55 +02:00
gisle f4d324da9b fix drone.yml
continuous-integration/drone/push Build is passing
2020-07-17 18:27:03 +02:00
gisle fff73cfe44 add go 1.14 to drone.yml
continuous-integration/drone/push Build is passing
2020-07-17 18:20:23 +02:00
gisle 31885d76f0 fix drone.yml
continuous-integration/drone/push Build is passing
2020-07-17 18:19:11 +02:00
gisle fec21a0045 fix drone.yml 2020-07-17 18:14:19 +02:00
gisle d721112738 migrate irc dependency to github version. 2020-07-17 17:21:18 +02:00
gisle dd51f7fd59 Updated irc dependency 2019-03-17 22:35:16 +01:00
gisle 42e6ca8f26 package: Updated irc dependency to fix ((Remove :01 goofs!*)) parse bug. 2019-01-20 21:15:36 +01:00
gisle 304c645043 handler: Fixed issue caused by trying to post empty post. Root cause is not found, but offending posts will be ignored. 2019-01-20 20:29:31 +01:00
gisle 31fd9f9a56 bot: Added !register command.
the build was successful
2018-12-02 18:40:55 +01:00
gisle e68503c72e bot: Added more logging of errors and such, exit on ERROR packet and configurable autorun commands on ready and channel op join.
the build was successful
2018-12-02 17:38:04 +01:00
gisle 32b5649465 package: Updated irc dependency to fix own permission modes not being set on join. 2018-12-02 17:29:36 +01:00
gisle e949a980fb bot: Fixed handling of action messages, and added NPC support.
the build was successful
2018-11-25 19:36:45 +01:00
gisle 9a0292c2fa package: Updated irc dependency to fix all CTCP messages being parsed as CTCP reply. 2018-11-25 19:36:02 +01:00
gisle d109349657 bot: Added logging of more numerics wrt. nick and added logging of direct messages.
the build was successful
2018-11-24 11:26:33 +01:00
gisle d4b3f5a5db package: Updated irc dependency to fix 433 not being handled. 2018-11-24 11:26:10 +01:00
14 changed files with 379 additions and 162 deletions
+75 -18
View File
@@ -1,21 +1,78 @@
workspace:
base: /go
path: src/git.aiterp.net/rpdata/logbot3
name: logbot3
pipeline:
docker-release:
image: plugins/docker
repo: vmaple/rpdata-logbot3
auto_tag: true
secrets: [ docker_username, docker_password ]
kind: pipeline
type: docker
steps:
- name: go-build-1.11
image: golang:1.11
commands:
- go mod download
- CGO_ENABLED=0 go build -ldflags "-w -s" .
when:
event: tag
ref: refs/tags/*
docker-release-latest:
image: plugins/docker
repo: vmaple/rpdata-logbot3
tags: latest
secrets: [ docker_username, docker_password ]
event:
exclude:
- tag
- name: go-build-1.12
image: golang:1.12
commands:
- go mod download
- CGO_ENABLED=0 go build -ldflags "-w -s" .
when:
event: tag
ref: refs/tags/*
event:
exclude:
- tag
- name: go-build-1.13
image: golang:1.13
commands:
- go mod download
- CGO_ENABLED=0 go build -ldflags "-w -s" .
when:
event:
exclude:
- tag
- name: go-build-1.14
image: golang:1.14
commands:
- go mod download
- CGO_ENABLED=0 go build -ldflags "-w -s" .
when:
event:
exclude:
- tag
- name: go-test
image: golang:1.14
commands:
- go test -v ./...
- name: docker-release-tag
image: plugins/docker
settings:
auto_tag: true
username:
from_secret: docker_username
password:
from_secret: docker_password
repo: r.vmaple.dev/rpdata/logbot3
registry: r.vmaple.dev
when:
event:
- tag
- name: docker-release-latest
image: plugins/docker
settings:
auto_tag: false
username:
from_secret: docker_username
password:
from_secret: docker_password
repo: r.vmaple.dev/rpdata/logbot3
registry: r.vmaple.dev
when:
event:
- tag
+3 -1
View File
@@ -3,4 +3,6 @@ debug
debug.exe
logbot3
logbot3.exe
vendor/
# Idea
/.idea
+7 -9
View File
@@ -1,29 +1,27 @@
## 1. Build Server
# Use Go 1.11
FROM golang:1.11 AS build
# Use Go 1.14
FROM golang:1.14 AS build
# Load repository into docker
WORKDIR /go/src/git.aiterp.net/rpdata/logbot3/
WORKDIR /project
COPY . .
RUN rm -rf ./vendor
# Load and run dependencies
RUN go get -u github.com/golang/dep/cmd/dep
RUN dep ensure
RUN go get
# Build server and tools (CGO disabled to make them 100% static)
ENV CGO_ENABLED 0
RUN go build -a -installsuffix cgo -ldflags="-s -w" -o /binaries/logbot3 .
RUN go build -a -installsuffix cgo -ldflags="-s -w" -o logbot3 .
## 2. Distribute
# Use alpine linux
FROM alpine:3.8 as main-dist
FROM alpine:3.12 as main-dist
# Install certificates to enable HTTPS GET requests
RUN apk add --no-cache ca-certificates
# Copy build files into final container
COPY --from=build /binaries/* /usr/local/bin/
COPY --from=build /project/logbot3 /usr/local/bin/
# The server is the main entry point
CMD ["/usr/local/bin/logbot3"]
Generated
-26
View File
@@ -1,26 +0,0 @@
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
[[projects]]
branch = "master"
name = "git.aiterp.net/gisle/irc"
packages = [
".",
"ircutil",
"isupport",
"list"
]
revision = "b4c7c5145f2e2c430d6a74035751e6d59df7ec32"
[[projects]]
name = "github.com/dgrijalva/jwt-go"
packages = ["."]
revision = "06ea1031745cb8b3dab3f6a236daf2b0aa468b7e"
version = "v3.2.0"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
inputs-digest = "7ef4c9d0736b7eb8e596036446cae5d12a8d7485db1559cfa8ade7ba1bf74263"
solver-name = "gps-cdcl"
solver-version = 1
-38
View File
@@ -1,38 +0,0 @@
# Gopkg.toml example
#
# Refer to https://golang.github.io/dep/docs/Gopkg.toml.html
# for detailed Gopkg.toml documentation.
#
# required = ["github.com/user/thing/cmd/thing"]
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
#
# [[constraint]]
# name = "github.com/user/project"
# version = "1.0.0"
#
# [[constraint]]
# name = "github.com/user/project2"
# branch = "dev"
# source = "github.com/myfork/project2"
#
# [[override]]
# name = "github.com/x/y"
# version = "2.4.0"
#
# [prune]
# non-go = false
# go-tests = true
# unused-packages = true
[[constraint]]
branch = "master"
name = "git.aiterp.net/gisle/irc"
[[constraint]]
name = "github.com/dgrijalva/jwt-go"
version = "3.2.0"
[prune]
go-tests = true
unused-packages = true
+8
View File
@@ -0,0 +1,8 @@
module git.aiterp.net/rpdata/logbot3
go 1.13
require (
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/gissleh/irc v0.0.0-20200720191144-af5dc31ff33d
)
+6
View File
@@ -0,0 +1,6 @@
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/gissleh/irc v0.0.0-20200720173331-512ba5aa6fca h1:vTL3kM7PkVnTy77i2/ewaC0VDmSEF7sAOMDTNIs6rKY=
github.com/gissleh/irc v0.0.0-20200720173331-512ba5aa6fca/go.mod h1:h2/fPlbnb11gFpitk0HGxYLgK+bCKGh55QSYNo36X8M=
github.com/gissleh/irc v0.0.0-20200720191144-af5dc31ff33d h1:PbjXoijU290Y6qTD/LM9yyFTWgX8iJtPMQ4Adwsn954=
github.com/gissleh/irc v0.0.0-20200720191144-af5dc31ff33d/go.mod h1:h2/fPlbnb11gFpitk0HGxYLgK+bCKGh55QSYNo36X8M=
+58 -41
View File
@@ -3,11 +3,11 @@ package bot
import (
"context"
"log"
"strings"
"time"
"git.aiterp.net/gisle/irc"
"git.aiterp.net/rpdata/logbot3/internal/models"
"git.aiterp.net/rpdata/logbot3/internal/models/channels"
"github.com/gissleh/irc"
)
var botKey = "git.aiterp.net/rpdata/logbot.Bot.key"
@@ -33,6 +33,8 @@ func New(ctx context.Context, nick string, alternatives []string, user string, r
SkipSSLVerification: false,
})
client.AddHandler(handler)
bot := &Bot{
client: client,
channels: make(map[string]*Channel),
@@ -51,6 +53,8 @@ func (bot *Bot) Connect(server string, ssl bool, maxRetries int) (err error) {
}
bot.ctx, bot.ctxCancel = context.WithCancel(bot.client.Context())
bot.channels = make(map[string]*Channel)
retries := 0
for maxRetries == 0 || retries < maxRetries {
err = bot.client.Connect(server, ssl)
@@ -73,12 +77,16 @@ func (bot *Bot) Connect(server string, ssl bool, maxRetries int) (err error) {
return err
}
func (bot *Bot) ClientState() irc.ClientState {
return bot.client.State()
}
func (bot *Bot) addChannel(channelName string) *Channel {
if bot.channels[channelName] != nil {
return bot.channels[channelName]
}
channel := newChannel(bot.ctx, channelName, bot.client)
channel := newChannel(bot, channelName, bot.client)
go channel.loop()
bot.channels[channelName] = channel
@@ -93,57 +101,66 @@ func (bot *Bot) handlePost(channelName string, post ChannelPost) {
channel.ch <- post
}
func (bot *Bot) runCommands(commands []string, target irc.Target, replacers map[string]string) {
if replacers == nil {
replacers = make(map[string]string)
}
replacers["me"] = bot.client.Nick()
for _, command := range commands {
for from, to := range replacers {
command = strings.Replace(command, "%"+from, to, -1)
}
bot.client.EmitInput(command, target)
}
}
func (bot *Bot) loop() {
bot.loopCtx, bot.loopCancel = context.WithCancel(bot.ctx)
channelChanges, err := channels.SubscribeLogged(bot.ctx)
if err != nil {
log.Println("Failed to get channel changes:", err)
return
}
ticker := time.NewTicker(time.Second * 10)
defer ticker.Stop()
for {
select {
case channel := <-channelChanges:
case <-ticker.C:
{
channels := []models.Channel{channel}
deadline := time.After(time.Second * 1)
buffering := true
for buffering {
select {
case channel := <-channelChanges:
channels = append(channels, channel)
case <-deadline:
buffering = false
loggedChannels, err := channels.ListLogged(bot.ctx)
if err != nil {
log.Println("Failed to list logged channels:", err)
continue
}
loggedMap := make(map[string]bool)
for _, channel := range loggedChannels {
loggedMap[channel.Name] = true
}
clientMap := make(map[string]bool)
for _, channel := range bot.client.Channels() {
clientMap[channel.Name()] = true
}
joins := make([]string, 0, len(loggedMap))
parts := make([]string, 0, len(clientMap))
for key := range loggedMap {
if !clientMap[key] {
joins = append(joins, key)
}
}
for key := range clientMap {
if !loggedMap[key] {
parts = append(parts, key)
}
}
decisions := make(map[string]bool)
for _, channel := range channels {
decisions[channel.Name] = channel.Logged
}
joins := make([]string, 0, len(decisions))
parts := make([]string, 0, len(decisions))
for channelName, logged := range decisions {
if logged {
if bot.client.Channel(channelName) != nil {
continue
}
joins = append(joins, channelName)
} else {
if bot.client.Channel(channelName) == nil {
continue
}
parts = append(parts, channelName)
}
}
if len(joins) > 0 {
bot.client.Join(joins...)
}
if len(parts) > 0 {
log.Println("Leaving", strings.Join(parts, ", "))
bot.client.Part(parts...)
}
if len(joins) > 0 {
log.Println("Joining", strings.Join(joins, ", "))
bot.client.Join(joins...)
}
}
case <-bot.loopCtx.Done():
+48 -7
View File
@@ -6,7 +6,8 @@ import (
"strings"
"time"
"git.aiterp.net/gisle/irc"
"github.com/gissleh/irc"
"git.aiterp.net/rpdata/logbot3/internal/config"
"git.aiterp.net/rpdata/logbot3/internal/models/channels"
"git.aiterp.net/rpdata/logbot3/internal/models/logs"
"git.aiterp.net/rpdata/logbot3/internal/models/posts"
@@ -23,15 +24,17 @@ type Channel struct {
name string
parentCtx context.Context
parent *Bot
client *irc.Client
}
func newChannel(parentCtx context.Context, name string, client *irc.Client) *Channel {
ctx, cancel := context.WithCancel(context.Background())
func newChannel(parent *Bot, name string, client *irc.Client) *Channel {
ctx, cancel := context.WithCancel(parent.ctx)
return &Channel{
name: name,
parentCtx: parentCtx,
parent: parent,
parentCtx: parent.ctx,
client: client,
ch: make(chan ChannelPost, 8),
@@ -47,6 +50,7 @@ func (channel *Channel) loop() {
defer channel.cancel()
defer minutely.Stop()
// In case of a crash, it should take ownership of sessions when rejoining.
session, err := logs.FindOpen(channel.parentCtx, channel.name)
if err == nil {
minsUntilDeadline := 1 + int(((time.Hour * 2) - time.Since(session.LatestTime())).Minutes())
@@ -60,12 +64,27 @@ func (channel *Channel) loop() {
channel.lastPostTime = session.LatestTime()
}
// If the bot is op, run the commands from the configuration.
go func() {
time.Sleep(time.Second)
commands := config.Get().Commands.OnJoinOp
target := channel.client.Channel(channel.name)
if len(commands) > 0 && target != nil {
me, ok := target.UserList().User(channel.client.Nick())
if ok && strings.ContainsRune(me.Modes, 'o') {
channel.parent.runCommands(commands, target, map[string]string{
"chan": channel.name,
})
}
}
}()
for {
select {
case post := <-channel.ch:
{
log.Printf("Received %s post from %s", post.Kind, post.Nick)
// Handle bot commands, or add to queue otherwise.
if cmd := post.botCommand(); cmd != nil {
switch cmd.Verb {
@@ -107,6 +126,25 @@ func (channel *Channel) loop() {
log.Println("Could not set event name:", err)
}
}
case "register":
{
target := channel.client.Channel(channel.name)
if target != nil {
me, ok := target.UserList().User(channel.client.Nick())
if !ok || !strings.ContainsRune(me.Modes, 'o') {
channel.client.Say(channel.name, "This unit has insufficient privileges.")
break
}
chanServNick := config.Get().Names.ChanServ
if chanServNick == "" {
channel.client.Say(channel.name, "This function is not enabled.")
break
}
channel.client.Sayf(chanServNick, "REGISTER %s", target.Name())
}
}
}
} else {
queue = append(queue, post)
@@ -116,6 +154,9 @@ func (channel *Channel) loop() {
}
}
// Posts after here aren't going to be bot commands, so log its reception.
log.Printf("Received %s post from %s", post.Kind, post.Nick)
// Stop here if there's nothing to post.
if len(queue) == 0 {
break
@@ -225,7 +266,7 @@ func (channel *Channel) loop() {
channel.lastPostSessionID = ""
}
case <-channel.parentCtx.Done():
case <-channel.ctx.Done():
{
// Time to pack up shop.
return
+72 -20
View File
@@ -2,10 +2,12 @@ package bot
import (
"log"
"os"
"strings"
"git.aiterp.net/gisle/irc"
"github.com/gissleh/irc"
"git.aiterp.net/rpdata/logbot3/internal/config"
"git.aiterp.net/rpdata/logbot3/internal/util"
)
func handler(event *irc.Event, client *irc.Client) {
@@ -20,6 +22,8 @@ func handler(event *irc.Event, client *irc.Client) {
{
log.Println("Client is ready!")
go bot.loop()
bot.runCommands(config.Get().Commands.OnReady, bot.client.Status(), nil)
}
case "client.disconnect":
{
@@ -27,7 +31,17 @@ func handler(event *irc.Event, client *irc.Client) {
bot.stopLoop()
conf := config.Get().Server
go bot.Connect(conf.Address, conf.SSL, 0)
go bot.Connect(conf.Address, conf.SSL, 100)
}
// Log Connections
case "client.connecting":
{
log.Println("Client is connecting...")
}
case "client.connect":
{
log.Println("Client connected!")
}
// Log joins and leaves
@@ -50,54 +64,92 @@ func handler(event *irc.Event, client *irc.Client) {
case "ctcp.action", "packet.privmsg":
{
// The echo-message polyfill will return to sender.
if event.Nick == bot.client.Nick() {
break
}
// Only continue with channel messages, but log the others.
channel := event.ChannelTarget()
if channel == nil {
if event.Verb() == "action" {
log.Printf("Message: * %s %s", event.Nick, event.Text)
} else {
log.Printf("Message: <%s> %s", event.Nick, event.Text)
}
break
}
// Parse account, just in case it'll be useful one day.
account := ""
if user, ok := bot.client.FindUser(event.Nick); ok {
account = user.Account
}
// Get the kind of psot this is.
kind := ""
if event.Verb() == "action" {
if strings.HasPrefix(event.Nick, "=") || strings.HasPrefix(event.Nick, "\x1F=") {
kind = "scene"
} else if strings.ToLower(event.Verb()) == "action" {
kind = "action"
} else {
if strings.HasPrefix(event.Nick, "=") {
kind = "scene"
} else {
kind = "text"
}
kind = "text"
}
post := ChannelPost{
// Format NPC posts' nick and text.
nick := event.Nick
text := event.Text
if strings.HasPrefix(event.Nick, "\x1F") || kind == "scene" {
nick = strings.Replace(event.Nick, "\x1F", "", 2)
text = util.RemoveNPCAttribution(event.Text)
}
// Do not dispatch empty messages.
if len(text) < 1 || len(nick) < 1 || len(kind) < 1 {
log.Println("Ignoring empty message from:", event.Nick)
break
}
// Dispatch it.
bot.handlePost(channel.Name(), ChannelPost{
Account: account,
Kind: kind,
Nick: event.Nick,
Nick: nick,
Time: event.Time,
Text: event.Text,
}
bot.handlePost(channel.Name(), post)
Text: text,
})
}
// Log initial numerics for debugging's sake
// Log notices
case "packet.notice":
{
log.Printf("Message: -%s- %s", event.Nick, event.Text)
}
// Log initial numerics for debugging
case "packet.001", "packet.002", "packet.003", "packet.251", "packet.255", "packet.265", "packet.266", "packet.250", "packet.375", "packet.372", "packet.376":
{
log.Printf("(%s) %s\n", event.Verb(), event.Text)
}
case "packet.005", "packet.254":
case "packet.005", "packet.cap", "packet.254":
{
log.Printf("(%s) %s %s\n", event.Verb(), strings.Join(event.Args, " "), event.Text)
log.Printf("(%s) %s %s\n", event.Verb(), strings.Join(event.Args[1:], " "), event.Text)
}
case "packet.error":
{
log.Printf("(%s) %s\n", event.Verb(), event.Text)
os.Exit(1)
}
}
if event.Kind() == "error" {
log.Printf("Client Error (%s) %s\n", event.Verb(), event.Text)
}
// Always log error numerics.
if len(event.Verb()) == 3 && event.Verb()[0] == '4' || event.Verb()[0] == '9' {
log.Printf("(%s) %s %s\n", event.Verb(), strings.Join(event.Args, " "), event.Text)
}
}
func init() {
irc.Handle(handler)
}
+12 -1
View File
@@ -14,10 +14,14 @@ type Config struct {
User string `json:"user"`
RealName string `json:"realName"`
} `json:"bot"`
Debug struct {
Enabled bool `json:"enabled"`
Listen string `json:"listen"`
} `json:"debug"`
Server struct {
Address string `json:"server"`
SSL bool `json:"ssl"`
}
} `json:"server"`
API struct {
Endpoint string `json:"endpoint"`
Username string `json:"username"`
@@ -26,6 +30,13 @@ type Config struct {
Secret string `json:"secret"`
} `json:"key"`
} `json:"api"`
Commands struct {
OnJoinOp []string `json:"onJoinOp"`
OnReady []string `json:"onReady"`
} `json:"commands"`
Names struct {
ChanServ string `json:"chanserv"`
} `json:"names"`
}
var mutex sync.Mutex
+23
View File
@@ -0,0 +1,23 @@
package util
import (
"strings"
)
// RemoveNPCAttribution removes NPC attribution.
func RemoveNPCAttribution(str string) string {
lastOpen := strings.LastIndex(str, "(")
lastClose := strings.LastIndex(str, ")")
ending := len(str) - 1
if lastOpen > 0 && str[lastOpen-1] != '(' && (lastClose == -1 || lastClose == ending) {
cutoff := lastOpen
for cutoff > 1 && str[cutoff-1] == ' ' {
cutoff--
}
return str[:cutoff]
}
return str
}
+48
View File
@@ -0,0 +1,48 @@
package util_test
import (
"fmt"
"testing"
"git.aiterp.net/rpdata/logbot3/internal/util"
)
func TestRemoveNPCAttribution(t *testing.T) {
table := []struct {
Input string
Output string
}{
{
Input: "Character does stuff. (Player_Name)",
Output: "Character does stuff.",
},
{
Input: "Text (with some clarification) about stuff.",
Output: "Text (with some clarification) about stuff.",
},
{
Input: "Text without anything.",
Output: "Text without anything.",
},
{
Input: "((*Correction))",
Output: "((*Correction))",
},
{
Input: "((Remove :01 goofs!*))",
Output: "((Remove :01 goofs!*))",
},
}
for i, row := range table {
t.Run(fmt.Sprintf("Row_%d", i), func(t *testing.T) {
result := util.RemoveNPCAttribution(row.Input)
if result != row.Output {
t.Errorf("Row %d failed", i)
t.Errorf("Expected: %#+v", row.Output)
t.Errorf("Result: %#+v", result)
}
})
}
}
+19 -1
View File
@@ -2,7 +2,9 @@ package main
import (
"context"
"encoding/json"
"log"
"net/http"
"os"
"os/signal"
"strings"
@@ -10,7 +12,6 @@ import (
"git.aiterp.net/rpdata/logbot3/internal/bot"
"git.aiterp.net/rpdata/logbot3/internal/config"
"git.aiterp.net/rpdata/logbot3/internal/models/users"
)
@@ -32,6 +33,23 @@ func main() {
os.Exit(1)
}
if conf.Debug.Enabled {
mux := http.NewServeMux()
mux.HandleFunc("/state", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(200)
_ = json.NewEncoder(w).Encode(bot.ClientState())
})
go func() {
err := http.ListenAndServe(conf.Debug.Listen, mux)
if err != nil {
log.Println("Failed to setup debug listener", err)
}
}()
}
// Listen for a quit signal.
interrupt := make(chan os.Signal, 1)
signal.Notify(interrupt, os.Interrupt)