From e8f8b3cb499406a8877b40b07c3fbe0d07de36e1 Mon Sep 17 00:00:00 2001 From: Sergei Date: Sat, 29 Jul 2023 22:23:11 -0700 Subject: [PATCH] Massive cleanup - coding style, tiny bugs, typos and other errors --- Makefile | 5 +- README.md | 41 +- buny.go | 23 +- collection.go | 18 +- event_parser.go | 39 +- globals.go | 36 +- go.mod | 4 +- go.sum | 8 +- lib.go | 235 +++++++----- main.go | 5 +- types.go | 17 +- vendor/github.com/eleksir/go-xmpp/xmpp.go | 24 +- vendor/golang.org/x/exp/slices/cmp.go | 44 +++ vendor/golang.org/x/exp/slices/slices.go | 353 ++++++++++++++---- vendor/golang.org/x/exp/slices/sort.go | 115 ++++-- .../slices/{zsortfunc.go => zsortanyfunc.go} | 154 ++++---- .../golang.org/x/exp/slices/zsortordered.go | 34 +- vendor/modules.txt | 4 +- 18 files changed, 767 insertions(+), 392 deletions(-) create mode 100644 vendor/golang.org/x/exp/slices/cmp.go rename vendor/golang.org/x/exp/slices/{zsortfunc.go => zsortanyfunc.go} (64%) diff --git a/Makefile b/Makefile index 175118e..75e7320 100644 --- a/Makefile +++ b/Makefile @@ -1,13 +1,12 @@ #!/usr/bin/env gmake -f -GOOPTS=CGO_ENABLED=0 +CGO_ENABLED=0 BUILDOPTS=-ldflags="-s -w" -a -gcflags=all=-l -trimpath all: clean build build: - ${GOOPTS} go build ${BUILDOPTS} -o "buny-jabber-bot" collection.go types.go globals.go lib.go event_parser.go \ - buny.go main.go + go build ${BUILDOPTS} -o "buny-jabber-bot" collection.go types.go globals.go lib.go event_parser.go buny.go main.go clean: go clean diff --git a/README.md b/README.md index ff9d897..557e396 100644 --- a/README.md +++ b/README.md @@ -2,13 +2,16 @@ Простой бот, работающий по протоколу xmpp. Его задача - банить абьюзеров чата. +Этот бот автоматизирует уборку человеческой грязи. Самый важный вопрос, который следует себе задать перед его +использованием - а не забаню ли я кого-то не того? + ## Что он может? -Имеет возможность соединяться с сервером по протоколу xmpp как с шифрованием, так и без. Шифрование работает по -классическим механикам (шифрованный канал связи, без возможности коммуникации в незашифрованном виде), без поддержки -механизма start tls. Что-то в гошке или на стороне сервера при выборе StartTLS не работает. +Имеет возможность соединяться с сервером по протоколу xmpp как с шифрованием, так и без. (Пока что) Шифрование +работает по классическим механикам (шифрованный канал связи, без возможности коммуникации в незашифрованном виде), +без поддержки механизма start tls. Что-то в гошке или на стороне сервера при выборе StartTLS не работает. -Имеет возможность заносить пользователей в бан-лист согласно заданным в конфиге правилам. +Имеет возможность заносить пользователей в бан-лист, согласно заданным в чёрном списке правилам. ## Что он не может? @@ -16,13 +19,9 @@ * Не умеет разгадывать капчу. ## Как заставить его работать? +Технически, бот собирается гошкой 1.20 под платформу linux. Работоспособность на других платформах не тестировалась. -Этот бот автоматизирует уборку человеческой грязи. Самый важный вопрос, кторый следует себе задать еред его -использоованием - а не забаню ли я кого-то не того? - -Технически, бот собирается гошкой 1.20 поод платформу linux. Работоспособность на других платформах не тестировалась. - -Чтобы получить готовые бинарники достаточно выполнить команду: +Чтобы получить готовые бинарники, достаточно выполнить команду: ```bash make @@ -30,8 +29,8 @@ make в итоге получится бинарник **buny-jabber-bot**, которому нужен конфиг data/config.json для работы. -А также понадобятся **data/blacklist.json** и **data/whitelist.json** в которых списки правил, согласно которым *точно* -надо банить злодеев и списки пользователей, которых *точно* не надо банить. +А также понадобятся **data/blacklist.json** и **data/whitelist.json**, в которых находятся списки правил, злодеев и +списки проверенных пользователей. ### Замечание @@ -73,12 +72,14 @@ presence-ов тех, кто находится в комнате на моме ### Можно ли почистить ban-list? Да, бывают ситуации, когда по ошибке бот может забанить кого-то не того или, если не настроен белый список вообще ни в -каком виде, то будет банить всех входящих. И если вдруг произошло так, что в бан попали все админы комнаты, то разбанить -их может либо овнер (никто же не даст овнера боту), либо тот, кто зайдёт в комнату из-под аккаунта бота и разбанить всех -от его имени. +каком виде, то будет банить всех входящих. -В далёких планах есть идея релизовать очистку бан-листа при запуске бота с настройкой `"wipe_bans": true`` в -whitelist.json для канала; кроме того, есть планы такое реализовать и в качестве команды, когда у бота появятся команды. +И если вдруг произошло так, что в бан попали все админы комнаты, то... + +Во-первых, давать овнера боту не надо ни при каких обстоятельствах. + +Во-вторых, разбанить их может только овнер, для чего надо зайти в комнату из-под аккаунта бота и разбанить всех кого +надо. ## Дисклеймер @@ -86,8 +87,8 @@ whitelist.json для канала; кроме того, есть планы т ## Благодарности -Во-первых `сообществу`, без него не было бы чятиков. +Во-первых, `сообществу`, без него не было бы чятиков. -Во-вторых `batman46(new)` из конференций на `jabber.ru`, за наблюдения и за некоторые идеи, воплощённыые в этом боте. +Во-вторых, `batman46(new)` из конференций на `jabber.ru`, за наблюдения и за некоторые идеи, воплощённые в этом боте. -В-третьих `U2` из конференции `radio@conference.jabber.ru` за мысли, идеи и за позитивный настрой. +В-третьих, `U2` из конференции `radio@conference.jabber.ru` за мысли, идеи и за позитивный настрой. diff --git a/buny.go b/buny.go index 2ca627d..b4e6da7 100644 --- a/buny.go +++ b/buny.go @@ -10,7 +10,7 @@ import ( log "github.com/sirupsen/logrus" ) -func buny(v xmpp.Presence) error { +func buny(v xmpp.Presence) error { //nolint:gocognit var err error // Если у presence-а есть JID и presence из одной из комнат, в которой мы есть и если его домен в чёрном @@ -24,7 +24,7 @@ func buny(v xmpp.Presence) error { room := strings.SplitN(v.From, "/", 2)[0] if room == "" { - log.Infof("We got empty room in repsence event, which kinda strange: %s", spew.Sdump(v)) + log.Infof("We got empty room field in presence event, which kinda strange: %s", spew.Sdump(v)) return err } @@ -58,14 +58,13 @@ func buny(v xmpp.Presence) error { for _, cRoom := range roomsConnected { if cRoom == room { for _, bEntry := range blackList.Blacklist { - - // Обработаем правила гобального чёрного списка + // Обработаем правила глобального чёрного списка if bEntry.RoomName == "" { - for _, jid_regexp := range bEntry.JidRe { - re, err := regexp.Compile(jid_regexp) + for _, jidRegexp := range bEntry.JidRe { + re, err := regexp.Compile(jidRegexp) if err != nil { - log.Errorf("Incorrect regexp in blacklist: %s, skipping", jid_regexp) + log.Errorf("Incorrect regexp in blacklist: %s, skipping", jidRegexp) continue } @@ -91,7 +90,6 @@ func buny(v xmpp.Presence) error { return err } - } continue @@ -99,11 +97,11 @@ func buny(v xmpp.Presence) error { // Обработаем правила конкретного канала, комнаты конференции if bEntry.RoomName == room { - for _, jid_regexp := range bEntry.JidRe { - re, err := regexp.Compile(jid_regexp) + for _, jidRegexp := range bEntry.JidRe { + re, err := regexp.Compile(jidRegexp) if err != nil { - log.Errorf("Incorrect regexp in blacklist: %s, skipping", jid_regexp) + log.Errorf("Incorrect regexp in blacklist: %s, skipping", jidRegexp) continue } @@ -129,7 +127,6 @@ func buny(v xmpp.Presence) error { return err } - } continue @@ -141,3 +138,5 @@ func buny(v xmpp.Presence) error { return err } + +/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */ diff --git a/collection.go b/collection.go index 9805c97..d99613e 100644 --- a/collection.go +++ b/collection.go @@ -4,27 +4,27 @@ import ( "sync" ) -// Collection это sync.Map-ка, хранящая в себе произвольные итемы +// Collection это sync.Map-ка, хранящая в себе произвольные итемы. type Collection struct { items sync.Map close chan struct{} } -// Item представляет собой интерфейс для хранения произвольных данных +// Item представляет собой интерфейс для хранения произвольных данных. type item struct { data interface{} } -// NewCollection создаёт инстанс коллекции +// NewCollection создаёт инстанс коллекции. func NewCollection() *Collection { - c := &Collection{ + c := &Collection{ //nolint:exhaustruct close: make(chan struct{}), } return c } -// Get достаёт данные по заданному ключу из коллекции +// (collection *Collection) Get(key interface{}) (interface{}, bool) достаёт данные по заданному ключу из коллекции. func (collection *Collection) Get(key interface{}) (interface{}, bool) { obj, exists := collection.items.Load(key) @@ -37,14 +37,14 @@ func (collection *Collection) Get(key interface{}) (interface{}, bool) { return item.data, true } -// Set сохраняет данные с заданным ключом в коллекцию +// (collection *Collection) Set(key interface{}, value interface{}) сохраняет данные с заданным ключом в коллекцию. func (collection *Collection) Set(key interface{}, value interface{}) { collection.items.Store(key, item{ data: value, }) } -// Range применяет функцию f ко всем ключам в коллекции +// (collection *Collection) Range(f func(key, value interface{}) bool) применяет функцию f ко всем ключам в коллекции. func (collection *Collection) Range(f func(key, value interface{}) bool) { fn := func(key, value interface{}) bool { item := value.(item) @@ -55,12 +55,12 @@ func (collection *Collection) Range(f func(key, value interface{}) bool) { collection.items.Range(fn) } -// Delete удаляет ключ и значение из коллекции данных +// (collection *Collection) Delete(key interface{}) удаляет ключ и значение из коллекции данных. func (collection *Collection) Delete(key interface{}) { collection.items.Delete(key) } -// Close очищает и высвобождает ресурсы, занятые коллекцией +// (collection *Collection) Close() очищает и высвобождает ресурсы, занятые коллекцией. func (collection *Collection) Close() { collection.close <- struct{}{} collection.items = sync.Map{} diff --git a/event_parser.go b/event_parser.go index 0544928..ec8d072 100644 --- a/event_parser.go +++ b/event_parser.go @@ -13,7 +13,7 @@ import ( "golang.org/x/exp/slices" ) -func parseEvent(e interface{}) { +func parseEvent(e interface{}) { //nolint:gocognit,gocyclo lastServerActivity = time.Now().Unix() switch v := e.(type) { @@ -30,7 +30,7 @@ func parseEvent(e interface{}) { // В то же время сообщения от людей приходят с пустым subject, но с заполненным text if v.Text != "" { // Чятики бывают групповые и не групповые, от этого зависит Remote, куда направлять сообщение - switch v.Type { //nolint:whitespace + switch v.Type { //nolint:wsl,whitespace // Групповой чятик case "groupchat": @@ -87,16 +87,18 @@ func parseEvent(e interface{}) { } } - var iqStruct jabberSimpleIqGetQuery - var parseError bool + var ( + iqStruct jabberSimpleIqGetQuery + parseError bool + ) if err := xml.Unmarshal(v.Query, &iqStruct); err == nil { - switch iqStruct.Xmlns { //nolint:whitespace + switch iqStruct.Xmlns { //nolint:wsl,whitespace // Запрос номера версии приложения case "jabber:iq:version": log.Infof("Got IQ get request for version from %s", v.From) - answer := "" + answer := "" //nolint:wsl answer += "buny-jabber-bot" answer += "1.с_чем-то" answer += "линупс" @@ -109,7 +111,7 @@ func parseEvent(e interface{}) { xmpp.IQTypeResult, answer, ); err != nil { - log.Errorf("Unable to send verion info to jabber server: id=%s, err=%s", id, err) + log.Errorf("Unable to send version info to jabber server: id=%s, err=%s", id, err) os.Exit(1) } @@ -186,9 +188,9 @@ func parseEvent(e interface{}) { answer := "" - answer += "" + answer += "" //nolint:goconst answer += "" - answer += "" + answer += "" //nolint:goconst if id, err := talk.RawInformation( v.To, @@ -287,7 +289,7 @@ func parseEvent(e interface{}) { default: log.Info("Got unknown IQ get pubsub something request, discarding") log.Info(spew.Sdump(e)) - parseError = true + parseError = true //nolint:wsl } } else { parseError = true @@ -311,6 +313,7 @@ func parseEvent(e interface{}) { "", ); err != nil { log.Errorf("Unable to send pong to jabber server: id=%s, err=%s", id, err) + os.Exit(1) } } @@ -335,7 +338,7 @@ func parseEvent(e interface{}) { // Похоже на pong от сервера (по стандарту в ответе нету query, но go-xmpp нам подсовывает это) case v.From == config.Jabber.Server && v.To == talk.JID() && string(v.Query) == "": log.Debugf("Got S2C pong answer from %s to %s", v.From, v.To) - serverPingTimestampRx = time.Now().Unix() + serverPingTimestampRx = time.Now().Unix() //nolint:wsl // Похоже на понг второй стадии xep-0410 MUC-Ping-а, который у нас не реализован case v.To == talk.JID() && string(v.Query) == "": @@ -405,8 +408,10 @@ func parseEvent(e interface{}) { case xmpp.IQTypeError: // Если сервер не хочет пинговаться и отвечает ошибкой на пинг, то наверно он не умеет в пинги, // хотя если мы его пингуем, значит он анонсировал такой capability. Вот, засранец! - var iqPingStruct jabberIqPing - var parseError = true + var ( + iqPingStruct jabberIqPing + parseError = true + ) if err := xml.Unmarshal(v.Query, &iqPingStruct); err == nil { if iqPingStruct.Xmlns == "urn:xmpp:ping" { @@ -429,7 +434,7 @@ func parseEvent(e interface{}) { // Это у нас пинг xep-0410 и мы не в комнате, предполагается, что надо бы заджойниться if parseError { - var iqErrorCancelNotAcceptable jabberIqErrorCancelNotAccepatble + var iqErrorCancelNotAcceptable jabberIqErrorCancelNotAcceptable if err := xml.Unmarshal(v.Query, &iqErrorCancelNotAcceptable); err == nil { if v.To == talk.JID() { @@ -503,8 +508,8 @@ func parseEvent(e interface{}) { log.Errorf(spew.Sdump(v)) } } else { - log.Infof("Presence notification, Type: %s, From: %s, To: %s Show: %s, Status: %s, Affilation: %s, Role: %s, JID: %s", - v.Type, v.From, v.To, v.Show, v.Status, v.Affilation, v.Role, v.JID) + log.Infof("Presence notification, Type: %s, From: %s, To: %s Show: %s, Status: %s, Affiliation: %s, Role: %s, JID: %s", + v.Type, v.From, v.To, v.Show, v.Status, v.Affiliation, v.Role, v.JID) // Это наш собственный Presence if v.Show == "" && v.Status == "" { @@ -544,7 +549,7 @@ func parseEvent(e interface{}) { for _, feature := range v.Features { log.Debugf("MUC %s announced that it supports feature: %s", v.From, feature) - mucCaps[feature] = true + mucCaps[feature] = true //nolint:wsl } mucCapsList.Set(v.From, mucCaps) diff --git a/globals.go b/globals.go index b5d5012..04e31f7 100644 --- a/globals.go +++ b/globals.go @@ -6,60 +6,60 @@ import ( "github.com/eleksir/go-xmpp" ) -// Config - это у нас глобальная штука :) +// Config - это у нас глобальная штука :) . var config myConfig -// Списки тех, кого мы не баним никогда +// Списки тех, кого мы не баним никогда. var whiteList myWhiteList -// Списки злодеев, которых мы банм +// Списки злодеев, которых мы баним. var blackList myBlackList -// Ставится в true, если мы получили сигнал на выключение +// Ставится в true, если мы получили сигнал на выключение. var shutdown = false -// Чтобы не организовывать драку за установку коннекта +// Чтобы не организовывать драку за установку коннекта. var connecting = false -// Глобальное состояние соединения +// Глобальное состояние соединения. var isConnected = false -// Канал, в который приходят уведомления для хэндлера сигналов от траппера сигналов +// Канал, в который приходят уведомления для хэндлера сигналов от траппера сигналов. var sigChan = make(chan os.Signal, 1) -// Основной инстанс xmpp-клиента +// Основной инстанс xmpp-клиента. var talk *xmpp.Client -// Опции подключения к xmpp-серверу +// Опции подключения к xmpp-серверу. var options *xmpp.Options -// Список комнат, в которых находится бот +// Список комнат, в которых находится бот. var roomsConnected []string -// Время последней активности, нужно для jabber:iq:last +// Время последней активности, нужно для jabber:iq:last. var lastActivity int64 // Время последней активности, нужно для c2s пингов - посылаем пинги, только если давненько ничего не приходило с -// сервера +// сервера. var lastServerActivity int64 // Время последней активности MUC-ов, нужно для пингов - посылаем пинги, только если давненько ничего не приходило из -// muc-ов +// muc-ов. var lastMucActivity = NewCollection() -// Получен ли ответ на запрос disco#info к серверу +// Получен ли ответ на запрос disco#info к серверу. var serverCapsQueried bool -// sync.Map-ка с капабилити сервера +// sync.Map-ка с капабилити сервера. var serverCapsList = NewCollection() -// sync.Map-ка с комнатами и их capability +// sync.Map-ка с комнатами и их capability. var mucCapsList = NewCollection() -// Время, когда был отправлен c2s ping +// Время, когда был отправлен c2s ping. var serverPingTimestampTx int64 -// Время, когда был принят s2c pong +// Время, когда был принят s2c pong. var serverPingTimestampRx int64 /* vim: set ft=go noet ai ts=4 sw=4 sts=4: */ diff --git a/go.mod b/go.mod index b2e1d2b..f7f779e 100644 --- a/go.mod +++ b/go.mod @@ -4,10 +4,10 @@ go 1.20 require ( github.com/davecgh/go-spew v1.1.1 - github.com/eleksir/go-xmpp v1.3.0 + github.com/eleksir/go-xmpp v1.4.0 github.com/hjson/hjson-go v3.3.0+incompatible github.com/sirupsen/logrus v1.9.3 - golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 + golang.org/x/exp v0.0.0-20230728194245-b0cb94b80691 ) require ( diff --git a/go.sum b/go.sum index 1cd2df2..30c3183 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/eleksir/go-xmpp v1.3.0 h1:A4vonXmHJ+3W4LW+BDM24GkAT0zRMhCAMZhAqTRarLE= -github.com/eleksir/go-xmpp v1.3.0/go.mod h1:KnvbXvUpttEVFl8l5eDO2Q7J97ZxtTMvOsDWULD2Rn8= +github.com/eleksir/go-xmpp v1.4.0 h1:CJ91zeoLoV0FqhhcvZnkBjeuaXWbWgRTJ/HrzKugM4s= +github.com/eleksir/go-xmpp v1.4.0/go.mod h1:KnvbXvUpttEVFl8l5eDO2Q7J97ZxtTMvOsDWULD2Rn8= github.com/hjson/hjson-go v3.3.0+incompatible h1:Rqr+Ya+0aCJMjaE4s8E9YKvuJLuLVpEvz4ONum52vnI= github.com/hjson/hjson-go v3.3.0+incompatible/go.mod h1:qsetwF8NlsTsOTwZTApNlTCerV+b2GjYRRcIk4JMFio= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -14,8 +14,8 @@ github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5Cc github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA= golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= -golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 h1:MGwJjxBy0HJshjDNfLsYO8xppfqWlA5ZT9OhtUUhTNw= -golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= +golang.org/x/exp v0.0.0-20230728194245-b0cb94b80691 h1:/yRP+0AN7mf5DkD3BAI6TOFnd51gEoDEb8o35jIFtgw= +golang.org/x/exp v0.0.0-20230728194245-b0cb94b80691/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/lib.go b/lib.go index 28a8a7b..228e42a 100644 --- a/lib.go +++ b/lib.go @@ -15,7 +15,7 @@ import ( "golang.org/x/exp/slices" ) -// Хэндлер сигналов закрывает все бд, все сетевые соединения и сваливает из приложения +// sigHandler() Хэндлер сигналов закрывает все бд, все сетевые соединения и сваливает из приложения. func sigHandler() { log.Debug("Installing signal handler") @@ -28,19 +28,19 @@ func sigHandler() { case syscall.SIGQUIT: log.Infoln("Got SIGQUIT, quitting") - // Заходим на новую итерацию, если у нас "неинтересный" сигнал + // Заходим на новую итерацию, если у нас "неинтересный" сигнал. default: continue } - // Чтобы не срать в логи ошибками, проставим shutdown state приложения в true + // Чтобы не срать в логи ошибками, проставим shutdown state приложения в true. shutdown = true if isConnected && !shutdown { log.Debug("Try to set our presence to Unavailable and status to Offline") // Вот тут понадобится коллекция известных пользователей, чтобы им разослать presence, что бот свалил в offline - // Пока за неимением лучшего сообщим об этом самим себе + // Пока за неимением лучшего сообщим об этом самим себе. for _, room := range roomsConnected { if _, err := talk.SendPresence( xmpp.Presence{ //nolint:exhaustruct @@ -53,7 +53,7 @@ func sigHandler() { } } - // И закрываем соединение + // И закрываем соединение. log.Debugf("Closing connection to jabber server") if err := talk.Close(); err != nil { @@ -65,13 +65,16 @@ func sigHandler() { } } -// Читает и валидирует конфиг, а также выставляет некоторые default-ы, если значений для параметров в конфиге нет -func readConfig() { +// readConfig читает и валидирует конфиг, а также выставляет некоторые default-ы, если значений для параметров в конфиге +// нет. +func readConfig() { //nolint:gocognit configLoaded := false executablePath, err := os.Executable() if err != nil { log.Errorf("Unable to get current executable path: %s", err) + + os.Exit(1) } configJSONPath := fmt.Sprintf("%s/data/config.json", filepath.Dir(executablePath)) @@ -86,12 +89,12 @@ func readConfig() { for _, location := range locations { fileInfo, err := os.Stat(location) - // Предполагаем, что файла либо нет, либо мы не можем его прочитать, второе надо бы логгировать, но пока забьём + // Предполагаем, что файла либо нет, либо мы не можем его прочитать, второе надо бы логгировать, но пока забьём. if err != nil { continue } - // Конфиг-файл длинноват для конфига, попробуем следующего кандидата + // Конфиг-файл длинноват для конфига, попробуем следующего кандидата. if fileInfo.Size() > 65535 { log.Warnf("Config file %s is too long for config, skipping", location) @@ -100,7 +103,7 @@ func readConfig() { buf, err := os.ReadFile(location) - // Не удалось прочитать, попробуем следующего кандидата + // Не удалось прочитать, попробуем следующего кандидата. if err != nil { log.Warnf("Skip reading config file %s: %s", location, err) @@ -110,18 +113,21 @@ func readConfig() { // Исходя из документации, hjson какбы умеет парсить "кривой" json, но парсит его в map-ку. // Интереснее на выходе получить структурку: то есть мы вначале конфиг преобразуем в map-ку, затем эту map-ку // сериализуем в json, а потом json превращаем в структурку. Не очень эффективно, но он и нечасто требуется. - var sampleConfig myConfig - var tmp map[string]interface{} + var ( + sampleConfig myConfig + tmp map[string]interface{} + ) + err = hjson.Unmarshal(buf, &tmp) - // Не удалось распарсить - попробуем следующего кандидата + // Не удалось распарсить - попробуем следующего кандидата. if err != nil { log.Warnf("Skip parsing config file %s: %s", location, err) continue } - tmpjson, err := json.Marshal(tmp) + tmpJSON, err := json.Marshal(tmp) // Не удалось преобразовать map-ку в json if err != nil { @@ -130,7 +136,7 @@ func readConfig() { continue } - if err := json.Unmarshal(tmpjson, &sampleConfig); err != nil { + if err := json.Unmarshal(tmpJSON, &sampleConfig); err != nil { log.Warnf("Skip parsing config file %s: %s", location, err) continue @@ -138,20 +144,22 @@ func readConfig() { // Значения для Jabber-клиента if sampleConfig.Jabber.Server == "" { - sampleConfig.Jabber.Server = "localhost" - log.Errorf("Jabber server is not defined in config, using localhost") + log.Error("Jabber server is not defined in config, using localhost") + sampleConfig.Jabber.Server = "localhost" //nolint:wsl } if sampleConfig.Jabber.Port == 0 { - sampleConfig.Jabber.Port = 5552 + sampleConfig.Jabber.Port = 5222 if sampleConfig.Jabber.Ssl { if !sampleConfig.Jabber.StartTLS { - sampleConfig.Jabber.Port = 5553 + sampleConfig.Jabber.Port = 5223 + + log.Info("Jabber port is not defined in config, using 5223") + } else { + log.Info("Jabber port is not defined in config, using 5222") } } - - log.Infof("Jabber port is not defined in config, using 6667") } if !sampleConfig.Jabber.Ssl { @@ -166,26 +174,38 @@ func readConfig() { if sampleConfig.Jabber.ConnectionTimeout == 0 { sampleConfig.Jabber.ConnectionTimeout = 10 + + log.Info("Jabber server connection_timeout not defined in config, using 10 seconds") } if sampleConfig.Jabber.ReconnectDelay == 0 { sampleConfig.Jabber.ReconnectDelay = 3 + + log.Info("Jabber server reconnect_delay not defined in config, using 3 seconds") } if sampleConfig.Jabber.ServerPingDelay == 0 { sampleConfig.Jabber.ServerPingDelay = 60 + + log.Info("Jabber server_ping_delay not defined in config, using 60 seconds") } if sampleConfig.Jabber.MucPingDelay == 0 { sampleConfig.Jabber.MucPingDelay = 900 + + log.Info("Jabber muc_ping_delay not defined in config, using 900 seconds") } if sampleConfig.Jabber.MucRejoinDelay == 0 { sampleConfig.Jabber.MucRejoinDelay = 3 + + log.Info("Jabber muc_rejoin_delay not defined in config, using 3 seconds") } if sampleConfig.Jabber.PingSplayDelay == 0 { sampleConfig.Jabber.PingSplayDelay = 3 + + log.Info("Jabber ping_splay_delay not defined in config, using 3 seconds") } if sampleConfig.Jabber.Nick == "" { @@ -195,10 +215,14 @@ func readConfig() { if sampleConfig.Jabber.Resource == "" { sampleConfig.Jabber.Resource = "buny bot" + + log.Info("Jabber resource not defined in config, using buny bot") } if sampleConfig.Jabber.User == "" { sampleConfig.Jabber.User = fmt.Sprintf("%s@%s", sampleConfig.Jabber.Nick, sampleConfig.Jabber.Server) + + log.Infof("Jabber user not defined in config, guessing, it can be %s", sampleConfig.Jabber.User) } // Если sampleConfig.Jabber.Password не задан, то авторизации не будет @@ -212,13 +236,15 @@ func readConfig() { if sampleConfig.Loglevel == "" { sampleConfig.Loglevel = "info" + + log.Info("loglevel not defined in config, using info") } // sampleConfig.Log = "" if not set config = sampleConfig configLoaded = true - log.Infof("Using %s as config file", location) + log.Infof("Using %s as config file", location) //nolint:wsl break } @@ -229,13 +255,14 @@ func readConfig() { } } -// Читает и валидирует белые списки пользователей +// readWhitelist() читает и валидирует белые списки пользователей. func readWhitelist() { whitelistLoaded := false executablePath, err := os.Executable() if err != nil { log.Errorf("Unable to get current executable path: %s", err) + os.Exit(1) } whitelistJSONPath := fmt.Sprintf("%s/data/whitelist.json", filepath.Dir(executablePath)) @@ -274,8 +301,11 @@ func readWhitelist() { // Исходя из документации, hjson какбы умеет парсить "кривой" json, но парсит его в map-ку. // Интереснее на выходе получить структурку: то есть мы вначале конфиг преобразуем в map-ку, затем эту map-ку // сериализуем в json, а потом json превращаем в структурку. Не очень эффективно, но он и нечасто требуется. - var sampleWhitelist myWhiteList - var tmp map[string]interface{} + var ( + sampleWhitelist myWhiteList + tmp map[string]interface{} + ) + err = hjson.Unmarshal(buf, &tmp) // Не удалось распарсить - попробуем следующего кандидата @@ -285,7 +315,7 @@ func readWhitelist() { continue } - tmpjson, err := json.Marshal(tmp) + tmpJSON, err := json.Marshal(tmp) // Не удалось преобразовать map-ку в json if err != nil { @@ -294,7 +324,7 @@ func readWhitelist() { continue } - if err := json.Unmarshal(tmpjson, &sampleWhitelist); err != nil { + if err := json.Unmarshal(tmpJSON, &sampleWhitelist); err != nil { log.Warnf("Skip parsing whitelist file %s: %s", location, err) continue @@ -302,6 +332,7 @@ func readWhitelist() { whiteList = sampleWhitelist whitelistLoaded = true + log.Infof("Using %s as whiteList file", location) break @@ -309,17 +340,20 @@ func readWhitelist() { if !whitelistLoaded { log.Error("Whitelist was not loaded! Refusing to start.") + os.Exit(1) } } -// Читает и валидирует чёрные списки пользователей +// readBlacklist() читает и валидирует чёрные списки пользователей. func readBlacklist() { blacklistLoaded := false executablePath, err := os.Executable() if err != nil { log.Errorf("Unable to get current executable path: %s", err) + + os.Exit(1) } whitelistJSONPath := fmt.Sprintf("%s/data/blacklist.json", filepath.Dir(executablePath)) @@ -358,8 +392,11 @@ func readBlacklist() { // Исходя из документации, hjson какбы умеет парсить "кривой" json, но парсит его в map-ку. // Интереснее на выходе получить структурку: то есть мы вначале конфиг преобразуем в map-ку, затем эту map-ку // сериализуем в json, а потом json превращаем в структурку. Не очень эффективно, но он и нечасто требуется. - var sampleBlacklist myBlackList - var tmp map[string]interface{} + var ( + sampleBlacklist myBlackList + tmp map[string]interface{} + ) + err = hjson.Unmarshal(buf, &tmp) // Не удалось распарсить - попробуем следующего кандидата @@ -369,7 +406,7 @@ func readBlacklist() { continue } - tmpjson, err := json.Marshal(tmp) + tmpJSON, err := json.Marshal(tmp) // Не удалось преобразовать map-ку в json if err != nil { @@ -378,7 +415,7 @@ func readBlacklist() { continue } - if err := json.Unmarshal(tmpjson, &sampleBlacklist); err != nil { + if err := json.Unmarshal(tmpJSON, &sampleBlacklist); err != nil { log.Warnf("Skip parsing whitelist file %s: %s", location, err) continue @@ -386,6 +423,7 @@ func readBlacklist() { blackList = sampleBlacklist blacklistLoaded = true + log.Infof("Using %s as blacklist file", location) break @@ -393,10 +431,12 @@ func readBlacklist() { if !blacklistLoaded { log.Error("Blacklist was not loaded! Refusing to start.") + os.Exit(1) } } +// establishConnection() устанавливает соединение с jabber-сервером. func establishConnection() { var err error @@ -404,39 +444,32 @@ func establishConnection() { return } - // проставляем глобальные переменные + // Проставляем глобальные переменные. connecting = true isConnected = false roomsConnected = make([]string, 0) - for { - talk, err = options.NewClient() + talk, err = options.NewClient() - if err != nil { - log.Error(err) + if err != nil { + log.Error(err) - os.Exit(1) - } else { - break - } + os.Exit(1) } // По идее keepalive должен же проходить только, если мы уже на сервере, так? - for { - if _, err := talk.SendKeepAlive(); err != nil { - log.Errorf("Try to send initial KeepAlive, got error: %s", err) - time.Sleep(time.Duration(config.Jabber.ReconnectDelay) * time.Second) + if _, err := talk.SendKeepAlive(); err != nil { + log.Errorf("Try to send initial KeepAlive, got error: %s", err) + time.Sleep(time.Duration(config.Jabber.ReconnectDelay) * time.Second) - // Понятно, что мы уже не в процессе установления соединения, а произошла ошибка - connecting = false - os.Exit(1) - } else { - log.Info("Connected") + // Понятно, что мы уже не в процессе установления соединения, а произошла ошибка + connecting = false - break - } + os.Exit(1) } + log.Info("Connected") + // Джойнимся к чятикам, но делаем это в фоне, чтобы не блочиться на ошибках, например, если бота забанили for _, room := range config.Jabber.Channels { go joinMuc(room) @@ -467,22 +500,7 @@ func establishConnection() { } } -func reEstablishConnection() { - // Серверные capabilities нам *уже* неизвестны - serverCapsQueried = false - serverCapsList = NewCollection() - mucCapsList = NewCollection() - lastMucActivity = NewCollection() - - // Пока соединение не установлено, мы про пинги ничего не знаем - serverPingTimestampRx = time.Now().Unix() // Считаем, что если коннект запустился, то первый пинг успешен - serverPingTimestampTx = 0 - time.Sleep(time.Duration(config.Jabber.ReconnectDelay) * time.Second) - connecting = false - establishConnection() -} - -// Джойнится к конференциям/каналам/комнатам в джаббере +// joinMuc(room string) джойнится к конференциям/каналам/комнатам в джаббере. func joinMuc(room string) { log.Debugf("Sending disco#info from %s to %s", talk.JID(), room) @@ -492,11 +510,13 @@ func joinMuc(room string) { os.Exit(1) } - // Ждём, пока muc нам вернёт список фичей + // Ждём, пока muc нам вернёт список фичей. for i := 0; i < (20 * int(config.Jabber.ConnectionTimeout)); i++ { - var myRoom interface{} - var supported bool - var exist bool + var ( + myRoom interface{} + supported bool + exist bool + ) time.Sleep(50 * time.Millisecond) @@ -508,11 +528,11 @@ func joinMuc(room string) { if supported, exist = myRoom.(map[string]bool)["muc_unsecured"]; exist { if supported { break - } else { - log.Infof("Unable to join to password-protected room. Don't know how to enter passwords :)") - - return } + + log.Infof("Unable to join to password-protected room. Don't know how to enter passwords :)") + + return } } @@ -520,20 +540,34 @@ func joinMuc(room string) { log.Errorf("Unable to join to MUC: %s", room) os.Exit(1) - } else { - log.Infof("Joining to MUC: %s", room) } - // Ждём, когда прилетит presence из комнаты, тогда мы точно знаем, что мы вошли + log.Infof("Joining to MUC: %s", room) + + // Ждём, когда прилетит presence из комнаты, тогда мы точно знаем, что мы вошли. + entered := false + for i := 0; i < (20 * int(config.Jabber.ConnectionTimeout)); i++ { time.Sleep(50 * time.Millisecond) if slices.Contains(roomsConnected, room) { + entered = true + break } } - // Вот теперь точно можно слать статус + if !entered { + log.Errorf( + "Unable to enter to MUC %s, join timeout after %d seconds (server does not return my presence for this room)", + room, + 20*int(config.Jabber.ConnectionTimeout)+1, + ) + + return + } + + // Вот теперь точно можно слать статус. log.Infof("Joined to MUC: %s", room) if _, err := talk.SendPresence( @@ -547,8 +581,9 @@ func joinMuc(room string) { } } -// Проверяет живость соединения с сервером -func probeServerLiveness() { +// probeServerLiveness() проверяет живость соединения с сервером. Для многих серверов обязательная штука, без которой +// они выкидывают клиента через некоторое время неактивности. +func probeServerLiveness() { //nolint:gocognit for { if shutdown { return @@ -569,8 +604,10 @@ func probeServerLiveness() { // } if serverCapsQueried { // Сервер ответил на disco#info - var value interface{} - var exist bool + var ( + value interface{} + exist bool + ) value, exist = serverCapsList.Get("urn:xmpp:ping") @@ -602,18 +639,18 @@ func probeServerLiveness() { if err := talk.PingC2S(talk.JID(), config.Jabber.Server); err != nil { os.Exit(1) - } else { - serverPingTimestampTx = time.Now().Unix() } + + serverPingTimestampTx = time.Now().Unix() } } else { // Первая пуля пока не вылетела, отправляем log.Debugf("Sending first c2s ping from %s to %s", talk.JID(), config.Jabber.Server) if err := talk.PingC2S(talk.JID(), config.Jabber.Server); err != nil { os.Exit(1) - } else { - serverPingTimestampTx = time.Now().Unix() } + + serverPingTimestampTx = time.Now().Unix() } // Сервер не анонсировал, что умеет в c2s пинги @@ -622,8 +659,6 @@ func probeServerLiveness() { if _, err := talk.SendKeepAlive(); err != nil { os.Exit(1) - - continue } } } else { // Сервер не ответил на disco#info @@ -631,28 +666,28 @@ func probeServerLiveness() { if _, err := talk.SendKeepAlive(); err != nil { os.Exit(1) - - continue } } } } -// Пингует MUC-и, нужно для проверки, что клиент ещё находится в MUC-е +// probeMUCLiveness() Пингует MUC-и, нужно для проверки, что клиент ещё находится в MUC-е. func probeMUCLiveness() { for { for _, room := range roomsConnected { - var exist bool - var lastActivityTs interface{} + var ( + exist bool + lastActivityTS interface{} + ) // Если записи про комнату нету, то пинговать её бессмысленно. - if lastActivityTs, exist = lastMucActivity.Get(room); !exist { + if lastActivityTS, exist = lastMucActivity.Get(room); !exist { continue } // Если время последней активности в чятике не превысило - // config.Jabber.ServerPingDelay + config.Jabber.PingSplayDelay, ничего не пингуем - if (time.Now().Unix() - lastActivityTs.(int64)) < (config.Jabber.ServerPingDelay + config.Jabber.PingSplayDelay) { + // config.Jabber.ServerPingDelay + config.Jabber.PingSplayDelay, ничего не пингуем. + if (time.Now().Unix() - lastActivityTS.(int64)) < (config.Jabber.ServerPingDelay + config.Jabber.PingSplayDelay) { continue } @@ -673,10 +708,10 @@ func probeMUCLiveness() { roomMap, exist = mucCapsList.Get(room) // Пинги комнаты проводим, только если она записана, как прошедшая disco#info и поддерживающая - // Server Optimization + // Server Optimization. if exist && roomMap.(map[string]bool)["http://jabber.org/protocol/muc#self-ping-optimization"] { go func(room string) { - // Небольшая рандомная задержка перед пингом комнаты + // Небольшая рандомная задержка перед пингом комнаты. sleepTime := time.Duration(rand.Int63n(1000*config.Jabber.PingSplayDelay)) * time.Millisecond //nolint:gosec time.Sleep(sleepTime) diff --git a/main.go b/main.go index 00bc56b..25775ac 100644 --- a/main.go +++ b/main.go @@ -94,8 +94,8 @@ func main() { Debug: verboseClient, Session: false, Status: "xa", - StatusMessage: "echo bot is starting up", - DialTimeout: 10 * time.Second, + StatusMessage: fmt.Sprintf("%s bot is starting up", config.Jabber.Nick), + DialTimeout: time.Duration(config.Jabber.ConnectionTimeout) * time.Second, } go sigHandler() @@ -145,7 +145,6 @@ func main() { default: // Это уже что-то странное. // Вероятно, ошибка парсинга xml. Собственно, баг сервера, тут мы ничего поделать не можем - os.Exit(1) } } diff --git a/types.go b/types.go index a2092d9..0baabda 100644 --- a/types.go +++ b/types.go @@ -4,7 +4,7 @@ import ( "encoding/xml" ) -// Конфиг +// myConfig прототип структурки с конфигом. type myConfig struct { Jabber struct { Server string `json:"server,omitempty"` @@ -32,15 +32,16 @@ type myConfig struct { NoEcho bool `json:"noecho,omitempty"` } -// Белый список +// myWhiteList прототип структурки с белым списком jid-ов. type myWhiteList struct { Whitelist []struct { RoomName string `json:"room_name,omitempty"` Jid []string `json:"jid,omitempty"` WipeBans bool `json:"wipe_bans,omitempty"` - } `json:"whitelist,omitampty"` + } `json:"whitelist,omitempty"` } +// myBlackList прототип структурки с чёрным списком jid-ов. type myBlackList struct { Blacklist []struct { RoomName string `json:"room_name,omitempty"` @@ -48,6 +49,8 @@ type myBlackList struct { } `json:"blacklist,omitempty"` } +// jabberSimpleIqGetQuery прототип структурки для разбора запросов xmpp discovery query, например, +// https://xmpp.org/extensions/xep-0030.html#example-18 . type jabberSimpleIqGetQuery struct { XMLName xml.Name `xml:"query"` Text string `xml:",chardata"` @@ -55,6 +58,7 @@ type jabberSimpleIqGetQuery struct { Node string `xml:"node,attr,omitempty"` // для xmlns="http://jabber.org/protocol/disco#items" } +// jabberPubsubIQGetQuery прототип структурки для разбора запросов xmpp pubsub. type jabberPubsubIQGetQuery struct { XMLName xml.Name `xml:"pubsub"` Text string `xml:",chardata"` @@ -66,6 +70,8 @@ type jabberPubsubIQGetQuery struct { } `xml:"items"` } +// jabberTimeIqGetQuery прототип структурки для разбора IQ запросов на локальное время клиента, +// https://xmpp.org/extensions/xep-0202.html type jabberTimeIqGetQuery struct { //