diff --git a/Makefile b/Makefile index 514d997..5cbe3e0 100644 --- a/Makefile +++ b/Makefile @@ -1,10 +1,8 @@ #!/usr/bin/env gmake -f BUILDOPTS=-ldflags="-s -w" -a -gcflags=all=-l -trimpath -FILELIST= bf.go buny.go commands.go collection.go event_parser.go globals.go lib.go main.go norm_string.go \ - read_config.go types.go - -BINARY=buny-jabber-bot +MYNAME=buny-jabber-bot +BINARY=$(MYNAME) # На windows имя бинарника может зависеть не только от платформы, но и от выбранной цели, для linux-а суффикс .exe # не нужен @@ -34,21 +32,21 @@ ifeq ($(OS),Windows_NT) # вариант с powershell на windows ifeq ($(SHELL),sh.exe) SET CGO_ENABLED=0 - go build ${BUILDOPTS} -o ${BINARY} ${FILELIST} + go build ${BUILDOPTS} -o ${BINARY} ./cmd/${MYNAME} else # вариант с jetbrains golang на windows CGO_ENABLED=0 - go build ${BUILDOPTS} -o ${BINARY} ${FILELIST} + go build ${BUILDOPTS} -o ${BINARY} ./cmd/${MYNAME} endif # вариант с bash/git (windows) и bash (linux) else - CGO_ENABLED=0 go build ${BUILDOPTS} -o ${BINARY} ${FILELIST} + CGO_ENABLED=0 go build ${BUILDOPTS} -o ${BINARY} ./cmd/${MYNAME} endif ## Удаляем бинарник средствами go clean: - go clean + $(RM) ${BINARY} ## Служебный таргет, для целей разработки. Обновляет завендоренные либы, брутальным образом. diff --git a/globals.go b/globals.go deleted file mode 100644 index ad3fbb9..0000000 --- a/globals.go +++ /dev/null @@ -1,90 +0,0 @@ -package main - -import ( - "os" - - "github.com/eleksir/go-xmpp" - "github.com/jbrukh/bayesian" - "gopkg.in/tomb.v1" -) - -const ( - Bad bayesian.Class = "Bad" - Good bayesian.Class = "Good" -) - -// Config - это у нас глобальная штука :) . -var config myConfig - -// Списки тех, кого мы не баним никогда. -var whiteList myWhiteList - -// Списки злодеев, которых мы баним. -var blackList myBlackList - -// Ставится в true, если мы получили сигнал на выключение. -var shutdown = false - -// Чтобы не организовывать драку за установку коннекта. -var connecting = false - -// Глобальное состояние соединения. -var isConnected = false - -// Канал, в который приходят уведомления для хэндлера сигналов от траппера сигналов. -var sigChan = make(chan os.Signal, 1) - -// Основной инстанс xmpp-клиента. -var talk *xmpp.Client - -// Опции подключения к xmpp-серверу. -var options *xmpp.Options - -// Список комнат, в которых находится бот. -var roomsConnected []string - -// Время последней активности, нужно для jabber:iq:last. -var lastActivity int64 - -// Время последней активности, нужно для c2s пингов - посылаем пинги, только если давненько ничего не приходило с -// сервера. -var lastServerActivity int64 - -// Время последней активности MUC-ов, нужно для пингов - посылаем пинги, только если давненько ничего не приходило из -// muc-ов. -var lastMucActivity *Collection - -// Получен ли ответ на запрос disco#info к серверу. -var serverCapsQueried bool - -// sync.Map-ка с капабилити сервера. -var serverCapsList *Collection - -// sync.Map-ка с комнатами и их capability. -var mucCapsList *Collection - -// Время, когда был отправлен c2s ping. -var serverPingTimestampTx int64 - -// Время, когда был принят s2c pong. -var serverPingTimestampRx int64 - -// Объектик для хранения стейта утилизатора горутинок. -var gTomb tomb.Tomb - -// sync.Map-ка со списком участников конференций (в json-формате, согласно структуре xmpp.Presence, "room".[]json). -var roomPresences *Collection - -// Переменные для простенького нормализатора текста. -var ( - // Знаки препинания раз. - pMarks = []string{".", ",", "!", "@", "#", "$", "%", "^", "&", "*", "(", ")", "{", "}", "<", ">", "[", "]", "\\"} - - // Знаки препинания два-с. - pMarks2 = []string{"-", "_", "+", "=", ":", ";", "'", "`", "~", "\""} - - // Символы новой строки. - newLines = []string{"\n", "\r", "\n\r", "\r\n"} -) - -/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */ diff --git a/go.mod b/go.mod index f68004b..612d637 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module buny-jabber-bot -go 1.22 +go 1.22.8 require ( github.com/davecgh/go-spew v1.1.1 @@ -8,12 +8,12 @@ require ( github.com/hjson/hjson-go v3.3.0+incompatible github.com/jbrukh/bayesian v0.0.0-20231117143245-13ae6f916c7a github.com/sirupsen/logrus v1.9.3 - golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 + golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 ) require ( - golang.org/x/crypto v0.25.0 // indirect - golang.org/x/sys v0.22.0 // indirect - golang.org/x/text v0.16.0 // indirect + golang.org/x/crypto v0.10.0 // indirect + golang.org/x/sys v0.9.0 // indirect + golang.org/x/text v0.10.0 // indirect ) diff --git a/go.sum b/go.sum index ec7b896..a8030c9 100644 --- a/go.sum +++ b/go.sum @@ -14,15 +14,15 @@ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVs github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= -golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= -golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= -golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= +golang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM= +golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I= +golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY= +golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= -golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s= +golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58= +golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= diff --git a/bf.go b/internal/jabber/bf.go similarity index 87% rename from bf.go rename to internal/jabber/bf.go index 23f3e9a..e86fee2 100644 --- a/bf.go +++ b/internal/jabber/bf.go @@ -1,4 +1,4 @@ -package main +package jabber import ( "bufio" @@ -12,9 +12,11 @@ import ( ) const ( - goodDictionaryFile string = "./data/good_dictionary.txt" - badDictionaryFile string = "./data/bad_dictionary.txt" - dataFile string = "./data/data.bin" + goodDictionaryFile string = "./data/good_dictionary.txt" + badDictionaryFile string = "./data/bad_dictionary.txt" + dataFile string = "./data/data.bin" + Bad bayesian.Class = "Bad" + Good bayesian.Class = "Good" ) // Выдаёт "очки похожести" для данной фразы. diff --git a/internal/jabber/black-whilte_lists.go b/internal/jabber/black-whilte_lists.go new file mode 100644 index 0000000..1a24360 --- /dev/null +++ b/internal/jabber/black-whilte_lists.go @@ -0,0 +1,204 @@ +package jabber + +import ( + "encoding/json" + "errors" + "fmt" + "os" + "path/filepath" + + "github.com/hjson/hjson-go" + log "github.com/sirupsen/logrus" +) + +// ReadWhitelist читает и валидирует белые списки пользователей. +func (j *Jabber) ReadWhitelist() error { + var ( + whitelistLoaded = false + err error + executablePath string + ) + + executablePath, err = os.Executable() + + if err != nil { + err = fmt.Errorf("unable to get current executable path: %w", err) + + return err + } + + whitelistJSONPath := fmt.Sprintf("%s/data/whitelist.json", filepath.Dir(executablePath)) + + locations := []string{ + "~/.buny-jabber-bot-whitelist.json", + "~/buny-jabber-bot-whitelist.json", + "/etc/buny-jabber-bot-whitelist.json", + whitelistJSONPath, + } + + for _, location := range locations { + fileInfo, err := os.Stat(location) + + // Предполагаем, что файла либо нет, либо мы не можем его прочитать, второе надо бы логгировать, но пока забьём + if err != nil { + continue + } + + // Файл белого списка длинноват для белого списка, попробуем следующего кандидата + if fileInfo.Size() > 2097152 { + log.Warnf("Whitelist file %s is too long for whitelist, skipping", location) + + continue + } + + buf, err := os.ReadFile(location) + + // Не удалось прочитать, попробуем следующего кандидата + if err != nil { + log.Warnf("Skip reading whitelist file %s: %s", location, err) + + continue + } + + // Исходя из документации, hjson какбы умеет парсить "кривой" json, но парсит его в map-ку. + // Интереснее на выходе получить структурку: то есть мы вначале конфиг преобразуем в map-ку, затем эту map-ку + // сериализуем в json, а потом json превращаем в структурку. Не очень эффективно, но он и нечасто требуется. + var ( + sampleWhitelist MyWhiteList + tmp map[string]interface{} + ) + + err = hjson.Unmarshal(buf, &tmp) + + // Не удалось распарсить - попробуем следующего кандидата + if err != nil { + log.Warnf("Skip parsing whitelist file %s: %s", location, err) + + continue + } + + tmpJSON, err := json.Marshal(tmp) + + // Не удалось преобразовать map-ку в json + if err != nil { + log.Warnf("Skip parsing whitelist file %s: %s", location, err) + + continue + } + + if err := json.Unmarshal(tmpJSON, &sampleWhitelist); err != nil { + log.Warnf("Skip parsing whitelist file %s: %s", location, err) + + continue + } + + j.WhiteList = sampleWhitelist + whitelistLoaded = true + + log.Infof("Using %s as whiteList file", location) + + break + } + + if !whitelistLoaded { + return errors.New("whitelist was not loaded") //nolint:goerr113 + } + + return err +} + +// ReadBlacklist читает и валидирует чёрные списки пользователей. +func (j *Jabber) ReadBlacklist() error { + var ( + blacklistLoaded = false + err error + executablePath string + ) + + executablePath, err = os.Executable() + + if err != nil { + err = fmt.Errorf("unable to get current executable path: %w", err) + + return err + } + + whitelistJSONPath := fmt.Sprintf("%s/data/blacklist.json", filepath.Dir(executablePath)) + + locations := []string{ + "~/.bunyPresense-jabber-bot-blacklist.json", + "~/bunyPresense-jabber-bot-blacklist.json", + "/etc/bunyPresense-jabber-bot-blacklist.json", + whitelistJSONPath, + } + + for _, location := range locations { + fileInfo, err := os.Stat(location) + + // Предполагаем, что файла либо нет, либо мы не можем его прочитать, второе надо бы логгировать, но пока забьём + if err != nil { + continue + } + + // Файл чёрного списка длинноват для чёрного списка, попробуем следующего кандидата + if fileInfo.Size() > 16777216 { + log.Warnf("Blacklist file %s is too long for blacklist, skipping", location) + + continue + } + + buf, err := os.ReadFile(location) + + // Не удалось прочитать, попробуем следующего кандидата + if err != nil { + log.Warnf("Skip reading blacklist file %s: %s", location, err) + + continue + } + + // Исходя из документации, hjson какбы умеет парсить "кривой" json, но парсит его в map-ку. + // Интереснее на выходе получить структурку: то есть мы вначале конфиг преобразуем в map-ку, затем эту map-ку + // сериализуем в json, а потом json превращаем в структурку. Не очень эффективно, но он и нечасто требуется. + var ( + sampleBlacklist MyBlackList + tmp map[string]interface{} + ) + + err = hjson.Unmarshal(buf, &tmp) + + // Не удалось распарсить - попробуем следующего кандидата + if err != nil { + log.Warnf("Skip parsing blacklist file %s: %s", location, err) + + continue + } + + tmpJSON, err := json.Marshal(tmp) + + // Не удалось преобразовать map-ку в json + if err != nil { + log.Warnf("Skip parsing blacklist file %s: %s", location, err) + + continue + } + + if err := json.Unmarshal(tmpJSON, &sampleBlacklist); err != nil { + log.Warnf("Skip parsing whitelist file %s: %s", location, err) + + continue + } + + j.BlackList = sampleBlacklist + blacklistLoaded = true + + log.Infof("Using %s as blacklist file", location) + + break + } + + if !blacklistLoaded { + return errors.New("blacklist was not loaded") //nolint:goerr113 + } + + return err +} diff --git a/buny.go b/internal/jabber/buny.go similarity index 72% rename from buny.go rename to internal/jabber/buny.go index c2ff976..71f0a15 100644 --- a/buny.go +++ b/internal/jabber/buny.go @@ -1,26 +1,25 @@ -package main +package jabber import ( "fmt" "regexp" "strings" - "time" "github.com/davecgh/go-spew/spew" "github.com/eleksir/go-xmpp" log "github.com/sirupsen/logrus" ) -// bunyPresense производит проверку по бело-чёрным спискам. Если presence пришёл от злодея (из чёрного списка), то +// BunyPresense производит проверку по бело-чёрным спискам. Если presence пришёл от злодея (из чёрного списка), то // отправляет его в бан. -func bunyPresense(v xmpp.Presence) error { //nolint:gocognit,gocyclo +func (j *Jabber) BunyPresense(v xmpp.Presence) error { //nolint:gocognit,gocyclo var err error // Если у presence-а есть JID и presence из одной из комнат, в которой мы есть и если его домен в чёрном // списке, заносим пидора в список outcast-ов if v.JID != "" { // На всякий случай: себя никогда не баним, явным образом - if v.JID == talk.JID() { + if v.JID == j.Talk.JID() { return err } @@ -35,7 +34,7 @@ func bunyPresense(v xmpp.Presence) error { //nolint:gocognit,gocyclo goodJid := strings.SplitN(v.JID, "/", 2)[0] // Обрабатываем правила белого списка - for _, good := range whiteList.Whitelist { + for _, good := range j.WhiteList.Whitelist { // Глобальный белый список if good.RoomName == "" { for _, person := range good.Jid { @@ -65,9 +64,9 @@ func bunyPresense(v xmpp.Presence) error { //nolint:gocognit,gocyclo } // Обрабатываем правила чёрного списка - for _, cRoom := range roomsConnected { + for _, cRoom := range j.RoomsConnected { if cRoom == room { - for _, bEntry := range blackList.Blacklist { + for _, bEntry := range j.BlackList.Blacklist { // Обработаем правила глобального чёрного списка if bEntry.RoomName == "" { for _, jidRegexp := range bEntry.JidRe { @@ -93,7 +92,7 @@ func bunyPresense(v xmpp.Presence) error { //nolint:gocognit,gocyclo jidRegexp, ) - id, err := squash(room, evilJid, bEntry.ReasonEnable, v.Type) + id, err := j.Squash(room, evilJid, bEntry.ReasonEnable, v.Type) if err != nil { err := fmt.Errorf( @@ -102,7 +101,7 @@ func bunyPresense(v xmpp.Presence) error { //nolint:gocognit,gocyclo err, ) - gTomb.Kill(err) + j.GTomb.Kill(err) } return err @@ -133,7 +132,7 @@ func bunyPresense(v xmpp.Presence) error { //nolint:gocognit,gocyclo ) // Баним именно jid - id, err := squash(room, evilJid, bEntry.ReasonEnable, v.Type) + id, err := j.Squash(room, evilJid, bEntry.ReasonEnable, v.Type) if err != nil { err := fmt.Errorf( @@ -142,7 +141,7 @@ func bunyPresense(v xmpp.Presence) error { //nolint:gocognit,gocyclo err, ) - gTomb.Kill(err) + j.GTomb.Kill(err) } return err @@ -177,7 +176,7 @@ func bunyPresense(v xmpp.Presence) error { //nolint:gocognit,gocyclo jidRegexp, ) - id, err := squash(room, evilJid, bEntry.ReasonEnable, v.Type) + id, err := j.Squash(room, evilJid, bEntry.ReasonEnable, v.Type) if err != nil { err := fmt.Errorf( @@ -186,7 +185,7 @@ func bunyPresense(v xmpp.Presence) error { //nolint:gocognit,gocyclo err, ) - gTomb.Kill(err) + j.GTomb.Kill(err) } return err @@ -218,7 +217,7 @@ func bunyPresense(v xmpp.Presence) error { //nolint:gocognit,gocyclo ) // Баним именно jid - id, err := squash(room, evilJid, bEntry.ReasonEnable, v.Type) + id, err := j.Squash(room, evilJid, bEntry.ReasonEnable, v.Type) if err != nil { err := fmt.Errorf( @@ -227,7 +226,7 @@ func bunyPresense(v xmpp.Presence) error { //nolint:gocognit,gocyclo err, ) - gTomb.Kill(err) + j.GTomb.Kill(err) } return err @@ -245,9 +244,9 @@ func bunyPresense(v xmpp.Presence) error { //nolint:gocognit,gocyclo return err } -// bunyChat производит проверку сообщений участников чата по списку забаненных фраз и в случае нахождения запрещённого +// BunyChat производит проверку сообщений участников чата по списку забаненных фраз и в случае нахождения запрещённого // шаблона банит участника чата. -func bunyChat(v xmpp.Chat) error { +func (j *Jabber) BunyChat(v xmpp.Chat) error { var ( room = (strings.SplitN(v.Remote, "/", 2))[0] // nick = (strings.SplitN(v.Remote, "/", 2))[1] @@ -255,10 +254,10 @@ func bunyChat(v xmpp.Chat) error { ) // Действовать мы можем только в рамках тех комнат, где явно присуствуем. - for _, cRoom := range roomsConnected { + for _, cRoom := range j.RoomsConnected { if cRoom == room { // Перебирём правила чёрных списков. - for _, bEntry := range blackList.Blacklist { + for _, bEntry := range j.BlackList.Blacklist { // Обработаем правила глобального чёрного списка if bEntry.RoomName == "" { for _, phraseRegexp := range bEntry.PhraseRe { @@ -277,7 +276,7 @@ func bunyChat(v xmpp.Chat) error { log.Debugf("Checking phrase %s vs room %s blacklist regex %s", v.Text, room, phraseRegexp) if re.MatchString(v.Text) { - realJID := getRealJIDfromNick(v.Remote) + realJID := j.GetRealJIDfromNick(v.Remote) log.Warnf( "Hammer falls on %s (%s): phrase matches with global blacklist entry: %s vs %s", @@ -287,14 +286,14 @@ func bunyChat(v xmpp.Chat) error { phraseRegexp, ) - if id, err := squash(room, realJID, bEntry.ReasonEnable, v.Type); err != nil { + if id, err := j.Squash(room, realJID, bEntry.ReasonEnable, v.Type); err != nil { err := fmt.Errorf( "unable to ban user: id=%s, err=%w", id, err, ) - gTomb.Kill(err) + j.GTomb.Kill(err) } return err @@ -320,7 +319,7 @@ func bunyChat(v xmpp.Chat) error { log.Debugf("Checking phrase %s vs room %s blacklist regex %s", v.Text, room, phraseRegexp) if re.MatchString(v.Text) { - realJID := getRealJIDfromNick(v.Remote) + realJID := j.GetRealJIDfromNick(v.Remote) log.Warnf( "Hammer falls on %s (%s): phrase matches with room blacklist entry: %s vs %s", @@ -330,14 +329,14 @@ func bunyChat(v xmpp.Chat) error { phraseRegexp, ) - if id, err := squash(room, realJID, bEntry.ReasonEnable, v.Type); err != nil { + if id, err := j.Squash(room, realJID, bEntry.ReasonEnable, v.Type); err != nil { err := fmt.Errorf( "unable to ban user: id=%s, err=%w", id, err, ) - gTomb.Kill(err) + j.GTomb.Kill(err) } return err @@ -347,7 +346,7 @@ func bunyChat(v xmpp.Chat) error { } // Если включено, проверяем фразу на КАПС. - for _, channel := range config.Jabber.Channels { + for _, channel := range j.C.Jabber.Channels { if channel.Name == room && channel.AllCaps.Enabled { // Нормализуем строку и вырежем из неё пробелы normPhrase := strings.ReplaceAll(nString(v.Text), " ", "") @@ -357,8 +356,8 @@ func bunyChat(v xmpp.Chat) error { normPhraseUpper := strings.ReplaceAll(nStringUpper(v.Text), " ", "") if normPhrase == normPhraseUpper { - realJID := getRealJIDfromNick(v.Remote) - id, err := squash(room, realJID, false, v.Type) + realJID := j.GetRealJIDfromNick(v.Remote) + id, err := j.Squash(room, realJID, false, v.Type) if err != nil { err := fmt.Errorf( @@ -367,7 +366,7 @@ func bunyChat(v xmpp.Chat) error { err, ) - gTomb.Kill(err) + j.GTomb.Kill(err) } } } @@ -381,74 +380,4 @@ func bunyChat(v xmpp.Chat) error { return err } -// squash банит указанный jid в указанной комнате. -// reasonEnable указывает, надо ли писать дату автобана в банлисте в поле reason (это единственная причина, в которую -// умеет бот). -func squash(room, jid string, reasonEnable bool, vType string) (string, error) { - var ( - id string - err error - ) - - if config.Jabber.BanPhrasesEnable { - phrase := randomPhrase(config.Jabber.BanPhrases) - - if _, err = talk.Send( - xmpp.Chat{ //nolint:exhaustruct - Remote: room, - Text: phrase, - Type: vType, - }, - ); err != nil { - err = fmt.Errorf("unable to send phrase to room %s: %w", room, err) - - // Здесь возвращаем nil, т.к. за нас ошибку залоггирует код выше - return id, err - } - } - - // https://xmpp.org/extensions/xep-0045.html#ban баним вот таким сообщением - ban := "" - - if reasonEnable { - var t = time.Now() - ban += fmt.Sprintf( - "autoban at %04d.%02d.%02d %02d:%02d:%02d", - t.Year(), - t.Month(), - t.Day(), - t.Hour(), - t.Minute(), - t.Second(), - ) - } else { - ban += "" - } - - ban += "" - - // Выжидаем некоторое время перед баном. А то можно настолько рано забанить, что сервер не внесёт злодея в банлист - // комнаты и пришлёт affiliation: none вместо affiliation: outcast. - if config.Jabber.BanDelay > 0 { - time.Sleep(time.Duration(config.Jabber.BanDelay) * time.Millisecond) - } - - if id, err = talk.RawInformationQuery( - talk.JID(), - room, - "ban1", - xmpp.IQTypeSet, - "http://jabber.org/protocol/muc#admin", - ban, - ); err != nil { - err = fmt.Errorf( - "unable to ban user: id=%s, err=%w", - id, - err, - ) - } - - return id, err -} - /* vim: set ft=go noet ai ts=4 sw=4 sts=4: */ diff --git a/collection.go b/internal/jabber/collection.go similarity index 99% rename from collection.go rename to internal/jabber/collection.go index 9ec783a..653d6a7 100644 --- a/collection.go +++ b/internal/jabber/collection.go @@ -1,4 +1,4 @@ -package main +package jabber import ( "sync" diff --git a/commands.go b/internal/jabber/commands.go similarity index 76% rename from commands.go rename to internal/jabber/commands.go index 87cd92c..6e79402 100644 --- a/commands.go +++ b/internal/jabber/commands.go @@ -1,4 +1,4 @@ -package main +package jabber import ( "fmt" @@ -10,29 +10,29 @@ import ( log "github.com/sirupsen/logrus" ) -// cmd парсит команды из чятика. -func cmd(v xmpp.Chat) error { +// Cmd парсит команды из чятика. +func (j *Jabber) Cmd(v xmpp.Chat) error { var err error switch { - case v.Text == fmt.Sprintf("%shelp", config.CSign) || v.Text == fmt.Sprintf("%sпомощь", config.CSign): + case v.Text == fmt.Sprintf("%shelp", j.C.CSign) || v.Text == fmt.Sprintf("%sпомощь", j.C.CSign): var ( answer string chosenOneTalks = false ) - realJID := getRealJIDfromNick(v.Remote) + realJID := j.GetRealJIDfromNick(v.Remote) - for _, master := range config.Jabber.BotMasters { + for _, master := range j.C.Jabber.BotMasters { if (strings.SplitN(realJID, "/", 2))[0] == master { chosenOneTalks = true } } if chosenOneTalks { - answer = fmt.Sprintf("%sпомощь - этот список команд\n", config.CSign) - answer += fmt.Sprintf("%shelp - this commands list\n", config.CSign) - answer += fmt.Sprintf("%srehash - reload white and black lists", config.CSign) + answer = fmt.Sprintf("%sпомощь - этот список команд\n", j.C.CSign) + answer += fmt.Sprintf("%shelp - this commands list\n", j.C.CSign) + answer += fmt.Sprintf("%srehash - reload white and black lists", j.C.CSign) } else { answer = "Ничем помочь не могу. Луна не светит на тебя." } @@ -43,7 +43,7 @@ func cmd(v xmpp.Chat) error { dest = (strings.SplitN(v.Remote, "/", 2))[0] } - if _, err := talk.Send( + if _, err := j.Talk.Send( xmpp.Chat{ //nolint:exhaustruct Remote: dest, Text: strings.TrimSpace(answer), @@ -55,7 +55,7 @@ func cmd(v xmpp.Chat) error { return err } - case v.Text == fmt.Sprintf("%srehash", config.CSign): //nolint:wsl + case v.Text == fmt.Sprintf("%srehash", j.C.CSign): //nolint:wsl /* Эта команда может прилетать из приватной беседы с realjid-ом, из приватной беседы с chat-nick-ом, а также из * чятика. * Groupchat мы можем определить по v.Type (groupchat) и тогда понятно что делать. @@ -72,9 +72,9 @@ func cmd(v xmpp.Chat) error { listsLoaded = true ) - realJID := getRealJIDfromNick(v.Remote) + realJID := j.GetRealJIDfromNick(v.Remote) - for _, master := range config.Jabber.BotMasters { + for _, master := range j.C.Jabber.BotMasters { if (strings.SplitN(realJID, "/", 2))[0] == master { chosenOneTalks = true } @@ -83,7 +83,7 @@ func cmd(v xmpp.Chat) error { if !chosenOneTalks { log.Infof( "Command %srehash given by non-bot_master user %s(%s), ignoring", - config.CSign, + j.C.CSign, realJID, v.Remote, ) @@ -91,7 +91,7 @@ func cmd(v xmpp.Chat) error { return err } - if err := readWhitelist(); err != nil { + if err := j.ReadWhitelist(); err != nil { listsLoaded = false var msg xmpp.Chat @@ -99,12 +99,12 @@ func cmd(v xmpp.Chat) error { msg.Type = "chat" // private message msg.Text = fmt.Sprint(err) - if _, err := talk.Send(msg); err != nil { + if _, err := j.Talk.Send(msg); err != nil { return err } } - if err := readBlacklist(); err != nil { + if err := j.ReadBlacklist(); err != nil { listsLoaded = false var msg xmpp.Chat @@ -112,7 +112,7 @@ func cmd(v xmpp.Chat) error { msg.Type = "chat" // private message msg.Text = fmt.Sprint(err) - if _, err := talk.Send(msg); err != nil { + if _, err := j.Talk.Send(msg); err != nil { return err } } @@ -125,7 +125,7 @@ func cmd(v xmpp.Chat) error { msg.Type = v.Type msg.Text = "Сделано" - if _, err := talk.Send(msg); err != nil { + if _, err := j.Talk.Send(msg); err != nil { return err } } @@ -133,16 +133,16 @@ func cmd(v xmpp.Chat) error { room := (strings.SplitN(v.Remote, "/", 2))[0] // Реагировать на приватную команду "из комнаты", только если бот в комнате - if slices.Contains(roomsConnected, room) { + if slices.Contains(j.RoomsConnected, room) { var ( chosenOneTalks = false listsLoaded = true ) // Реагировать на rehash только если собеседник является bot master-ом - realJID := getRealJIDfromNick(v.Remote) + realJID := j.GetRealJIDfromNick(v.Remote) - for _, master := range config.Jabber.BotMasters { + for _, master := range j.C.Jabber.BotMasters { if (strings.SplitN(realJID, "/", 2))[0] == master { chosenOneTalks = true } @@ -151,7 +151,7 @@ func cmd(v xmpp.Chat) error { if !chosenOneTalks { log.Infof( "Command %srehash given by non-bot_master user %s(%s), ignoring", - config.CSign, + j.C.CSign, realJID, v.Remote, ) @@ -159,7 +159,7 @@ func cmd(v xmpp.Chat) error { return err } - if err := readWhitelist(); err != nil { + if err := j.ReadWhitelist(); err != nil { listsLoaded = false var msg xmpp.Chat @@ -167,12 +167,12 @@ func cmd(v xmpp.Chat) error { msg.Type = v.Type msg.Text = fmt.Sprint(err) - if _, err := talk.Send(msg); err != nil { + if _, err := j.Talk.Send(msg); err != nil { return err } } - if err := readBlacklist(); err != nil { + if err := j.ReadBlacklist(); err != nil { listsLoaded = false var msg xmpp.Chat @@ -180,7 +180,7 @@ func cmd(v xmpp.Chat) error { msg.Type = v.Type msg.Text = fmt.Sprint(err) - if _, err := talk.Send(msg); err != nil { + if _, err := j.Talk.Send(msg); err != nil { return err } } @@ -191,7 +191,7 @@ func cmd(v xmpp.Chat) error { msg.Type = v.Type msg.Text = "Сделано" - if _, err := talk.Send(msg); err != nil { + if _, err := j.Talk.Send(msg); err != nil { return err } } diff --git a/event_parser.go b/internal/jabber/event_parser.go similarity index 83% rename from event_parser.go rename to internal/jabber/event_parser.go index 1ab551f..488af16 100644 --- a/event_parser.go +++ b/internal/jabber/event_parser.go @@ -1,4 +1,4 @@ -package main +package jabber import ( "encoding/json" @@ -15,9 +15,9 @@ import ( "golang.org/x/exp/slices" ) -// parseEvent парсит ивенты, прилетающие из модуля xmpp (изменения presence, фразы участников, ошибки). -func parseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo - lastServerActivity = time.Now().Unix() +// ParseEvent парсит ивенты, прилетающие из модуля xmpp (изменения presence, фразы участников, ошибки). +func (j *Jabber) ParseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo + j.LastServerActivity = time.Now().Unix() switch v := e.(type) { // Сообщение в чяти @@ -40,28 +40,28 @@ func parseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo log.Debugf("Message from public chat: %s", v.Text) - if nick == getBotNickFromRoomConfig(room) { + if nick == j.GetBotNickFromRoomConfig(room) { log.Debug("Skipping message from myself") return } - if err := cmd(v); err != nil { - gTomb.Kill(err) + if err := j.Cmd(v); err != nil { + j.GTomb.Kill(err) return } - if err := bunyChat(v); err != nil { - gTomb.Kill(err) + if err := j.BunyChat(v); err != nil { + j.GTomb.Kill(err) return } - lastActivity = lastServerActivity + j.LastActivity = j.LastServerActivity if muc, _ := strings.CutSuffix(v.Remote, "/"); muc != "" { - lastMucActivity.Set(muc, lastServerActivity) + j.LastMucActivity.Set(muc, j.LastServerActivity) } // Приватный чятик @@ -71,13 +71,13 @@ func parseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo // eleksir@jabber.ru/array.lan - это если мы работаем через ростер log.Debugf("Private message: %s", v.Text) - if err := cmd(v); err != nil { - gTomb.Kill(err) + if err := j.Cmd(v); err != nil { + j.GTomb.Kill(err) return } - lastActivity = lastServerActivity + j.LastActivity = j.LastServerActivity // Внезапно, ошибки. По идее они должны ассоциироваться с отправляемыми сообщениями, но по ходу это // не реализовано, поэтому мы получаем ошибки в форме отдельного чятика @@ -102,13 +102,13 @@ func parseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo log.Debug("Looks like IQ get query") if muc, _ := strings.CutSuffix(v.To, "/"); muc != "" { - if slices.Contains(roomsConnected, muc) { - lastMucActivity.Set(muc, lastServerActivity) + if slices.Contains(j.RoomsConnected, muc) { + j.LastMucActivity.Set(muc, j.LastServerActivity) } } var ( - iqStruct jabberSimpleIqGetQuery + iqStruct JabberSimpleIqGetQuery parseError bool ) @@ -119,7 +119,7 @@ func parseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo case "jabber:iq:version": log.Infof("Got IQ get request for version from %s", v.From) - if id, err := talk.IqVersionResponse( + if id, err := j.Talk.IqVersionResponse( v, "buny-jabber-bot", "1.dev", @@ -131,7 +131,7 @@ func parseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo err, ) - gTomb.Kill(err) + j.GTomb.Kill(err) return } @@ -142,14 +142,14 @@ func parseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo case "urn:xmpp:ping": log.Infof("Got IQ get request for pong from %s", v.From) - if id, err := talk.PingResponse(v); err != nil { + if id, err := j.Talk.PingResponse(v); err != nil { err := fmt.Errorf( "unable to send pong to jabber server: id=%s, err=%w", id, err, ) - gTomb.Kill(err) + j.GTomb.Kill(err) return } @@ -161,14 +161,14 @@ func parseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo case "jabber:iq:last": log.Infof("Got IQ get last activity time request from %s", v.From) - if id, err := talk.JabberIqLastResponse(v, lastActivity); err != nil { + if id, err := j.Talk.JabberIqLastResponse(v, j.LastActivity); err != nil { err := fmt.Errorf( "unable to send last activity time to jabber server: id=%s, err=%w", id, err, ) - gTomb.Kill(err) + j.GTomb.Kill(err) return } @@ -187,7 +187,7 @@ func parseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo answer += "" answer += "" - if id, err := talk.RawInformation( + if id, err := j.Talk.RawInformation( v.To, v.From, v.ID, @@ -200,7 +200,7 @@ func parseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo err, ) - gTomb.Kill(err) + j.GTomb.Kill(err) return } @@ -211,7 +211,7 @@ func parseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo case xmpp.XMPPNS_DISCO_ITEMS: log.Infof("Got IQ get disco#items request from %s, answer service unavailable", v.From) - if _, err := talk.ErrorServiceUnavailable( + if _, err := j.Talk.ErrorServiceUnavailable( v, "http://jabber.org/protocol/disco#info", "http://jabber.org/protocol/commands", @@ -222,7 +222,7 @@ func parseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo err, ) - gTomb.Kill(err) + j.GTomb.Kill(err) return } @@ -242,21 +242,21 @@ func parseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo // Попробуем распарсить входящий запрос как urn:xmpp:time if parseError { - var iqStruct jabberTimeIqGetQuery + var iqStruct JabberTimeIqGetQuery if err := xml.Unmarshal(v.Query, &iqStruct); err == nil { switch iqStruct.Xmlns { case "urn:xmpp:time": log.Infof("Got IQ get time request from %s", v.From) - if id, err := talk.UrnXMPPTimeResponse(v, "+00:00"); err != nil { + if id, err := j.Talk.UrnXMPPTimeResponse(v, "+00:00"); err != nil { err := fmt.Errorf( "unable to send urn:xmpp:time to jabber server: id=%s, err=%w", id, err, ) - gTomb.Kill(err) + j.GTomb.Kill(err) return } @@ -276,14 +276,14 @@ func parseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo // Попробуем распарсить входящий запрос как http://jabber.org/protocol/pubsub if parseError { - var iqStruct jabberPubsubIQGetQuery + var iqStruct JabberPubsubIQGetQuery if err := xml.Unmarshal(v.Query, &iqStruct); err == nil { switch iqStruct.Xmlns { case xmpp.XMPPNS_PUBSUB: log.Infof("Got IQ get pubsub request from %s, answer feature unimplemented", v.From) - if id, err := talk.ErrorNotImplemented( + if id, err := j.Talk.ErrorNotImplemented( v, "http://jabber.org/protocol/pubsub#errors", "subscribe", @@ -293,7 +293,7 @@ func parseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo err, ) - gTomb.Kill(err) + j.GTomb.Kill(err) return } @@ -314,19 +314,19 @@ func parseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo // Предполагается, что такие респонсы должны приходить только для пинга без серверной оптимизации. // Хотя идейно мы не поддерживаем работу без серверной оптимизации, но на пинг ответим, нам несложно. if parseError { - var iqStruct jabberIqPing + var iqStruct JabberIqPing if err := xml.Unmarshal(v.Query, &iqStruct); err == nil { log.Debugf("Got IQ get request (actually, response) for MUC Self-Ping from %s", v.From) - if id, err := talk.PingResponse(v); err != nil { + if id, err := j.Talk.PingResponse(v); err != nil { err := fmt.Errorf( "unable to send pong to jabber server: id=%s, err=%w", id, err, ) - gTomb.Kill(err) + j.GTomb.Kill(err) return } @@ -343,28 +343,28 @@ func parseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo case xmpp.IQTypeResult: if muc, _ := strings.CutSuffix(v.To, "/"); muc != "" { - if slices.Contains(roomsConnected, muc) { - lastMucActivity.Set(muc, lastServerActivity) + if slices.Contains(j.RoomsConnected, muc) { + j.LastMucActivity.Set(muc, j.LastServerActivity) } } switch { // Похоже на pong от сервера (по стандарту в ответе нету query, но go-xmpp нам подсовывает это) - case v.From == config.Jabber.Server && v.To == talk.JID() && string(v.Query) == "": + case v.From == j.C.Jabber.Server && v.To == j.Talk.JID() && string(v.Query) == "": log.Debugf("Got S2C pong answer from %s to %s", v.From, v.To) - serverPingTimestampRx = time.Now().Unix() //nolint:wsl + j.ServerPingTimestampRx = time.Now().Unix() //nolint:wsl // Похоже на понг второй стадии xep-0410 MUC-Ping-а, который у нас не реализован - case v.To == talk.JID() && string(v.Query) == "": - mucNameMatch := slices.Contains(roomsConnected, v.From) + case v.To == j.Talk.JID() && string(v.Query) == "": + mucNameMatch := slices.Contains(j.RoomsConnected, v.From) if mucNameMatch { log.Debugf("Got server-optimized MUC pong answer (xep-0410) from %s to %s", v.From, v.To) } else { mucNickMatch := false - for _, room := range roomsConnected { - mucNick := fmt.Sprintf("%s/%s", room, getBotNickFromRoomConfig(room)) + for _, room := range j.RoomsConnected { + mucNick := fmt.Sprintf("%s/%s", room, j.GetBotNickFromRoomConfig(room)) if v.From == mucNick { mucNickMatch = true @@ -383,15 +383,15 @@ func parseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo } // Подтверждение бана - case v.To == talk.JID() && v.ID == "ban1": + case v.To == j.Talk.JID() && v.ID == "ban1": mucNickMatch := false // Если ник, которому предназначается сообщение, совпадает с ником, из конфига бота (глобального или для // комнаты), то считаем, что мы есть в этой комнате. Потому что в противном случае в roomsConnected не // будет искомой комнаты и мы не сможем составить ник бота, чтобы сравнить его с тем, кому адресовано // уведомление. - for _, room := range roomsConnected { - mucNick := fmt.Sprintf("%s/%s", room, getBotNickFromRoomConfig(room)) + for _, room := range j.RoomsConnected { + mucNick := fmt.Sprintf("%s/%s", room, j.GetBotNickFromRoomConfig(room)) if v.To == mucNick { mucNickMatch = true @@ -403,7 +403,7 @@ func parseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo // Формально, ответ должен парситься как пустой result и совпадать с типом jabberSimpleIqGetQuery // Ответ приходит с 2 xmlns, похоже, он парсится неправильно в go-xmpp. // TODO: после исправления go-xmpp, надо исправить это тут - var iqStruct jabberSimpleIqGetQuery + var iqStruct JabberSimpleIqGetQuery if err := xml.Unmarshal(v.Query, &iqStruct); err == nil { if iqStruct.Xmlns == "http://jabber.org/protocol/muc#admin" && iqStruct.Text == "" { @@ -432,14 +432,14 @@ func parseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo // Этот бот не управляется со стороны сервера, поэтому все попытки порулить игнорируем case xmpp.IQTypeSet: if muc, _ := strings.CutSuffix(v.To, "/"); muc != "" { - if slices.Contains(roomsConnected, muc) { - lastMucActivity.Set(muc, lastServerActivity) + if slices.Contains(j.RoomsConnected, muc) { + j.LastMucActivity.Set(muc, j.LastServerActivity) } } log.Info("Got an IQ request for set something. Answer not implemented") - if id, err := talk.ErrorNotImplemented( + if id, err := j.Talk.ErrorNotImplemented( v, "http://jabber.org/protocol/commands", xmpp.IQTypeSet, @@ -450,7 +450,7 @@ func parseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo err, ) - gTomb.Kill(err) + j.GTomb.Kill(err) return } @@ -462,18 +462,18 @@ func parseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo // Если сервер не хочет пинговаться и отвечает ошибкой на пинг, то наверно он не умеет в пинги, // хотя если мы его пингуем, значит он анонсировал такой capability. Вот, засранец! var ( - iqPingStruct jabberIqPing + iqPingStruct JabberIqPing parseError = true ) if err := xml.Unmarshal(v.Query, &iqPingStruct); err == nil { if iqPingStruct.Xmlns == "urn:xmpp:ping" { - if v.From == config.Jabber.Server { + if v.From == j.C.Jabber.Server { msg := "Server announced that it can answer c2s ping, but gives us an error to such query, " msg += "fallback to keepalive whitespace pings" log.Error(msg) - serverCapsList.Set("urn:xmpp:ping", false) + j.ServerCapsList.Set("urn:xmpp:ping", false) } else { log.Errorf("Got 'ping unsupported' message from: %s to: %s", v.From, v.To) } @@ -487,30 +487,30 @@ func parseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo // Это у нас пинг xep-0410 и мы не в комнате, предполагается, что надо бы заджойниться if parseError { - var iqErrorCancelNotAcceptable jabberIqErrorCancelNotAcceptable + var iqErrorCancelNotAcceptable JabberIqErrorCancelNotAcceptable if err := xml.Unmarshal(v.Query, &iqErrorCancelNotAcceptable); err == nil { - if v.To == talk.JID() { + if v.To == j.Talk.JID() { nick := strings.SplitN(v.From, "/", 2)[1] room := strings.SplitN(v.From, "/", 2)[0] - if slices.Contains(roomsConnected, iqErrorCancelNotAcceptable.By) && - nick == getBotNickFromRoomConfig(room) { + if slices.Contains(j.RoomsConnected, iqErrorCancelNotAcceptable.By) && + nick == j.GetBotNickFromRoomConfig(room) { log.Errorf( "Got Iq error message from: %s to: %s. Looks like i'm not in MUC anymore", v.From, v.To, ) - time.Sleep(time.Duration(config.Jabber.MucRejoinDelay) * time.Second) + time.Sleep(time.Duration(j.C.Jabber.MucRejoinDelay) * time.Second) - if _, err := talk.JoinMUCNoHistory(iqErrorCancelNotAcceptable.By, nick); err != nil { + if _, err := j.Talk.JoinMUCNoHistory(iqErrorCancelNotAcceptable.By, nick); err != nil { err := fmt.Errorf( "looks like connection to server also lost err=%w", err, ) - gTomb.Kill(err) + j.GTomb.Kill(err) return } @@ -546,8 +546,8 @@ func parseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo // Смена статуса участника case xmpp.Presence: if muc, _ := strings.CutSuffix(v.To, "/"); muc != "" { - if slices.Contains(roomsConnected, muc) { - lastMucActivity.Set(muc, lastServerActivity) + if slices.Contains(j.RoomsConnected, muc) { + j.LastMucActivity.Set(muc, j.LastServerActivity) } } @@ -559,12 +559,12 @@ func parseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo // Однако, это не значит, что такую ситуацию мы не должны корректным образом обрабатывать. if v.Type == "unavailable" { // Считаем, что мы больше не в комнате, поэтому не знаем, кто там есть - roomPresences.Delete(v.From) + j.RoomPresences.Delete(v.From) log.Error("Presence notification - looks like another instance of client leaves room") - if slices.Contains(roomsConnected, v.From) { - go joinMuc(v.From) + if slices.Contains(j.RoomsConnected, v.From) { + go j.JoinMuc(v.From) } } else { log.Errorf("Presence notification, Type: %s, From: %s, Show: %s, Status: %s", @@ -588,19 +588,19 @@ func parseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo // Это наш собственный Presence if v.Show == "" && v.Status == "" { - if nick == getBotNickFromRoomConfig(room) { - roomsConnected = append(roomsConnected, room) + if nick == j.GetBotNickFromRoomConfig(room) { + j.RoomsConnected = append(j.RoomsConnected, room) // На всякий случай дедуплицируем список комнат, к которым мы заджойнились. - sort.Strings(roomsConnected) - slices.Compact(roomsConnected) + sort.Strings(j.RoomsConnected) + slices.Compact(j.RoomsConnected) } } switch v.Role { // Участник ушёл case "none": - if presenceJSONInterface, present := roomPresences.Get(room); present { - presenceJSONStrings := interfaceToStringSlice(presenceJSONInterface) + if presenceJSONInterface, present := j.RoomPresences.Get(room); present { + presenceJSONStrings := InterfaceToStringSlice(presenceJSONInterface) var newPresenceJSONStrings []string @@ -615,7 +615,7 @@ func parseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo newPresenceJSONStrings = append(newPresenceJSONStrings, presenceJSONstring) } - roomPresences.Set(room, newPresenceJSONStrings) + j.RoomPresences.Set(room, newPresenceJSONStrings) } // Участник пришёл default: @@ -625,8 +625,8 @@ func parseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo presenceJSONBytes []byte ) - if presenceJSONInterface, present := roomPresences.Get(room); present { - presenceJSONStrings = interfaceToStringSlice(presenceJSONInterface) + if presenceJSONInterface, present := j.RoomPresences.Get(room); present { + presenceJSONStrings = InterfaceToStringSlice(presenceJSONInterface) } for _, presenceJSONString := range presenceJSONStrings { @@ -644,16 +644,16 @@ func parseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo presenceJSONBytes, _ = json.Marshal(v) //nolint:errchkjson newPresenceJSONStrings = append(newPresenceJSONStrings, string(presenceJSONBytes)) - roomPresences.Set(room, newPresenceJSONStrings) + j.RoomPresences.Set(room, newPresenceJSONStrings) } // Проверяем, а не злодей ли зашёл? Сделать это мы можем, только если мы находимся в комнате. // По правилам, мы можем что-то делать, только после того, как нам прилетит наш собственный presence, это // значит, что мы вошли в комнату. - if slices.Contains(roomsConnected, room) { + if slices.Contains(j.RoomsConnected, room) { if v.Affiliation != "outcast" { - if err := bunyPresense(v); err != nil { - gTomb.Kill(err) + if err := j.BunyPresense(v); err != nil { + j.GTomb.Kill(err) return } @@ -664,8 +664,8 @@ func parseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo // Ответ на запрос поддерживаемых фич, который "http://jabber.org/protocol/disco#info" case xmpp.DiscoResult: if muc, _ := strings.CutSuffix(v.To, "/"); muc != "" { - if slices.Contains(roomsConnected, muc) { - lastMucActivity.Set(muc, lastServerActivity) + if slices.Contains(j.RoomsConnected, muc) { + j.LastMucActivity.Set(muc, j.LastServerActivity) } } @@ -677,10 +677,10 @@ func parseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo // Конкретно сейчас нас интересует только поддержка c2s ping for _, feature := range v.Features { log.Debugf("Server %s announced that it supports feature: %s", v.From, feature) - serverCapsList.Set(feature, true) + j.ServerCapsList.Set(feature, true) } - serverCapsQueried = true + j.ServerCapsQueried = true case "conference": mucCaps := make(map[string]bool) @@ -690,7 +690,7 @@ func parseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo mucCaps[feature] = true //nolint:wsl } - mucCapsList.Set(v.From, mucCaps) + j.MucCapsList.Set(v.From, mucCaps) case "pubsub": log.Debugf("PubSub component %s reply to disco#info, skipping", v.From) diff --git a/internal/jabber/main_loop.go b/internal/jabber/main_loop.go new file mode 100644 index 0000000..976d0ac --- /dev/null +++ b/internal/jabber/main_loop.go @@ -0,0 +1,77 @@ +package jabber + +import ( + "time" + + log "github.com/sirupsen/logrus" +) + +// MyLoop - основной цикл программы. +func (j *Jabber) MyLoop() { + for { + select { + case <-j.GTomb.Dying(): + j.GTomb.Done() + return //nolint:nlreturn + + default: + // Зададим начальное значение глобальным переменным + j.ServerPingTimestampRx = 0 + j.ServerPingTimestampTx = 0 + j.RoomsConnected = make([]string, 1) + j.LastActivity = 0 + j.LastServerActivity = 0 + j.LastMucActivity = NewCollection() + j.ServerCapsQueried = false + j.ServerCapsList = NewCollection() + j.MucCapsList = NewCollection() + j.ServerPingTimestampTx = 0 + j.ServerPingTimestampRx = 0 + j.RoomPresences = NewCollection() + + // Установим коннект + j.EstablishConnection() + + j.ServerPingTimestampRx = time.Now().Unix() // Считаем, что если коннект запустился, то первый пинг успешен. + + // Тыкаем сервер палочкой, проверяем, что коннект жив и вываливаемся из mainLoop, если он не жив. + go j.ProbeServerLiveness() + + // Тыкаем muc-и палочкой, проверяем, что они живы и вываливаемся из mainLoop, если пинги пропали. + // Если пинги до комнаты пропали, то это фактически значит, что либо сервер потерял связь с MUC-компонентом, + // либо у нас какой-то wire error. + go j.ProbeMUCLiveness() + + // Гребём ивенты... + for { + // Стриггерилось завершение работы приложения, или соединение не установлено (порвалось, например) + // грести не надо + if j.Shutdown { + break + } + + if !j.IsConnected { + // Tight loop - это наверно не очень хорошо, думаю, ничего страшного не будет, если мы поспим 100мс. + time.Sleep(100 * time.Millisecond) + + continue + } + + // Вынимаем ивент из "провода" + chat, err := j.Talk.Recv() + + if err != nil { + log.Errorf("Unable to get events from server: %s", err) + j.GTomb.Kill(err) + + // Не забываем выходить на ошибке :( + return + } + + j.ParseEvent(chat) + } + } + } +} + +/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */ diff --git a/norm_string.go b/internal/jabber/norm_string.go similarity index 73% rename from norm_string.go rename to internal/jabber/norm_string.go index eb24aec..df6da61 100644 --- a/norm_string.go +++ b/internal/jabber/norm_string.go @@ -1,10 +1,22 @@ -package main +package jabber import ( "regexp" "strings" ) +// Переменные для простенького нормализатора текста. +var ( + // Знаки препинания раз. + pMarks = []string{".", ",", "!", "@", "#", "$", "%", "^", "&", "*", "(", ")", "{", "}", "<", ">", "[", "]", "\\"} + + // Знаки препинания два-с. + pMarks2 = []string{"-", "_", "+", "=", ":", ";", "'", "`", "~", "\""} + + // Символы новой строки. + newLines = []string{"\n", "\r", "\n\r", "\r\n"} +) + // Нормализует текстовый буфер. Убирает начальные и конечные пробельные символы, заменяет сносы строк пробелами, удаляет // знаки препинания, схлопывает повторяющиеся пробелы в один символ. func nString(buf string) string { @@ -40,3 +52,5 @@ func nStringLower(buf string) string { func nStringUpper(buf string) string { return strings.ToUpper(nString(buf)) } + +/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */ diff --git a/read_config.go b/internal/jabber/read_config.go similarity index 96% rename from read_config.go rename to internal/jabber/read_config.go index 10a431b..cc6866f 100644 --- a/read_config.go +++ b/internal/jabber/read_config.go @@ -1,4 +1,4 @@ -package main +package jabber import ( "encoding/json" @@ -11,9 +11,9 @@ import ( log "github.com/sirupsen/logrus" ) -// readConfig читает и валидирует конфиг, а также выставляет некоторые default-ы, если значений для параметров в конфиге +// ReadConfig читает и валидирует конфиг, а также выставляет некоторые default-ы, если значений для параметров в конфиге // нет. -func readConfig() error { //nolint:gocognit,gocyclo +func (j *Jabber) ReadConfig() error { //nolint:gocognit,gocyclo var ( err error executablePath string @@ -31,9 +31,9 @@ func readConfig() error { //nolint:gocognit,gocyclo configJSONPath := fmt.Sprintf("%s/data/config.json", filepath.Dir(executablePath)) locations := []string{ - "~/.bunyPresense-jabber-bot.json", - "~/bunyPresense-jabber-bot.json", - "/etc/bunyPresense-jabber-bot.json", + "~/.buny-jabber-bot.json", + "~/buny-jabber-bot.json", + "/etc/buny-jabber-bot.json", configJSONPath, } @@ -65,7 +65,7 @@ func readConfig() error { //nolint:gocognit,gocyclo // Интереснее на выходе получить структурку: то есть мы вначале конфиг преобразуем в map-ку, затем эту map-ку // сериализуем в json, а потом json превращаем в структурку. Не очень эффективно, но он и нечасто требуется. var ( - sampleConfig myConfig + sampleConfig MyConfig tmp map[string]interface{} ) @@ -262,7 +262,7 @@ func readConfig() error { //nolint:gocognit,gocyclo // sampleConfig.Log = "" if not set - config = sampleConfig + j.C = sampleConfig configLoaded = true log.Infof("Using %s as config file", location) @@ -276,3 +276,5 @@ func readConfig() error { //nolint:gocognit,gocyclo return err //nolint:wrapcheck } + +/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */ diff --git a/internal/jabber/sig_handler.go b/internal/jabber/sig_handler.go new file mode 100644 index 0000000..3f7a68c --- /dev/null +++ b/internal/jabber/sig_handler.go @@ -0,0 +1,58 @@ +package jabber + +import ( + "os" + "syscall" + + "github.com/eleksir/go-xmpp" + log "github.com/sirupsen/logrus" +) + +func (j *Jabber) SigHandler() { + log.Debug("Installing signal handler") + + for s := range j.SigChan { + switch s { + case syscall.SIGINT: + log.Infoln("Got SIGINT, quitting") + case syscall.SIGTERM: + log.Infoln("Got SIGTERM, quitting") + case syscall.SIGQUIT: + log.Infoln("Got SIGQUIT, quitting") + + // Заходим на новую итерацию, если у нас "неинтересный" сигнал. + default: + continue + } + + // Чтобы не срать в логи ошибками, проставим shutdown state приложения в true. + j.Shutdown = true + + if j.IsConnected && !j.Shutdown { + log.Debug("Try to set our presence to Unavailable and status to Offline") + + // Вот тут понадобится коллекция известных пользователей, чтобы им разослать presence, что бот свалил в + // offline. Пока за неимением лучшего сообщим об этом самим себе. + for _, room := range j.RoomsConnected { + if _, err := j.Talk.SendPresence( + xmpp.Presence{ //nolint:exhaustruct + To: room, + Status: "Offline", + Type: "unavailable", + }, + ); err != nil { + log.Infof("Unable to send presence to jabber server: %s", err) + } + } + + // И закрываем соединение. + log.Infoln("Closing connection to jabber server") + + if err := j.Talk.Close(); err != nil { + log.Infof("Unable to close connection to jabber server: %s", err) + } + } + + os.Exit(0) + } +} diff --git a/internal/jabber/squash.go b/internal/jabber/squash.go new file mode 100644 index 0000000..108c732 --- /dev/null +++ b/internal/jabber/squash.go @@ -0,0 +1,78 @@ +package jabber + +import ( + "fmt" + "time" + + "github.com/eleksir/go-xmpp" +) + +// Squash банит указанный jid в указанной комнате. +// reasonEnable указывает, надо ли писать дату автобана в банлисте в поле reason (это единственная причина, в которую +// умеет бот). +func (j *Jabber) Squash(room, jid string, reasonEnable bool, vType string) (string, error) { + var ( + id string + err error + ) + + if j.C.Jabber.BanPhrasesEnable { + phrase := RandomPhrase(j.C.Jabber.BanPhrases) + + if _, err = j.Talk.Send( + xmpp.Chat{ //nolint:exhaustruct + Remote: room, + Text: phrase, + Type: vType, + }, + ); err != nil { + err = fmt.Errorf("unable to send phrase to room %s: %w", room, err) + + // Здесь возвращаем nil, т.к. за нас ошибку залоггирует код выше. + return id, err + } + } + + // https://xmpp.org/extensions/xep-0045.html#ban баним вот таким сообщением + ban := "" + + if reasonEnable { + var t = time.Now() + ban += fmt.Sprintf( + "autoban at %04d.%02d.%02d %02d:%02d:%02d", + t.Year(), + t.Month(), + t.Day(), + t.Hour(), + t.Minute(), + t.Second(), + ) + } else { + ban += "" + } + + ban += "" + + // Выжидаем некоторое время перед баном. А то можно настолько рано забанить, что сервер не внесёт злодея в банлист + // комнаты и пришлёт affiliation: none вместо affiliation: outcast. + if j.C.Jabber.BanDelay > 0 { + time.Sleep(time.Duration(j.C.Jabber.BanDelay) * time.Millisecond) + } + + if id, err = j.Talk.RawInformationQuery( + j.Talk.JID(), + room, + "ban1", + xmpp.IQTypeSet, + "http://jabber.org/protocol/muc#admin", + ban, + ); err != nil { + err = fmt.Errorf( + "unable to ban user: id=%s, err=%w", + id, + err, + ) + } + + return id, err +} diff --git a/types.go b/internal/jabber/types.go similarity index 58% rename from types.go rename to internal/jabber/types.go index 58e0505..7579fc0 100644 --- a/types.go +++ b/internal/jabber/types.go @@ -1,11 +1,15 @@ -package main +package jabber import ( "encoding/xml" + "os" + + "github.com/eleksir/go-xmpp" + "gopkg.in/tomb.v1" ) -// myConfig прототип структурки с конфигом. -type myConfig struct { +// MyConfig прототип структурки с конфигом. +type MyConfig struct { Jabber struct { Server string `json:"server,omitempty"` Port int `json:"port,omitempty"` @@ -56,8 +60,8 @@ type myConfig struct { Log string `json:"log,omitempty"` } -// myWhiteList прототип структурки с белым списком jid-ов. -type myWhiteList struct { +// MyWhiteList прототип структурки с белым списком jid-ов. +type MyWhiteList struct { Whitelist []struct { RoomName string `json:"room_name,omitempty"` Jid []string `json:"jid,omitempty"` @@ -65,8 +69,8 @@ type myWhiteList struct { } `json:"whitelist,omitempty"` } -// myBlackList прототип структурки с чёрным списком jid-ов. -type myBlackList struct { +// MyBlackList прототип структурки с чёрным списком jid-ов. +type MyBlackList struct { Blacklist []struct { RoomName string `json:"room_name,omitempty"` ReasonEnable bool `json:"reason_enable,omitempty"` @@ -76,17 +80,82 @@ type myBlackList struct { } `json:"blacklist,omitempty"` } -// jabberSimpleIqGetQuery прототип структурки для разбора запросов xmpp discovery query, например, +// Jabber основная структура-объект, содержащая стейты и проч. +type Jabber struct { + // C - конфиг, как он распарсился из конфиг-файла. + C MyConfig + + // WhiteList - структурка с безусловно разрешёнными jid-ами + WhiteList MyWhiteList + + // BlackList - структурка с запрещёнными по регуляркам фразами, никами, jid-ами. + BlackList MyBlackList + + // Опции подключения к xmpp-серверу. + Options *xmpp.Options + + // gTomb пул активных горутин. + GTomb tomb.Tomb + + // Talk основная структурка xmpp-клиента. + Talk *xmpp.Client + + // sync.Map-ка с капабилити сервера. + ServerCapsList *Collection + + // ServerCapsQueried показывает, были ли запрошены capabilities сервера. + ServerCapsQueried bool + + // Время последней активности, нужно для c2s пингов - посылаем пинги, только если давненько ничего не приходило с + // сервера. + LastServerActivity int64 + + // Время, когда был отправлен c2s ping. + ServerPingTimestampTx int64 + + // Время, когда был принят s2c pong. + ServerPingTimestampRx int64 + + // Время последней активности, нужно для jabber:iq:last. + LastActivity int64 + + // sync.Map-ка с комнатами и их capability. + MucCapsList *Collection + + // Время последней активности MUC-ов, нужно для пингов - посылаем пинги, только если давненько ничего не приходило из + // muc-ов. + LastMucActivity *Collection + + // Список комнат, в которых находится бот. + RoomsConnected []string + + // sync.Map-ка со списком участников конференций (в json-формате, согласно структуре xmpp.Presence, "room".[]json). + RoomPresences *Collection + + // Канал, по котором приходят сообщения о том, что ОС отправила некие сигналы процессу. + SigChan chan os.Signal + + // Индиктор того, что процесс завершается. + Shutdown bool + + // Индиктор того, что соединение установлено. + IsConnected bool + + // Индикатор того, что соединение в процессе достукивания до сервера. + Connecting bool +} + +// JabberSimpleIqGetQuery прототип структурки для разбора запросов xmpp discovery query, например, // https://xmpp.org/extensions/xep-0030.html#example-18 . -type jabberSimpleIqGetQuery struct { +type JabberSimpleIqGetQuery struct { XMLName xml.Name `xml:"query"` Text string `xml:",chardata"` Xmlns string `xml:"xmlns,attr"` Node string `xml:"node,attr,omitempty"` // для xmlns="http://jabber.org/protocol/disco#items" } -// jabberPubsubIQGetQuery прототип структурки для разбора запросов xmpp pubsub. -type jabberPubsubIQGetQuery struct { +// JabberPubsubIQGetQuery прототип структурки для разбора запросов xmpp pubsub. +type JabberPubsubIQGetQuery struct { XMLName xml.Name `xml:"pubsub"` Text string `xml:",chardata"` Xmlns string `xml:"xmlns,attr"` @@ -97,25 +166,25 @@ type jabberPubsubIQGetQuery struct { } `xml:"items"` } -// jabberTimeIqGetQuery прототип структурки для разбора IQ запросов на локальное время клиента, +// JabberTimeIqGetQuery прототип структурки для разбора IQ запросов на локальное время клиента, // https://xmpp.org/extensions/xep-0202.html -type jabberTimeIqGetQuery struct { +type JabberTimeIqGetQuery struct { //