Update linter, fix linter errors

This commit is contained in:
eleksir
2025-11-15 15:52:34 +03:00
parent 3ebd2b48d1
commit ee5d40cfee
21 changed files with 1106 additions and 737 deletions

View File

@@ -1,3 +1,4 @@
// Package jabber contains all buny jabber bot guts.
package jabber
import (
@@ -12,11 +13,16 @@ import (
)
const (
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"
// goodDictionaryFile путь до файла с заведомо хорошими фразами.
goodDictionaryFile string = "./data/good_dictionary.txt" //nolint: unused
// badDictionaryFile путь до файла с заведемо плохими фразами.
badDictionaryFile string = "./data/bad_dictionary.txt" //nolint: unused
// dataFile путь до файла с данными тренировки фильтра.
dataFile string = "./data/data.bin" //nolint: unused
// Bad bayessian class with bad items.
Bad bayesian.Class = "Bad"
// Good bayessian class with good items.
Good bayesian.Class = "Good"
)
// Выдаёт "очки похожести" для данной фразы.

View File

@@ -27,7 +27,7 @@ func (j *Jabber) ReadWhitelist() error {
return err
}
whitelistJSONPath := fmt.Sprintf("%s/data/whitelist.json", filepath.Dir(executablePath))
whitelistJSONPath := filepath.Dir(executablePath) + "/data/whitelist.json"
locations := []string{
"~/.buny-jabber-bot-whitelist.json",
@@ -39,12 +39,12 @@ func (j *Jabber) ReadWhitelist() error {
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)
@@ -53,7 +53,7 @@ func (j *Jabber) ReadWhitelist() error {
buf, err := os.ReadFile(location)
// Не удалось прочитать, попробуем следующего кандидата
// Не удалось прочитать, попробуем следующего кандидата.
if err != nil {
log.Warnf("Skip reading whitelist file %s: %s", location, err)
@@ -65,12 +65,12 @@ func (j *Jabber) ReadWhitelist() error {
// сериализуем в json, а потом json превращаем в структурку. Не очень эффективно, но он и нечасто требуется.
var (
sampleWhitelist MyWhiteList
tmp map[string]interface{}
tmp map[string]any
)
err = hjson.Unmarshal(buf, &tmp)
// Не удалось распарсить - попробуем следующего кандидата
// Не удалось распарсить - попробуем следующего кандидата.
if err != nil {
log.Warnf("Skip parsing whitelist file %s: %s", location, err)
@@ -79,7 +79,7 @@ func (j *Jabber) ReadWhitelist() error {
tmpJSON, err := json.Marshal(tmp)
// Не удалось преобразовать map-ку в json
// Не удалось преобразовать map-ку в json.
if err != nil {
log.Warnf("Skip parsing whitelist file %s: %s", location, err)
@@ -123,7 +123,7 @@ func (j *Jabber) ReadBlacklist() error {
return err
}
whitelistJSONPath := fmt.Sprintf("%s/data/blacklist.json", filepath.Dir(executablePath))
whitelistJSONPath := filepath.Dir(executablePath) + "/data/blacklist.json"
locations := []string{
"~/.bunyPresense-jabber-bot-blacklist.json",
@@ -135,12 +135,12 @@ func (j *Jabber) ReadBlacklist() error {
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)
@@ -149,7 +149,7 @@ func (j *Jabber) ReadBlacklist() error {
buf, err := os.ReadFile(location)
// Не удалось прочитать, попробуем следующего кандидата
// Не удалось прочитать, попробуем следующего кандидата.
if err != nil {
log.Warnf("Skip reading blacklist file %s: %s", location, err)
@@ -161,12 +161,12 @@ func (j *Jabber) ReadBlacklist() error {
// сериализуем в json, а потом json превращаем в структурку. Не очень эффективно, но он и нечасто требуется.
var (
sampleBlacklist MyBlackList
tmp map[string]interface{}
tmp map[string]any
)
err = hjson.Unmarshal(buf, &tmp)
// Не удалось распарсить - попробуем следующего кандидата
// Не удалось распарсить - попробуем следующего кандидата.
if err != nil {
log.Warnf("Skip parsing blacklist file %s: %s", location, err)
@@ -175,7 +175,7 @@ func (j *Jabber) ReadBlacklist() error {
tmpJSON, err := json.Marshal(tmp)
// Не удалось преобразовать map-ку в json
// Не удалось преобразовать map-ку в json.
if err != nil {
log.Warnf("Skip parsing blacklist file %s: %s", location, err)

View File

@@ -4,6 +4,7 @@ import (
"encoding/json"
"fmt"
"regexp"
"slices"
"strings"
"github.com/davecgh/go-spew/spew"
@@ -17,9 +18,9 @@ func (j *Jabber) BunyPresense(v xmpp.Presence) error { //nolint:gocognit,gocyclo
var err error
// Если у presence-а есть JID и presence из одной из комнат, в которой мы есть и если его домен в чёрном
// списке, заносим пидора в список outcast-ов
// списке, заносим пидора в список outcast-ов.
if v.JID != "" {
// На всякий случай: себя никогда не баним, явным образом
// На всякий случай: себя никогда не баним, явным образом.
if v.JID == j.Talk.JID() {
return err
}
@@ -34,23 +35,19 @@ func (j *Jabber) BunyPresense(v xmpp.Presence) error { //nolint:gocognit,gocyclo
goodJid := strings.SplitN(v.JID, "/", 2)[0]
// Обрабатываем правила белого списка
// Обрабатываем правила белого списка.
for _, good := range j.WhiteList.Whitelist {
// Глобальный белый список
// Глобальный белый список.
if good.RoomName == "" {
for _, person := range good.Jid {
if person == goodJid {
return err
}
if slices.Contains(good.Jid, goodJid) {
return err
}
}
// Белый список для конкретных каналов
// Белый список для конкретных каналов.
if good.RoomName == room {
for _, person := range good.Jid {
if person == goodJid {
return err
}
if slices.Contains(good.Jid, goodJid) {
return err
}
}
}
@@ -74,16 +71,16 @@ func (j *Jabber) BunyPresense(v xmpp.Presence) error { //nolint:gocognit,gocyclo
evilNick := ""
evilNicks := strings.SplitN(v.From, "/", 2)
// Давайте-ка похэндлим нонсенс - когда у нас в строке нету разделителя
// Давайте-ка похэндлим нонсенс - когда у нас в строке нету разделителя.
if len(evilNicks) > 1 {
evilNick = evilNicks[1]
}
// Обрабатываем правила чёрного списка
// Обрабатываем правила чёрного списка.
for _, cRoom := range j.RoomsConnected {
if cRoom == room {
for _, bEntry := range j.BlackList.Blacklist {
// Обработаем правила глобального чёрного списка
// Обработаем правила глобального чёрного списка.
if bEntry.RoomName == "" {
for _, jidRegexp := range bEntry.JidRe {
if jidRegexp == "" {
@@ -147,7 +144,7 @@ func (j *Jabber) BunyPresense(v xmpp.Presence) error { //nolint:gocognit,gocyclo
nickRegexp,
)
// Баним именно jid
// Баним именно jid.
id, err := j.Squash(room, evilJid, bEntry.ReasonEnable, v.Type)
if err != nil {
@@ -167,7 +164,7 @@ func (j *Jabber) BunyPresense(v xmpp.Presence) error { //nolint:gocognit,gocyclo
continue
}
// Обработаем правила конкретного канала, комнаты конференции
// Обработаем правила конкретного канала, комнаты конференции.
if bEntry.RoomName == room {
for _, jidRegexp := range bEntry.JidRe {
if jidRegexp == "" {
@@ -232,7 +229,7 @@ func (j *Jabber) BunyPresense(v xmpp.Presence) error { //nolint:gocognit,gocyclo
nickRegexp,
)
// Баним именно jid
// Баним именно jid.
id, err := j.Squash(room, evilJid, bEntry.ReasonEnable, v.Type)
if err != nil {
@@ -265,8 +262,7 @@ func (j *Jabber) BunyPresense(v xmpp.Presence) error { //nolint:gocognit,gocyclo
func (j *Jabber) BunyChat(v xmpp.Chat) error {
var (
room = (strings.SplitN(v.Remote, "/", 2))[0]
// nick = (strings.SplitN(v.Remote, "/", 2))[1]
err error
err error
)
// Действовать мы можем только в рамках тех комнат, где явно присуствуем.
@@ -274,7 +270,7 @@ func (j *Jabber) BunyChat(v xmpp.Chat) error {
if cRoom == room {
// Перебирём правила чёрных списков.
for _, bEntry := range j.BlackList.Blacklist {
// Обработаем правила глобального чёрного списка
// Обработаем правила глобального чёрного списка.
if bEntry.RoomName == "" {
for _, phraseRegexp := range bEntry.PhraseRe {
if phraseRegexp == "" {
@@ -317,7 +313,7 @@ func (j *Jabber) BunyChat(v xmpp.Chat) error {
}
}
// Обработаем правила чёрного списка для конкретной комнаты
// Обработаем правила чёрного списка для конкретной комнаты.
if bEntry.RoomName == room {
for _, phraseRegexp := range bEntry.PhraseRe {
if phraseRegexp == "" {
@@ -364,10 +360,10 @@ func (j *Jabber) BunyChat(v xmpp.Chat) error {
// Если включено, проверяем фразу на КАПС.
for _, channel := range j.C.Jabber.Channels {
if channel.Name == room && channel.AllCaps.Enabled {
// Нормализуем строку и вырежем из неё пробелы
// Нормализуем строку и вырежем из неё пробелы.
normPhrase := strings.ReplaceAll(nString(v.Text), " ", "")
// Проверяем согласно тому, что длина фразы более чем сколько-то символов
// Проверяем согласно тому, что длина фразы более чем сколько-то символов.
if len(normPhrase) >= channel.AllCaps.MinLength {
normPhraseUpper := strings.ReplaceAll(nStringUpper(v.Text), " ", "")
@@ -401,7 +397,7 @@ func (j *Jabber) BunySoftwareVersion(v xmpp.IQ, ver IqResultSoftwareVersion) err
var err error
if v.From != "" {
// На всякий случай: себя никогда не баним, явным образом
// На всякий случай: себя никогда не баним, явным образом.
if v.From == j.Talk.JID() {
return err
}
@@ -420,6 +416,7 @@ func (j *Jabber) BunySoftwareVersion(v xmpp.IQ, ver IqResultSoftwareVersion) err
for _, presenceJSONString := range presenceJSONStrings {
var p xmpp.Presence
_ = json.Unmarshal([]byte(presenceJSONString), &p)
if p.Affiliation == "admin" || p.Affiliation == "owner" {
@@ -442,7 +439,7 @@ func (j *Jabber) BunySoftwareVersion(v xmpp.IQ, ver IqResultSoftwareVersion) err
for _, cRoom := range j.RoomsConnected {
if cRoom == room {
for _, bEntry := range j.BlackList.Blacklist {
// Обработаем правила глобального чёрного списка
// Обработаем правила глобального чёрного списка.
if bEntry.RoomName == "" {
for _, useragent := range bEntry.UserAgent {
switch {

View File

@@ -4,16 +4,18 @@ import (
"sync"
)
// Collection это sync.Map-ка, хранящая в себе произвольные итемы.
type Collection struct {
items sync.Map
close chan struct{}
}
type (
// Collection это sync.Map-ка, хранящая в себе произвольные итемы.
Collection struct {
close chan struct{}
items sync.Map
}
// Item представляет собой интерфейс для хранения произвольных данных.
type item struct {
data interface{}
}
// Item представляет собой интерфейс для хранения произвольных данных.
item struct {
data any
}
)
// NewCollection создаёт инстанс коллекции.
func NewCollection() *Collection {
@@ -25,7 +27,7 @@ func NewCollection() *Collection {
}
// Get достаёт данные по заданному ключу из коллекции.
func (collection *Collection) Get(key interface{}) (interface{}, bool) {
func (collection *Collection) Get(key any) (any, bool) {
obj, exists := collection.items.Load(key)
if !exists {
@@ -38,15 +40,15 @@ func (collection *Collection) Get(key interface{}) (interface{}, bool) {
}
// Set сохраняет данные с заданным ключом в коллекцию.
func (collection *Collection) Set(key interface{}, value interface{}) {
func (collection *Collection) Set(key any, value any) {
collection.items.Store(key, item{
data: value,
})
}
// Range применяет функцию f ко всем ключам в коллекции.
func (collection *Collection) Range(f func(key, value interface{}) bool) {
fn := func(key, value interface{}) bool {
func (collection *Collection) Range(f func(key, value any) bool) {
fn := func(key, value any) bool {
item := value.(item)
return f(key, item.data)
@@ -56,13 +58,14 @@ func (collection *Collection) Range(f func(key, value interface{}) bool) {
}
// Delete удаляет ключ и значение из коллекции данных.
func (collection *Collection) Delete(key interface{}) {
func (collection *Collection) Delete(key any) {
collection.items.Delete(key)
}
// Close очищает и высвобождает ресурсы, занятые коллекцией.
func (collection *Collection) Close() {
collection.close <- struct{}{}
collection.items = sync.Map{}
}

View File

@@ -14,8 +14,8 @@ import (
func (j *Jabber) Cmd(v xmpp.Chat) error {
var err error
switch {
case v.Text == fmt.Sprintf("%shelp", j.C.CSign) || v.Text == fmt.Sprintf("%sпомощь", j.C.CSign):
switch v.Text {
case j.C.CSign + "help", j.C.CSign + "помощь":
var (
answer string
chosenOneTalks = false
@@ -30,10 +30,11 @@ func (j *Jabber) Cmd(v xmpp.Chat) error {
}
if chosenOneTalks {
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 (available to bot admins only)\n", j.C.CSign)
answer += fmt.Sprintf("%sver|%sversion - prints version of software", j.C.CSign, j.C.CSign)
answer = j.C.CSign + "помощь - этот список команд\n"
answer += j.C.CSign + "help - this commands list\n"
answer += j.C.CSign + "rehash - reload white and black lists (available to bot admins only)\n"
answer += j.C.CSign + "ver | " + j.C.CSign + "version - prints version of software"
} else {
answer = "Ничем помочь не могу. Луна не светит на тебя."
}
@@ -56,15 +57,14 @@ func (j *Jabber) Cmd(v xmpp.Chat) error {
return err
}
case v.Text == fmt.Sprintf("%srehash", j.C.CSign): //nolint:wsl
case j.C.CSign + "rehash":
/* Эта команда может прилетать из приватной беседы с realjid-ом, из приватной беседы с chat-nick-ом, а также из
* чятика.
* Groupchat мы можем определить по v.Type (groupchat) и тогда понятно что делать.
* А вот, например, приватные притязания определить несколько сложно-вато. Но можно попробовать, например,
* пытаться по списку presence-ов находить персонажа и его real jid и уже из этого определять master он или нет.
*/
// Попробуем определить каков алгоритм наших действий, в зависимости от типа сообщения
// Попробуем определить каков алгоритм наших действий, в зависимости от типа сообщения.
switch v.Type {
case "groupchat":
// Проверим, является ли "командир" избранным или это самозванец?
@@ -90,6 +90,7 @@ func (j *Jabber) Cmd(v xmpp.Chat) error {
)
var msg xmpp.Chat
msg.Remote = v.Remote
msg.Type = v.Type
msg.Text = "Ничем помочь не могу. Луна не светит на тебя."
@@ -103,8 +104,9 @@ func (j *Jabber) Cmd(v xmpp.Chat) error {
listsLoaded = false
var msg xmpp.Chat
msg.Remote = v.Remote
msg.Type = "chat" // private message
msg.Type = "chat" // private message.
msg.Text = fmt.Sprint(err)
if _, err := j.Talk.Send(msg); err != nil {
@@ -116,8 +118,9 @@ func (j *Jabber) Cmd(v xmpp.Chat) error {
listsLoaded = false
var msg xmpp.Chat
msg.Remote = v.Remote
msg.Type = "chat" // private message
msg.Type = "chat" // private message.
msg.Text = fmt.Sprint(err)
if _, err := j.Talk.Send(msg); err != nil {
@@ -129,6 +132,7 @@ func (j *Jabber) Cmd(v xmpp.Chat) error {
room := (strings.SplitN(v.Remote, "/", 2))[0]
var msg xmpp.Chat
msg.Remote = room
msg.Type = v.Type
msg.Text = "Сделано"
@@ -137,17 +141,18 @@ func (j *Jabber) Cmd(v xmpp.Chat) error {
return err
}
}
case "chat":
room := (strings.SplitN(v.Remote, "/", 2))[0]
// Реагировать на приватную команду "из комнаты", только если бот в комнате
// Реагировать на приватную команду "из комнаты", только если бот в комнате.
if slices.Contains(j.RoomsConnected, room) {
var (
chosenOneTalks = false
listsLoaded = true
)
// Реагировать на rehash только если собеседник является bot master-ом
// Реагировать на rehash только если собеседник является bot master-ом.
realJID := j.GetRealJIDfromNick(v.Remote)
for _, master := range j.C.Jabber.BotMasters {
@@ -165,6 +170,7 @@ func (j *Jabber) Cmd(v xmpp.Chat) error {
)
var msg xmpp.Chat
msg.Remote = v.Remote
msg.Type = v.Type
msg.Text = "Ничем помочь не могу. Луна не светит на тебя."
@@ -178,6 +184,7 @@ func (j *Jabber) Cmd(v xmpp.Chat) error {
listsLoaded = false
var msg xmpp.Chat
msg.Remote = v.Remote
msg.Type = v.Type
msg.Text = fmt.Sprint(err)
@@ -191,6 +198,7 @@ func (j *Jabber) Cmd(v xmpp.Chat) error {
listsLoaded = false
var msg xmpp.Chat
msg.Remote = v.Remote
msg.Type = v.Type
msg.Text = fmt.Sprint(err)
@@ -202,6 +210,7 @@ func (j *Jabber) Cmd(v xmpp.Chat) error {
if listsLoaded {
var msg xmpp.Chat
msg.Remote = v.Remote
msg.Type = v.Type
msg.Text = "Сделано"
@@ -214,12 +223,14 @@ func (j *Jabber) Cmd(v xmpp.Chat) error {
default:
log.Infof("Rehash command from outer space: %s", spew.Sdump(v))
}
return err
case v.Text == fmt.Sprintf("%sver", j.C.CSign) || v.Text == fmt.Sprintf("%sversion", j.C.CSign):
case j.C.CSign + "ver", j.C.CSign + "version":
var (
answer = fmt.Sprintf("Version %s", j.C.Version)
answer = "Version " + j.C.Version
)
dest := v.Remote

View File

@@ -5,6 +5,7 @@ import (
"encoding/xml"
"fmt"
"runtime"
"slices"
"sort"
"strings"
"time"
@@ -12,26 +13,24 @@ import (
"github.com/davecgh/go-spew/spew"
"github.com/eleksir/go-xmpp"
log "github.com/sirupsen/logrus"
"golang.org/x/exp/slices"
)
// ParseEvent парсит ивенты, прилетающие из модуля xmpp (изменения presence, фразы участников, ошибки).
func (j *Jabber) ParseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo
func (j *Jabber) ParseEvent(e any) { //nolint:maintidx,gocognit,gocyclo
j.LastServerActivity = time.Now().Unix()
switch v := e.(type) {
// Сообщение в чяти
// Сообщение в чяти.
case xmpp.Chat:
log.Debugf("Looks like message, ChatType: %s, From: %s, Subject: %s Text: %s",
v.Type, v.Remote, v.Subject, v.Text)
// Топик чятика присылается в виде сообщения с subject, но без text
// В то же время сообщения от людей приходят с пустым subject, но с заполненным text
// Топик чятика присылается в виде сообщения с subject, но без text.
// В то же время сообщения от людей приходят с пустым subject, но с заполненным text.
if v.Text != "" {
// Чятики бывают групповые и не групповые, от этого зависит Remote, куда направлять сообщение
// Чятики бывают групповые и не групповые, от этого зависит Remote, куда направлять сообщение.
switch v.Type { //nolint:wsl,whitespace
// Групповой чятик
// Групповой чятик.
case "groupchat":
var (
room = strings.SplitN(v.Remote, "/", 2)[0]
@@ -64,7 +63,7 @@ func (j *Jabber) ParseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo
j.LastMucActivity.Set(muc, j.LastServerActivity)
}
// Приватный чятик
// Приватный чятик.
case "chat":
// Здесь у нас может быть 2 вида From:
// slackware-current@conference.jabber.ru/eleksir - это если сообщение из публичного чятика
@@ -80,7 +79,7 @@ func (j *Jabber) ParseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo
j.LastActivity = j.LastServerActivity
// Внезапно, ошибки. По идее они должны ассоциироваться с отправляемыми сообщениями, но по ходу это
// не реализовано, поэтому мы получаем ошибки в форме отдельного чятика
// не реализовано, поэтому мы получаем ошибки в форме отдельного чятика.
case "error":
// Это ошибка со стороны сервера, видимо, в ответ на наши действия и поскольку такие вещи происходят
// асинхронно, связать их с конкретными нашими действиями довольно сложно.
@@ -90,7 +89,7 @@ func (j *Jabber) ParseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo
// IQ-сообщение - пинг, понг, что-то ещё...
case xmpp.IQ:
// По правилам IQ обязательно должна содержать ID, причём не пустой
// По правилам IQ обязательно должна содержать ID, причём не пустой.
if v.ID == "" {
log.Info("Got an IQ stanza with empty id, discarding")
@@ -114,8 +113,7 @@ func (j *Jabber) ParseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo
if err := xml.Unmarshal(v.Query, &iqStruct); err == nil {
switch iqStruct.Xmlns { //nolint:wsl,whitespace
// Запрос номера версии приложения
// Запрос номера версии приложения.
case "jabber:iq:version":
log.Infof("Got IQ get request for version from %s", v.From)
@@ -138,7 +136,7 @@ func (j *Jabber) ParseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo
parseError = false
// Нам прислали попингуй
// Нам прислали попингуй.
case "urn:xmpp:ping":
log.Infof("Got IQ get request for pong from %s", v.From)
@@ -175,11 +173,12 @@ func (j *Jabber) ParseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo
parseError = false
// Запрос на список поддерживаемых фич - версия, локальное время, итд
// Запрос на список поддерживаемых фич - версия, локальное время, итд.
case xmpp.XMPPNS_DISCO_INFO:
log.Infof("Got IQ get disco#info request from %s", v.From)
answer := "<query xmlns=\"http://jabber.org/protocol/disco#info\">"
answer += "<feature var=\"jabber:iq:version\" />"
answer += "<feature var=\"urn:xmpp:time\" />"
answer += "<feature var=\"urn:xmpp:ping\" />"
@@ -209,7 +208,7 @@ func (j *Jabber) ParseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo
parseError = false
// Спрашивают список команд, чтобы поуправлять этим клиентом (xep-0030)
// Спрашивают список команд, чтобы поуправлять этим клиентом (xep-0030).
case xmpp.XMPPNS_DISCO_ITEMS:
log.Infof("Got IQ get disco#items request from %s, answer service unavailable", v.From)
@@ -231,7 +230,7 @@ func (j *Jabber) ParseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo
parseError = false
// Запрашивают что-то, о чём мы не имеем представления
// Запрашивают что-то, о чём мы не имеем представления.
default:
log.Info("Got an unknown IQ get request, discarding")
log.Info(spew.Sdump(e))
@@ -242,7 +241,7 @@ func (j *Jabber) ParseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo
parseError = true
}
// Попробуем распарсить входящий запрос как urn:xmpp:time
// Попробуем распарсить входящий запрос как urn:xmpp:time.
if parseError {
var iqStruct TimeIqGetQuery
@@ -276,7 +275,7 @@ func (j *Jabber) ParseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo
}
}
// Попробуем распарсить входящий запрос как http://jabber.org/protocol/pubsub
// Попробуем распарсить входящий запрос как http://jabber.org/protocol/pubsub.
if parseError {
var iqStruct PubsubIQGetQuery
@@ -305,6 +304,7 @@ func (j *Jabber) ParseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo
default:
log.Info("Got unknown IQ get pubsub something request, discarding")
log.Info(spew.Sdump(e))
parseError = true //nolint:wsl
}
} else {
@@ -312,7 +312,7 @@ func (j *Jabber) ParseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo
}
}
// Попробуем распарсить входящий запрос как urn:xmpp:ping он приходит при MUC Self-Ping (Schrödinger's Chat)
// Попробуем распарсить входящий запрос как urn:xmpp:ping он приходит при MUC Self-Ping (Schrödinger's Chat).
// Предполагается, что такие респонсы должны приходить только для пинга без серверной оптимизации.
// Хотя идейно мы не поддерживаем работу без серверной оптимизации, но на пинг ответим, нам несложно.
if parseError {
@@ -337,7 +337,7 @@ func (j *Jabber) ParseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo
parseError = false
}
// Не знаю, как парсить... залоггируем это дело и пойдём дальше
// Не знаю, как парсить... залоггируем это дело и пойдём дальше.
if parseError {
log.Infof("Does not look like parsable via iqStruct")
log.Info(spew.Sdump(e))
@@ -351,9 +351,10 @@ func (j *Jabber) ParseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo
}
switch {
// Похоже на pong от сервера (по стандарту в ответе нету query, но go-xmpp нам подсовывает это)
// Похоже на pong от сервера (по стандарту в ответе нету query, но go-xmpp нам подсовывает это).
case v.From == j.C.Jabber.Server && v.To == j.Talk.JID() && string(v.Query) == "<XMLElement></XMLElement>":
log.Debugf("Got S2C pong answer from %s to %s", v.From, v.To)
j.ServerPingTimestampRx = time.Now().Unix() //nolint:wsl
// Похоже на понг второй стадии xep-0410 MUC-Ping-а, который у нас не реализован.
@@ -378,13 +379,13 @@ func (j *Jabber) ParseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo
if mucNickMatch {
log.Debugf("Got 2-nd stage MUC pong answer (xep-0410) from %s to %s", v.From, v.To)
} else {
// Поскольку никакой логики у нас на этот счёт не предусмотрено, то просто пропускаем ответ
// Поскольку никакой логики у нас на этот счёт не предусмотрено, то просто пропускаем ответ.
log.Infof("Got unknown pong from %s to %s", v.From, v.To)
log.Debug(spew.Sdump(e))
}
}
// Подтверждение бана
// Подтверждение бана.
case v.To == j.Talk.JID() && v.ID == "ban1":
mucNickMatch := false
@@ -402,15 +403,15 @@ func (j *Jabber) ParseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo
}
}
// Формально, ответ должен парситься как пустой result и совпадать с типом jabberSimpleIqGetQuery
// Формально, ответ должен парситься как пустой result и совпадать с типом jabberSimpleIqGetQuery.
// Ответ приходит с 2 xmlns, похоже, он парсится неправильно в go-xmpp.
// TODO: после исправления go-xmpp, надо исправить это тут
// TODO: после исправления go-xmpp, надо исправить это тут.
var iqStruct SimpleIqGetQuery
if err := xml.Unmarshal(v.Query, &iqStruct); err == nil {
if iqStruct.Xmlns == "http://jabber.org/protocol/muc#admin" && iqStruct.Text == "" {
if mucNickMatch {
// Поскольку никакой логики у нас на этот счёт не предусмотрено, то просто пропускаем ответ
// Поскольку никакой логики у нас на этот счёт не предусмотрено, то просто пропускаем ответ.
log.Infof("Got ban successful from %s to %s", v.From, v.To)
} else {
log.Infof("Got ban successful from %s to %s, from room we are not in", v.From, v.To)
@@ -465,7 +466,7 @@ func (j *Jabber) ParseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo
log.Debug(spew.Sdump(e))
}
// Этот бот не управляется со стороны сервера, поэтому все попытки порулить игнорируем
// Этот бот не управляется со стороны сервера, поэтому все попытки порулить игнорируем.
case xmpp.IQTypeSet:
if muc, _ := strings.CutSuffix(v.To, "/"); muc != "" {
if slices.Contains(j.RoomsConnected, muc) {
@@ -493,7 +494,7 @@ func (j *Jabber) ParseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo
log.Debug(spew.Sdump(e))
// Нам прилетело сообщение об ошибке
// Нам прилетело сообщение об ошибке.
case xmpp.IQTypeError:
// Если сервер не хочет пинговаться и отвечает ошибкой на пинг, то наверно он не умеет в пинги,
// хотя если мы его пингуем, значит он анонсировал такой capability. Вот, засранец!
@@ -506,6 +507,7 @@ func (j *Jabber) ParseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo
if iqPingStruct.Xmlns == "urn:xmpp:ping" {
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)
@@ -521,7 +523,7 @@ func (j *Jabber) ParseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo
parseError = false
}
// Это у нас пинг xep-0410 и мы не в комнате, предполагается, что надо бы заджойниться
// Это у нас пинг xep-0410 и мы не в комнате, предполагается, что надо бы заджойниться.
if parseError {
var iqErrorCancelNotAcceptable IqErrorCancelNotAcceptable
@@ -572,13 +574,13 @@ func (j *Jabber) ParseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo
log.Error("Unhandled IQ Error message")
log.Error(spew.Sdump(e))
}
// Нам прилетело что-то неизвестное из семейства IQ stanza
// Нам прилетело что-то неизвестное из семейства IQ stanza.
default:
log.Info("Got an unknown IQ request. Dunno how deal with it, discarding")
log.Info(spew.Sdump(e))
}
// Смена статуса участника
// Смена статуса участника.
case xmpp.Presence:
if muc, _ := strings.CutSuffix(v.To, "/"); muc != "" {
if slices.Contains(j.RoomsConnected, muc) {
@@ -593,13 +595,21 @@ func (j *Jabber) ParseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo
// клиента забанят из-за спама "вошёл-вышел".
// Однако, это не значит, что такую ситуацию мы не должны корректным образом обрабатывать.
if v.Type == "unavailable" {
// Считаем, что мы больше не в комнате, поэтому не знаем, кто там есть
// Считаем, что мы больше не в комнате, поэтому не знаем, кто там есть.
j.RoomPresences.Delete(v.From)
log.Error("Presence notification - looks like another instance of client leaves room")
if slices.Contains(j.RoomsConnected, v.From) {
go j.JoinMuc(v.From)
j.GTomb.Go(
func() error {
if err := j.JoinMuc(v.From); err != nil {
return fmt.Errorf("unable to join MUC %s: %w", v.From, err)
}
return nil
},
)
}
} else {
log.Errorf("Presence notification, Type: %s, From: %s, Show: %s, Status: %s",
@@ -621,18 +631,19 @@ func (j *Jabber) ParseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo
return
}
// Это наш собственный Presence
// Это наш собственный Presence.
if v.Show == "" && v.Status == "" {
if nick == j.GetBotNickFromRoomConfig(room) {
j.RoomsConnected = append(j.RoomsConnected, room)
// На всякий случай дедуплицируем список комнат, к которым мы заджойнились.
sort.Strings(j.RoomsConnected)
slices.Compact(j.RoomsConnected)
j.RoomsConnected = slices.Compact(j.RoomsConnected)
}
}
switch v.Role {
// Участник ушёл
// Участник ушёл.
case "none":
if presenceJSONInterface, present := j.RoomPresences.Get(room); present {
presenceJSONStrings := InterfaceToStringSlice(presenceJSONInterface)
@@ -641,6 +652,7 @@ func (j *Jabber) ParseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo
for _, presenceJSONstring := range presenceJSONStrings {
var p xmpp.Presence
_ = json.Unmarshal([]byte(presenceJSONstring), &p)
if p.From == v.From {
@@ -652,7 +664,7 @@ func (j *Jabber) ParseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo
j.RoomPresences.Set(room, newPresenceJSONStrings)
}
// Участник пришёл
// Участник пришёл.
default:
var (
presenceJSONStrings []string
@@ -666,6 +678,7 @@ func (j *Jabber) ParseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo
for _, presenceJSONString := range presenceJSONStrings {
var p xmpp.Presence
_ = json.Unmarshal([]byte(presenceJSONString), &p)
// Если находим, что у нас уже есть клиент с таким же From, то есть полным nick-ом (для groupchat)
@@ -696,7 +709,7 @@ func (j *Jabber) ParseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo
}
}
// Ответ на запрос поддерживаемых фич, который "http://jabber.org/protocol/disco#info"
// Ответ на запрос поддерживаемых фич, который "http://jabber.org/protocol/disco#info".
case xmpp.DiscoResult:
if muc, _ := strings.CutSuffix(v.To, "/"); muc != "" {
if slices.Contains(j.RoomsConnected, muc) {
@@ -704,12 +717,12 @@ func (j *Jabber) ParseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo
}
}
// Я видел 2 типа disco result и они отличались только []identities. Попробуем вытащить известный identity
// TODO: проверять адресата v.To
// Я видел 2 типа disco result и они отличались только []identities. Попробуем вытащить известный identity.
// TODO: проверять адресата v.To.
for _, ident := range v.Identities {
switch ident.Category {
case "server":
// Конкретно сейчас нас интересует только поддержка c2s ping
// Конкретно сейчас нас интересует только поддержка c2s ping.
for _, feature := range v.Features {
log.Debugf("Server %s announced that it supports feature: %s", v.From, feature)
j.ServerCapsList.Set(feature, true)
@@ -722,6 +735,7 @@ func (j *Jabber) ParseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo
for _, feature := range v.Features {
log.Debugf("MUC %s announced that it supports feature: %s", v.From, feature)
mucCaps[feature] = true //nolint:wsl
}
@@ -736,7 +750,7 @@ func (j *Jabber) ParseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo
}
}
// Это что-то неизвестное, подампим событие в лог
// Это что-то неизвестное, подампим событие в лог.
default:
log.Info(spew.Sdump(e))
}

View File

@@ -9,7 +9,7 @@ import (
// MyLoop - основной цикл программы.
func (j *Jabber) MyLoop() error {
for {
// Зададим начальное значение глобальным переменным
// Зададим начальное значение глобальным переменным.
j.ServerPingTimestampRx = 0
j.ServerPingTimestampTx = 0
j.RoomsConnected = make([]string, 1)
@@ -23,7 +23,7 @@ func (j *Jabber) MyLoop() error {
j.ServerPingTimestampRx = 0
j.RoomPresences = NewCollection()
// Установим коннект
// Установим коннект.
if err := j.EstablishConnection(); err != nil {
log.Error(err)
@@ -48,14 +48,8 @@ func (j *Jabber) MyLoop() error {
// j.GTomb.Go() тут зовётся внутри на каждую комнату, если у нас ошибка, эта горутинка рано или поздно выйдет.
go j.ProbeMUCLiveness()
// Гребём ивенты...
for {
// Стриггерилось завершение работы приложения, или соединение не установлено (порвалось, например)
// грести не надо
if j.Shutdown {
break
}
// Гребём ивенты, пока стриггерилось завершение работы приложения, или соединение не порвалось.
for !j.Shutdown {
if !j.IsConnected {
// Tight loop - это наверно не очень хорошо, думаю, ничего страшного не будет, если мы поспим 100мс.
time.Sleep(100 * time.Millisecond)
@@ -63,7 +57,7 @@ func (j *Jabber) MyLoop() error {
continue
}
// Вынимаем ивент из "провода"
// Вынимаем ивент из "провода".
chat, err := j.Talk.Recv()
if err != nil {

View File

@@ -28,7 +28,7 @@ func (j *Jabber) ReadConfig() error { //nolint:gocognit,gocyclo
return err
}
configJSONPath := fmt.Sprintf("%s/data/config.json", filepath.Dir(executablePath))
configJSONPath := filepath.Dir(executablePath) + "/data/config.json"
locations := []string{
"~/.buny-jabber-bot.json",
@@ -66,7 +66,7 @@ func (j *Jabber) ReadConfig() error { //nolint:gocognit,gocyclo
// сериализуем в json, а потом json превращаем в структурку. Не очень эффективно, но он и нечасто требуется.
var (
sampleConfig MyConfig
tmp map[string]interface{}
tmp map[string]any
)
err = hjson.Unmarshal(buf, &tmp)
@@ -80,7 +80,7 @@ func (j *Jabber) ReadConfig() error { //nolint:gocognit,gocyclo
tmpJSON, err := json.Marshal(tmp)
// Не удалось преобразовать map-ку в json
// Не удалось преобразовать map-ку в json.
if err != nil {
log.Warnf("Skip parsing config file %s: %s", location, err)
@@ -93,9 +93,10 @@ func (j *Jabber) ReadConfig() error { //nolint:gocognit,gocyclo
continue
}
// Значения для Jabber-клиента
// Значения для Jabber-клиента.
if sampleConfig.Jabber.Server == "" {
log.Error("Jabber server is not defined in config, using localhost")
sampleConfig.Jabber.Server = "localhost" //nolint:wsl
}
@@ -121,7 +122,7 @@ func (j *Jabber) ReadConfig() error { //nolint:gocognit,gocyclo
sampleConfig.Jabber.SslVerify = false
}
// sampleConfig.Jabber.InsecureAllowUnencryptedAuth = false, если не задан
// sampleConfig.Jabber.InsecureAllowUnencryptedAuth = false, если не задан.
if sampleConfig.Jabber.ConnectionTimeout == 0 {
sampleConfig.Jabber.ConnectionTimeout = 10
@@ -175,14 +176,14 @@ func (j *Jabber) ReadConfig() error { //nolint:gocognit,gocyclo
log.Infof("Jabber user not defined in config, guessing, it can be %s", sampleConfig.Jabber.User)
}
// Если sampleConfig.Jabber.Password не задан, то авторизации не будет
// Если sampleConfig.Jabber.Password не задан, то авторизации не будет.
// Если не задано ни одного мастера, то бот сам себе мастер
// Если не задано ни одного мастера, то бот сам себе мастер.
if len(sampleConfig.Jabber.BotMasters) == 0 {
sampleConfig.Jabber.BotMasters[0] = sampleConfig.Jabber.User
}
// Нам бот нужен в каких-то чат-румах, а не "просто так"
// Нам бот нужен в каких-то чат-румах, а не "просто так".
if len(sampleConfig.Jabber.Channels) < 1 {
return errors.New("no jabber channels/rooms defined in config, quitting") //nolint:goerr113
}
@@ -196,8 +197,8 @@ func (j *Jabber) ReadConfig() error { //nolint:gocognit,gocyclo
sampleConfig.Jabber.Channels[n].Nick = sampleConfig.Jabber.Nick
}
// channel.Password может быть пустым, тогда пароля нет
// channel.Bayes.Enabled будет false, если не проставлен
// channel.Password может быть пустым, тогда пароля нет.
// channel.Bayes.Enabled будет false, если не проставлен.
if channel.Bayes.MinLength < 40 {
channel.Bayes.MinLength = 40
@@ -215,7 +216,7 @@ func (j *Jabber) ReadConfig() error { //nolint:gocognit,gocyclo
channel.Bayes.DefaultAction = "log"
}
// channel.AllCaps.Enabled будет false, если не указан
// channel.AllCaps.Enabled будет false, если не указан.
if channel.AllCaps.MinLength < 10 {
channel.AllCaps.MinLength = 10
}
@@ -229,23 +230,23 @@ func (j *Jabber) ReadConfig() error { //nolint:gocognit,gocyclo
}
}
// Если список фраз с которыми стартует бот пустой, вносим в него 1 запись с пустой строкой
// Если список фраз с которыми стартует бот пустой, вносим в него 1 запись с пустой строкой.
if len(sampleConfig.Jabber.StartupStatus) == 0 {
sampleConfig.Jabber.StartupStatus[0] = ""
}
// Если список статусов, с которыми работает бот пустой, вносим в него 1 запись с пустой строкой
// Если список статусов, с которыми работает бот пустой, вносим в него 1 запись с пустой строкой.
if len(sampleConfig.Jabber.RuntimeStatus.Text) == 0 {
sampleConfig.Jabber.RuntimeStatus.Text[0] = ""
}
// Если sampleConfig.Jabber.RuntimeStatus.RotationTime не задан, то он равен 0
// Если sampleConfig.Jabber.RuntimeStatus.RotationSplayTime не задан, то он равен 0
// Если sampleConfig.Jabber.RuntimeStatus.RotationTime не задан, то он равен 0.
// Если sampleConfig.Jabber.RuntimeStatus.RotationSplayTime не задан, то он равен 0.
// Если sampleConfig.Jabber.BanDelay не задан, то он равен 0
// Если sampleConfig.Jabber.BanPhrasesEnable не задан, то он false
// Если sampleConfig.Jabber.BanDelay не задан, то он равен 0.
// Если sampleConfig.Jabber.BanPhrasesEnable не задан, то он false.
// Если список фраз, с которыми банят пустой, то вносим в него одну позицию с пустой строкой
// Если список фраз, с которыми банят пустой, то вносим в него одну позицию с пустой строкой.
if len(sampleConfig.Jabber.BanPhrases) == 0 {
sampleConfig.Jabber.BanPhrases[0] = ""
}
@@ -260,7 +261,7 @@ func (j *Jabber) ReadConfig() error { //nolint:gocognit,gocyclo
log.Info("loglevel not defined in config, using info")
}
// sampleConfig.Log = "" if not set
// sampleConfig.Log = "" if not set.
j.C = sampleConfig
configLoaded = true

View File

@@ -8,6 +8,8 @@ import (
log "github.com/sirupsen/logrus"
)
// SigHandler срабатывает если происходит завершение программы, инициированное либо init-ом, либо пользователем.
// Обрабатвает сигналы INT, TERM, QUIT.
func (j *Jabber) SigHandler() error {
log.Debug("Installing signal handler")

View File

@@ -38,6 +38,7 @@ func (j *Jabber) Squash(room, jid string, reasonEnable bool, vType string) (stri
if reasonEnable {
var t = time.Now()
ban += fmt.Sprintf(
"<reason>autoban at %04d.%02d.%02d %02d:%02d:%02d</reason>",
t.Year(),

View File

@@ -8,208 +8,209 @@ import (
"gopkg.in/tomb.v2"
)
// MyConfig прототип структурки с конфигом.
type MyConfig struct {
type (
// MyConfig прототип структурки с конфигом.
MyConfig struct {
CSign string `json:"csign,omitempty"`
Loglevel string `json:"loglevel,omitempty"`
Log string `json:"log,omitempty"`
Version string `json:"version,omitempty"`
ExeName string `json:"exe_name,omitempty"`
Jabber struct {
Server string `json:"server,omitempty"`
Nick string `json:"nick,omitempty"`
Resource string `json:"resource,omitempty"`
User string `json:"user,omitempty"`
Password string `json:"password,omitempty"`
StartupStatus []string `json:"startup_status,omitempty"`
BotMasters []string `json:"bot_masters,omitempty"`
BanPhrases []string `json:"ban_phrases,omitempty"`
RuntimeStatus struct {
Text []string `json:"text,omitempty"`
RotationTime int64 `json:"rotation_time,omitempty"`
RotationSplayTime int64 `json:"rotation_splay_time,omitempty"`
} `json:"runtime_status"`
Channels []struct {
Name string `json:"name,omitempty"`
Nick string `json:"nick,omitempty"`
Password string `json:"password,omitempty"`
AllCaps struct {
DefaultAction string `json:"default_action,omitempty"`
MinLength int `json:"min_length,omitempty"`
Enabled bool `json:"enabled,omitempty"`
} `json:"all_caps"`
Bayes struct {
DefaultAction string `json:"default_action,omitempty"`
MinWords int64 `json:"min_words,omitempty"`
MinLength int `json:"min_length,omitempty"`
Enabled bool `json:"enabled,omitempty"`
} `json:"bayes"`
} `json:"channels"`
ConnectionTimeout int64 `json:"connection_timeout,omitempty"`
ReconnectDelay int64 `json:"reconnect_delay,omitempty"`
ServerPingDelay int64 `json:"server_ping_delay,omitempty"`
MucPingDelay int64 `json:"muc_ping_delay,omitempty"`
MucRejoinDelay int64 `json:"muc_rejoin_delay,omitempty"`
PingSplayDelay int64 `json:"ping_splay_delay,omitempty"`
BanDelay int64 `json:"ban_delay,omitempty"`
Port int `json:"port,omitempty"`
Ssl bool `json:"ssl,omitempty"`
StartTLS bool `json:"starttls,omitempty"`
SslVerify bool `json:"ssl_verify,omitempty"`
InsecureAllowUnencryptedAuth bool `json:"insecureallowunencryptedauth,omitempty"`
BanPhrasesEnable bool `json:"ban_phrases_enable,omitempty"`
} `json:"jabber"`
}
// MyWhiteList прототип структурки с белым списком jid-ов.
MyWhiteList struct {
Whitelist []struct {
RoomName string `json:"room_name,omitempty"`
Jid []string `json:"jid,omitempty"`
WipeBans bool `json:"wipe_bans,omitempty"`
} `json:"whitelist,omitempty"`
}
// MyBlackList прототип структурки с чёрным списком jid-ов.
MyBlackList struct {
Blacklist []struct {
RoomName string `json:"room_name,omitempty"`
JidRe []string `json:"jid_re,omitempty"`
NickRe []string `json:"nick_re,omitempty"`
PhraseRe []string `json:"phrase_re,omitempty"`
UserAgent []struct {
Name string `json:"name,omitempty"`
Version string `json:"version,omitempty"`
Os string `json:"os,omitempty"`
} `json:"user_agent"`
ReasonEnable bool `json:"reason_enable,omitempty"`
} `json:"blacklist"`
}
// Jabber основная структура-объект, содержащая стейты и проч.
Jabber struct {
Server string `json:"server,omitempty"`
Port int `json:"port,omitempty"`
Ssl bool `json:"ssl,omitempty"`
StartTLS bool `json:"starttls,omitempty"`
SslVerify bool `json:"ssl_verify,omitempty"`
InsecureAllowUnencryptedAuth bool `json:"insecureallowunencryptedauth,omitempty"`
ConnectionTimeout int64 `json:"connection_timeout,omitempty"`
ReconnectDelay int64 `json:"reconnect_delay,omitempty"`
ServerPingDelay int64 `json:"server_ping_delay,omitempty"`
MucPingDelay int64 `json:"muc_ping_delay,omitempty"`
MucRejoinDelay int64 `json:"muc_rejoin_delay,omitempty"`
PingSplayDelay int64 `json:"ping_splay_delay,omitempty"`
Nick string `json:"nick,omitempty"`
Resource string `json:"resource,omitempty"`
User string `json:"user,omitempty"`
Password string `json:"password,omitempty"`
BotMasters []string `json:"bot_masters,omitempty"`
Channels []struct {
Name string `json:"name,omitempty"`
Nick string `json:"nick,omitempty"`
Password string `json:"password,omitempty"`
Bayes struct {
Enabled bool `json:"enabled,omitempty"`
MinWords int64 `json:"min_words,omitempty"`
MinLength int `json:"min_length,omitempty"`
DefaultAction string `json:"default_action,omitempty"`
} `json:"bayes,omitempty"`
AllCaps struct {
Enabled bool `json:"enabled,omitempty"`
MinLength int `json:"min_length,omitempty"`
DefaultAction string `json:"default_action,omitempty"`
} `json:"all_caps,omitempty"`
} `json:"channels"`
StartupStatus []string `json:"startup_status,omitempty"`
RuntimeStatus struct {
Text []string `json:"text,omitempty"`
RotationTime int64 `json:"rotation_time,omitempty"`
RotationSplayTime int64 `json:"rotation_splay_time,omitempty"`
} `json:"runtime_status,omitempty"`
BanDelay int64 `json:"ban_delay,omitempty"`
BanPhrasesEnable bool `json:"ban_phrases_enable,omitempty"`
BanPhrases []string `json:"ban_phrases,omitempty"`
} `json:"jabber,omitempty"`
// C - конфиг, как он распарсился из конфиг-файла.
C MyConfig
CSign string `json:"csign,omitempty"`
Loglevel string `json:"loglevel,omitempty"`
Log string `json:"log,omitempty"`
Version string `json:"version,omitempty"`
ExeName string `json:"exe_name,omitempty"`
}
// WhiteList - структурка с безусловно разрешёнными jid-ами.
WhiteList MyWhiteList
// 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,omitempty"`
}
// BlackList - структурка с запрещёнными по регуляркам фразами, никами, jid-ами.
BlackList MyBlackList
// MyBlackList прототип структурки с чёрным списком jid-ов.
type MyBlackList struct {
Blacklist []struct {
RoomName string `json:"room_name,omitempty"`
ReasonEnable bool `json:"reason_enable,omitempty"`
JidRe []string `json:"jid_re,omitempty"`
NickRe []string `json:"nick_re,omitempty"`
PhraseRe []string `json:"phrase_re,omitempty"`
UserAgent []struct {
Name string `json:"name,omitempty"`
Version string `json:"version,omitempty"`
Os string `json:"os,omitempty"`
} `json:"user_agent,omitempty"`
} `json:"blacklist,omitempty"`
}
// Опции подключения к xmpp-серверу.
Options *xmpp.Options
// Jabber основная структура-объект, содержащая стейты и проч.
type Jabber struct {
// C - конфиг, как он распарсился из конфиг-файла.
C MyConfig
// gTomb пул активных горутин.
GTomb tomb.Tomb
// WhiteList - структурка с безусловно разрешёнными jid-ами
WhiteList MyWhiteList
// Talk основная структурка xmpp-клиента.
Talk *xmpp.Client
// BlackList - структурка с запрещёнными по регуляркам фразами, никами, jid-ами.
BlackList MyBlackList
// sync.Map-ка с капабилити сервера.
ServerCapsList *Collection
// Опции подключения к xmpp-серверу.
Options *xmpp.Options
// ServerCapsQueried показывает, были ли запрошены capabilities сервера.
ServerCapsQueried bool
// gTomb пул активных горутин.
GTomb tomb.Tomb
// Время последней активности, нужно для c2s пингов - посылаем пинги, только если давненько ничего не приходило с
// сервера.
LastServerActivity int64
// Talk основная структурка xmpp-клиента.
Talk *xmpp.Client
// Время, когда был отправлен c2s ping.
ServerPingTimestampTx int64
// sync.Map-ка с капабилити сервера.
ServerCapsList *Collection
// Время, когда был принят s2c pong.
ServerPingTimestampRx int64
// ServerCapsQueried показывает, были ли запрошены capabilities сервера.
ServerCapsQueried bool
// Время последней активности, нужно для jabber:iq:last.
LastActivity int64
// Время последней активности, нужно для c2s пингов - посылаем пинги, только если давненько ничего не приходило с
// сервера.
LastServerActivity int64
// sync.Map-ка с комнатами и их capability.
MucCapsList *Collection
// Время, когда был отправлен c2s ping.
ServerPingTimestampTx int64
// Время последней активности MUC-ов, нужно для пингов - посылаем пинги, только если давненько ничего не приходило из
// muc-ов.
LastMucActivity *Collection
// Время, когда был принят s2c pong.
ServerPingTimestampRx int64
// Список комнат, в которых находится бот.
RoomsConnected []string
// Время последней активности, нужно для jabber:iq:last.
LastActivity int64
// sync.Map-ка со списком участников конференций (в json-формате, согласно структуре xmpp.Presence, "room".[]json).
RoomPresences *Collection
// sync.Map-ка с комнатами и их capability.
MucCapsList *Collection
// Канал, по котором приходят сообщения о том, что ОС отправила некие сигналы процессу.
SigChan chan os.Signal
// Время последней активности MUC-ов, нужно для пингов - посылаем пинги, только если давненько ничего не приходило из
// muc-ов.
LastMucActivity *Collection
// Индиктор того, что процесс завершается.
Shutdown bool
// Список комнат, в которых находится бот.
RoomsConnected []string
// Индиктор того, что соединение установлено.
IsConnected bool
// sync.Map-ка со списком участников конференций (в json-формате, согласно структуре xmpp.Presence, "room".[]json).
RoomPresences *Collection
// Индикатор того, что соединение в процессе достукивания до сервера.
Connecting bool
}
// Канал, по котором приходят сообщения о том, что ОС отправила некие сигналы процессу.
SigChan chan os.Signal
// SimpleIqGetQuery прототип структурки для разбора запросов xmpp discovery query, например,
// https://xmpp.org/extensions/xep-0030.html#example-18 .
SimpleIqGetQuery 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"
}
// Индиктор того, что процесс завершается.
Shutdown bool
// PubsubIQGetQuery прототип структурки для разбора запросов xmpp pubsub.
PubsubIQGetQuery struct {
XMLName xml.Name `xml:"pubsub"`
Text string `xml:",chardata"`
Xmlns string `xml:"xmlns,attr"`
Items struct {
Text string `xml:",chardata"`
Node string `xml:"node,attr"`
MaxItems string `xml:"max_items,attr"`
} `xml:"items"`
}
// Индиктор того, что соединение установлено.
IsConnected bool
// TimeIqGetQuery прототип структурки для разбора IQ запросов на локальное время клиента,
// https://xmpp.org/extensions/xep-0202.html
TimeIqGetQuery struct {
// <time xmlns="urn:xmpp:time"/>.
XMLName xml.Name `xml:"time"`
Text string `xml:",chardata"`
Xmlns string `xml:"xmlns,attr"`
}
// Индикатор того, что соединение в процессе достукивания до сервера.
Connecting bool
}
// IqPing прототип структурки для разбора IQ запросов на пинг клиента, https://xmpp.org/extensions/xep-0199.html
IqPing struct {
XMLName xml.Name `xml:"ping"`
Text string `xml:",chardata"`
Xmlns string `xml:"xmlns,attr"`
}
// SimpleIqGetQuery прототип структурки для разбора запросов xmpp discovery query, например,
// https://xmpp.org/extensions/xep-0030.html#example-18 .
type SimpleIqGetQuery 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"
}
// IqErrorCancelNotAcceptable прототип структурки для разбора IQ ответов, когда сервис (сервер, клиент, etc) не
// может или не хочет принимать наш iq-запрос https://xmpp.org/extensions/xep-0099.html
IqErrorCancelNotAcceptable struct {
XMLName xml.Name `xml:"error"`
Text string `xml:",chardata"`
Type string `xml:"type,attr"`
By string `xml:"by,attr"`
NotAcceptable struct {
Text string `xml:",chardata"`
Xmlns string `xml:"xmlns,attr"`
} `xml:"not-acceptable"`
}
// PubsubIQGetQuery прототип структурки для разбора запросов xmpp pubsub.
type PubsubIQGetQuery struct {
XMLName xml.Name `xml:"pubsub"`
Text string `xml:",chardata"`
Xmlns string `xml:"xmlns,attr"`
Items struct {
Text string `xml:",chardata"`
Node string `xml:"node,attr"`
MaxItems string `xml:"max_items,attr"`
} `xml:"items"`
}
// TimeIqGetQuery прототип структурки для разбора IQ запросов на локальное время клиента,
// https://xmpp.org/extensions/xep-0202.html
type TimeIqGetQuery struct {
// <time xmlns="urn:xmpp:time"/>
XMLName xml.Name `xml:"time"`
Text string `xml:",chardata"`
Xmlns string `xml:"xmlns,attr"`
}
// IqPing прототип структурки для разбора IQ запросов на пинг клиента, https://xmpp.org/extensions/xep-0199.html
type IqPing struct {
XMLName xml.Name `xml:"ping"`
Text string `xml:",chardata"`
Xmlns string `xml:"xmlns,attr"`
}
// IqErrorCancelNotAcceptable прототип структурки для разбора IQ ответов, когда сервис (сервер, клиент, etc) не
// может или не хочет принимать наш iq-запрос https://xmpp.org/extensions/xep-0099.html
type IqErrorCancelNotAcceptable struct {
XMLName xml.Name `xml:"error"`
Text string `xml:",chardata"`
Type string `xml:"type,attr"`
By string `xml:"by,attr"`
NotAcceptable struct {
Text string `xml:",chardata"`
Xmlns string `xml:"xmlns,attr"`
} `xml:"not-acceptable"`
}
// IqResultSoftwareVersion прототип структурки для разбора IQ ответов на запрос о названии и версии клиентского ПО.
type IqResultSoftwareVersion struct {
XMLName xml.Name `xml:"query"`
Text string `xml:",chardata"`
Xmlns string `xml:"xmlns,attr"`
Name string `xml:"name"`
Version string `xml:"version"`
Os string `xml:"os,omitempty"`
}
// IqResultSoftwareVersion прототип структурки для разбора IQ ответов на запрос о названии и версии клиентского ПО.
IqResultSoftwareVersion struct {
XMLName xml.Name `xml:"query"`
Text string `xml:",chardata"`
Xmlns string `xml:"xmlns,attr"`
Name string `xml:"name"`
Version string `xml:"version"`
Os string `xml:"os,omitempty"`
}
)
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */

View File

@@ -26,16 +26,16 @@ func RandomPhrase(list []string) string {
// InterfaceToStringSlice превращает данный интерфейс в слайс строк.
// Если может, конечно :) .
func InterfaceToStringSlice(iface interface{}) []string {
func InterfaceToStringSlice(iface any) []string {
var mySlice []string
// А теперь мы начинаем дурдом, нам надо превратить ёбанный interface{} в []string
// Поскольку interface{} может быть чем угодно, перестрахуемся
// А теперь мы начинаем дурдом, нам надо превратить ёбанный any в []string.
// Поскольку any может быть чем угодно, перестрахуемся.
if reflect.TypeOf(iface).Kind() == reflect.Slice {
shit := reflect.ValueOf(iface)
for i := 0; i < shit.Len(); i++ {
mySlice = append(mySlice, fmt.Sprint(shit.Index(i)))
for v := range shit.Seq() {
mySlice = append(mySlice, v.String())
}
}
@@ -49,10 +49,10 @@ func (j *Jabber) GetRealJIDfromNick(fullNick string) string {
room := (strings.SplitN(fullNick, "/", 2))[0]
// Достанем presence участника
// Достанем presence участника.
presenceJSONInterface, present := j.RoomPresences.Get(room)
// Никого нет дома
// Никого нет дома.
if !present {
return ""
}
@@ -85,8 +85,9 @@ func (j *Jabber) GetBotNickFromRoomConfig(room string) string {
// RotateStatus периодически изменяет статус бота в MUC-е согласно настройкам из кофига.
func (j *Jabber) RotateStatus(room string) error {
for {
// TODO: Переделать на ticker-ы
// TODO: Переделать на ticker-ы.
totalSleepTime := time.Duration(j.C.Jabber.RuntimeStatus.RotationTime) * time.Second
totalSleepTime += time.Duration(j.C.Jabber.RuntimeStatus.RotationSplayTime) * time.Second
for {
@@ -129,9 +130,9 @@ func (j *Jabber) JoinMuc(room string) error {
}
// Ждём, пока muc нам вернёт список фичей.
for i := 0; i < (20 * int(j.C.Jabber.ConnectionTimeout)); i++ {
for range 20 * int(j.C.Jabber.ConnectionTimeout) {
var (
myRoom interface{}
myRoom any
supported bool
exist bool
)
@@ -154,7 +155,7 @@ func (j *Jabber) JoinMuc(room string) error {
}
}
// Заходим в конфу, наконец-то
// Заходим в конфу, наконец-то.
if _, err := j.Talk.JoinMUCNoHistory(room, j.GetBotNickFromRoomConfig(room)); err != nil {
return fmt.Errorf("unable to join to MUC: %s, %w", room, err)
}
@@ -164,7 +165,7 @@ func (j *Jabber) JoinMuc(room string) error {
// Ждём, когда прилетит presence из комнаты, тогда мы точно знаем, что мы вошли.
entered := false
for i := 0; i < (20 * int(j.C.Jabber.ConnectionTimeout)); i++ {
for range 20 * int(j.C.Jabber.ConnectionTimeout) {
time.Sleep(50 * time.Millisecond)
if slices.Contains(j.RoomsConnected, room) {
@@ -189,7 +190,7 @@ func (j *Jabber) JoinMuc(room string) error {
j.GTomb.Go(func() error { return j.RotateStatus(room) })
// Время проверить участников на предмет злобности
// Время проверить участников на предмет злобности.
namesInterface, present := j.RoomPresences.Get(room)
// Если room есть в списке presence-ов, то фигачим. room там должен быть, просто обязан.
@@ -221,6 +222,7 @@ func (j *Jabber) EstablishConnection() error {
j.RoomsConnected = make([]string, 0)
log.Debugf("Establishing connection to %s", j.Options.Host)
j.Talk, err = j.Options.NewClient()
if err != nil {
@@ -234,7 +236,7 @@ func (j *Jabber) EstablishConnection() error {
log.Info("Connected")
// Джойнимся к чятикам, но делаем это в фоне, чтобы не блочиться на ошибках, например, если бота забанили
// Джойнимся к чятикам, но делаем это в фоне, чтобы не блочиться на ошибках, например, если бота забанили.
for _, roomStruct := range j.C.Jabber.Channels {
room := roomStruct.Name
@@ -268,6 +270,7 @@ func (j *Jabber) ProbeServerLiveness() error { //nolint:gocognit
}
sleepTime := time.Duration(j.C.Jabber.ServerPingDelay) * 1000 * time.Millisecond
sleepTime += time.Duration(rand.Int63n(1000*j.C.Jabber.PingSplayDelay)) * time.Millisecond
time.Sleep(sleepTime)
@@ -276,26 +279,25 @@ func (j *Jabber) ProbeServerLiveness() error { //nolint:gocognit
}
// Пингуем, только если не было никакой активности в течение > config.Jabber.ServerPingDelay,
// в худшем случе это будет ~ (config.Jabber.PingSplayDelay * 2) + config.Jabber.PingSplayDelay
// if (time.Now().Unix() - lastServerActivity) < (config.Jabber.ServerPingDelay + config.Jabber.PingSplayDelay) {
// continue
// }
// в худшем случе это будет ~ (config.Jabber.PingSplayDelay * 2) + config.Jabber.PingSplayDelay.
if j.ServerCapsQueried { // Сервер ответил на disco#info
if j.ServerCapsQueried { // Сервер ответил на disco#info.
var (
value interface{}
value any
exist bool
)
value, exist = j.ServerCapsList.Get("urn:xmpp:ping")
switch {
// Сервер анонсировал, что умеет в c2s пинги
// Сервер анонсировал, что умеет в c2s пинги.
case exist && value.(bool):
// Таймаут c2s пинга. Возьмём сумму задержки между пингами, добавим таймаут коннекта и добавим
// максимальную корректировку разброса.
txTimeout := j.C.Jabber.ServerPingDelay + j.C.Jabber.ConnectionTimeout
txTimeout += j.C.Jabber.PingSplayDelay
rxTimeout := txTimeout
rxTimeAgo := time.Now().Unix() - j.ServerPingTimestampRx
@@ -311,7 +313,7 @@ func (j *Jabber) ProbeServerLiveness() error { //nolint:gocognit
return err
// По-умолчанию, мы отправляем c2s пинг
// По-умолчанию, мы отправляем c2s пинг.
default:
log.Debugf("Sending c2s ping from %s to %s", j.Talk.JID(), j.C.Jabber.Server)
@@ -321,7 +323,7 @@ func (j *Jabber) ProbeServerLiveness() error { //nolint:gocognit
j.ServerPingTimestampTx = time.Now().Unix()
}
} else { // Первая пуля пока не вылетела, отправляем
} else { // Первая пуля пока не вылетела, отправляем.
log.Debugf("Sending first c2s ping from %s to %s", j.Talk.JID(), j.C.Jabber.Server)
if err := j.Talk.PingC2S(j.Talk.JID(), j.C.Jabber.Server); err != nil {
@@ -331,7 +333,7 @@ func (j *Jabber) ProbeServerLiveness() error { //nolint:gocognit
j.ServerPingTimestampTx = time.Now().Unix()
}
// Сервер не анонсировал, что умеет в c2s пинги
// Сервер не анонсировал, что умеет в c2s пинги.
default:
log.Debug("Sending keepalive whitespace ping")
@@ -339,7 +341,7 @@ func (j *Jabber) ProbeServerLiveness() error { //nolint:gocognit
return err
}
}
} else { // Сервер не ответил на disco#info
} else { // Сервер не ответил на disco#info.
log.Debug("Sending keepalive whitespace ping")
if _, err := j.Talk.SendKeepAlive(); err != nil {
@@ -358,7 +360,7 @@ func (j *Jabber) ProbeMUCLiveness() { //nolint:gocognit
for _, room := range j.RoomsConnected {
var (
exist bool
lastActivityTS interface{}
lastActivityTS any
)
// Если записи про комнату нету, то пинговать её бессмысленно.
@@ -393,7 +395,7 @@ func (j *Jabber) ProbeMUCLiveness() { //nolint:gocognit
}
)
*/
var roomMap interface{}
var roomMap any
roomMap, exist = j.MucCapsList.Get(room)