Migrate to go project layout, replace logrus with slog, fix some errors
This commit is contained in:
70
internal/anycollection/anycollection.go
Normal file
70
internal/anycollection/anycollection.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package anycollection
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Collection это sync.Map-ка, хранящая в себе произвольные итемы.
|
||||
type Collection struct {
|
||||
items sync.Map
|
||||
close chan struct{}
|
||||
}
|
||||
|
||||
// Item представляет собой интерфейс для хранения произвольных данных.
|
||||
type item struct {
|
||||
data any
|
||||
}
|
||||
|
||||
// NewCollection создаёт инстанс коллекции.
|
||||
func NewCollection() *Collection {
|
||||
c := &Collection{
|
||||
items: sync.Map{},
|
||||
close: make(chan struct{}),
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
// Get достаёт данные по заданному ключу из коллекции.
|
||||
func (collection *Collection) Get(key any) (any, bool) {
|
||||
obj, exists := collection.items.Load(key)
|
||||
|
||||
if !exists {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
item := obj.(item)
|
||||
|
||||
return item.data, true
|
||||
}
|
||||
|
||||
// Set сохраняет данные с заданным ключом в коллекцию.
|
||||
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 any) bool) {
|
||||
fn := func(key, value any) bool {
|
||||
item := value.(item)
|
||||
|
||||
return f(key, item.data)
|
||||
}
|
||||
|
||||
collection.items.Range(fn)
|
||||
}
|
||||
|
||||
// Delete удаляет ключ и значение из коллекции данных.
|
||||
func (collection *Collection) Delete(key any) {
|
||||
collection.items.Delete(key)
|
||||
}
|
||||
|
||||
// Close очищает и высвобождает ресурсы, занятые коллекцией.
|
||||
func (collection *Collection) Close() {
|
||||
collection.close <- struct{}{}
|
||||
collection.items = sync.Map{}
|
||||
}
|
||||
|
||||
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
||||
377
internal/jabber/commands.go
Normal file
377
internal/jabber/commands.go
Normal file
@@ -0,0 +1,377 @@
|
||||
package jabber
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"aleesa-jabber-go/internal/log"
|
||||
|
||||
"github.com/eleksir/go-xmpp"
|
||||
)
|
||||
|
||||
func cmd(v xmpp.Chat) error {
|
||||
var (
|
||||
err error
|
||||
message sMsg
|
||||
data []byte
|
||||
)
|
||||
|
||||
message.From = Config.Redis.MyChannel
|
||||
message.Threadid = "" // Тредиков в jabber-е нету, поэтому это поле отправляем пустым
|
||||
message.Message = v.Text
|
||||
message.Plugin = Config.Redis.MyChannel
|
||||
|
||||
// Список команд, публично доступных, без каких-то специальных настроек
|
||||
cmds := []string{"ping", "пинг", "пинх", "pong", "понг", "понх", "coin", "монетка", "roll", "dice", "кости",
|
||||
"ver", "version", "версия", "хэлп", "halp", "kde", "кде", "lat", "лат", "friday", "пятница", "proverb",
|
||||
"пословица", "пословиться", "fortune", "фортунка", "f", "ф", "anek", "анек", "анекдот", "buni", "cat",
|
||||
"кис", "drink", "праздник", "fox", "лис", "frog", "лягушка", "horse", "лошадь", "лошадка", "monkeyuser",
|
||||
"owl", "сова", "сыч", "rabbit", "bunny", "кролик", "snail", "улитка", "xkcd", "dig", "копать", "fish",
|
||||
"fishing", "рыба", "рыбка", "рыбалка", "karma", "карма", "w", "п", "weather", "погода", "погодка", "погадка"}
|
||||
|
||||
// Список отключаемых команд проверяется в case-е про groupchat, чуть ниже
|
||||
|
||||
// Список "сложных" команд, это те, которые имеют ключевые данные на конце команды. Пока что это только погодка.
|
||||
cmdsComplex := []string{"w ", "п ", "погода ", "погодка ", "погадка ", "weather "}
|
||||
|
||||
// Список команд бармэна. Основное отличие в том, что после команды ключевые данные могут быть, а могут и не быть.
|
||||
// То есть рюмашку можно заказать себе или кому-то в чятике.
|
||||
cmdsBartender := []string{"rum", "ром", "vodka", "водка", "beer", "пиво", "tequila", "текила", "whisky", "виски",
|
||||
"absinthe", "абсент"}
|
||||
|
||||
// Есть ещё команда на изменение кармы. Это команда-суффикс ++ или --. Она проверяется по-месту, потому что она одна
|
||||
// и вполне уникальна.
|
||||
|
||||
switch v.Type {
|
||||
case "groupchat":
|
||||
message.Mode = "public"
|
||||
message.Chatid = strings.SplitN(v.Remote, "/", 2)[0]
|
||||
userNick := strings.SplitN(v.Remote, "/", 2)[1]
|
||||
|
||||
if realJid := getRealJIDfromNick(v.Remote); realJid != "" {
|
||||
message.Userid = realJid
|
||||
} else {
|
||||
message.Userid = v.Remote
|
||||
}
|
||||
|
||||
message.Misc.Answer = 0
|
||||
message.Misc.Fwdcnt = 0
|
||||
message.Misc.Csign = Config.CSign
|
||||
message.Misc.Username = userNick
|
||||
message.Misc.Botnick = getBotNickFromRoomConfig(message.Chatid)
|
||||
message.Misc.Msgformat = 0
|
||||
|
||||
msgLen := len(v.Text)
|
||||
|
||||
// TODO: а вот тут надо впилить парсер команд
|
||||
if (msgLen > len(Config.CSign)) && (v.Text[:len(Config.CSign)] == Config.CSign) {
|
||||
var (
|
||||
cmd = v.Text[len(Config.CSign):]
|
||||
room = message.Chatid
|
||||
answer string
|
||||
)
|
||||
|
||||
// И таких кейсов у нас минимум на команды !help и семейство команд !admin, а также на те команды, которые не всегда работают
|
||||
switch {
|
||||
case cmd == "help" || cmd == "помощь":
|
||||
answer += fmt.Sprintf("%shelp | %sпомощь - это сообщение\n", Config.CSign, Config.CSign)
|
||||
answer += fmt.Sprintf("%sanek | %sанек | %sанекдот - рандомный анекдот с anekdot.ru\n", Config.CSign, Config.CSign, Config.CSign)
|
||||
answer += fmt.Sprintf("%sbuni - комикс-стрип hapi buni\n", Config.CSign)
|
||||
answer += fmt.Sprintf("%sbunny - кролик\n", Config.CSign)
|
||||
answer += fmt.Sprintf("%srabbit | %sкролик - кролик\n", Config.CSign, Config.CSign)
|
||||
answer += fmt.Sprintf("%scat | %sкис - кошечка\n", Config.CSign, Config.CSign)
|
||||
answer += fmt.Sprintf("%sdice | %sroll | %sкости - бросить кости\n", Config.CSign, Config.CSign, Config.CSign)
|
||||
answer += fmt.Sprintf("%sdig | %sкопать - заняться археологией\n", Config.CSign, Config.CSign)
|
||||
answer += fmt.Sprintf("%sdrink | %sпраздник - какой сегодня праздник?\n", Config.CSign, Config.CSign)
|
||||
answer += fmt.Sprintf("%sfish | %sfisher - порыбачить\n", Config.CSign, Config.CSign)
|
||||
answer += fmt.Sprintf("%sрыба | %sрыбка | %sрыбалка - порыбачить\n", Config.CSign, Config.CSign, Config.CSign)
|
||||
answer += fmt.Sprintf("%sf | %sф - рандомная фраза из сборника цитат fortune_mod\n", Config.CSign, Config.CSign)
|
||||
answer += fmt.Sprintf("%sfortune | %sфортунка - рандомная фраза из сборника цитат fortune_mod\n", Config.CSign, Config.CSign)
|
||||
answer += fmt.Sprintf("%sfox | %sлис - лисичка\n", Config.CSign, Config.CSign)
|
||||
answer += fmt.Sprintf("%sfriday | %sпятница - а не пятница ли сегодня?\n", Config.CSign, Config.CSign)
|
||||
answer += fmt.Sprintf("%sfrog | %sлягушка - лягушка\n", Config.CSign, Config.CSign)
|
||||
answer += fmt.Sprintf("%shorse | %sлошадь | %sлошадка - лошадка\n", Config.CSign, Config.CSign, Config.CSign)
|
||||
answer += fmt.Sprintf("%skarma фраза - посмотреть карму фразы\n", Config.CSign)
|
||||
answer += fmt.Sprintf("%sкарма фраза - посмотреть карму фразы\n", Config.CSign)
|
||||
answer += fmt.Sprintln("фраза++ | фраза-- - повысить или понизить карму фразы")
|
||||
answer += fmt.Sprintf("%slat | %sлат - сгенерировать фразу из крылатого латинского выражения\n", Config.CSign, Config.CSign)
|
||||
answer += fmt.Sprintf("%smonkeyuser - комикс-стрип MonkeyUser\n", Config.CSign)
|
||||
answer += fmt.Sprintf("%sowl | %sсова - сова\n", Config.CSign, Config.CSign)
|
||||
answer += fmt.Sprintf("%sping | %sпинг - попинговать бота\n", Config.CSign, Config.CSign)
|
||||
answer += fmt.Sprintf("%sproverb | %sпословица - рандомная русская пословица\n", Config.CSign, Config.CSign)
|
||||
answer += fmt.Sprintf("%ssnail | %sулитка - улитка\n", Config.CSign, Config.CSign)
|
||||
answer += fmt.Sprintf("%ssome_brew - выдать соответствующий напиток, бармен может налить rum, ром, vodka, водку, tequila, текила\n", Config.CSign)
|
||||
answer += fmt.Sprintf("%sver | %sversion - написать что-то про версию ПО\n", Config.CSign, Config.CSign)
|
||||
answer += fmt.Sprintf("%sверсия - написать что-то про версию ПО\n", Config.CSign)
|
||||
answer += fmt.Sprintf("%sw <город> | %sп <город> - погода в городе\n", Config.CSign, Config.CSign)
|
||||
answer += fmt.Sprintf("%sxkcd - комикс-стрип с xkcb.ru\n", Config.CSign)
|
||||
|
||||
// Для овнера или админа надо показывать команду admin
|
||||
if isMucAdmin(v.Remote) {
|
||||
answer += fmt.Sprintf("%sadmin - настройки некоторых плагинов бота для комнаты\n", Config.CSign)
|
||||
}
|
||||
|
||||
_, err = Talk.Send(xmpp.Chat{Remote: room, Type: v.Type, Text: answer}) //nolint:exhaustruct
|
||||
|
||||
return err
|
||||
|
||||
case cmd == "admin":
|
||||
if isMucAdmin(v.Remote) {
|
||||
answer += fmt.Sprintf("%sadmin oboobs # - где 1 - вкл, 0 - выкл плагина oboobs\n", Config.CSign)
|
||||
answer += fmt.Sprintf("%sadmin oboobs - показываем ли сисечки по просьбе участников чата (команды %stits, %stities, %sboobs, %sboobies, %sсиси, %sсисечки)\n", Config.CSign, Config.CSign, Config.CSign, Config.CSign, Config.CSign, Config.CSign, Config.CSign)
|
||||
answer += fmt.Sprintf("%sadmin obutts # - где 1 - вкл, 0 - выкл плагина obutts\n", Config.CSign)
|
||||
answer += fmt.Sprintf("%sadmin obutts - показываем ли попки по просьбе участников чата (команды %sass, %sbutt, %sbooty, %sпопа, %sпопка)\n", Config.CSign, Config.CSign, Config.CSign, Config.CSign, Config.CSign, Config.CSign)
|
||||
|
||||
_, err = Talk.Send(xmpp.Chat{Remote: room, Type: v.Type, Text: answer}) //nolint:exhaustruct
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
return err
|
||||
|
||||
case cmd == "admin oboobs":
|
||||
if isMucAdmin(v.Remote) {
|
||||
switch getSetting(room, "oboobs") {
|
||||
case "0":
|
||||
answer = "Плагин oboobs выключен"
|
||||
case "1":
|
||||
answer = "Плагин oboobs включен"
|
||||
case "":
|
||||
_ = saveSetting(room, "oboobs", "0")
|
||||
answer = "Плагин oboobs выключен"
|
||||
default:
|
||||
answer = "Плагин oboobs выключен"
|
||||
}
|
||||
|
||||
_, err = Talk.Send(xmpp.Chat{Remote: room, Type: v.Type, Text: answer}) //nolint:exhaustruct
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
case cmd == "admin oboobs 1":
|
||||
if isMucAdmin(v.Remote) {
|
||||
err := saveSetting(room, "oboobs", "1")
|
||||
|
||||
if err != nil {
|
||||
answer = "Плагин oboobs всё ещё выключен"
|
||||
} else {
|
||||
answer = "Плагин oboobs включен"
|
||||
}
|
||||
|
||||
_, err = Talk.Send(xmpp.Chat{Remote: room, Type: v.Type, Text: answer}) //nolint:exhaustruct
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
case cmd == "admin oboobs 0":
|
||||
if isMucAdmin(v.Remote) {
|
||||
_ = saveSetting(room, "oboobs", "0")
|
||||
answer = "Плагин oboobs выключен"
|
||||
|
||||
_, err = Talk.Send(xmpp.Chat{Remote: room, Type: v.Type, Text: answer}) //nolint:exhaustruct
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
case cmd == "admin obutts":
|
||||
if isMucAdmin(v.Remote) {
|
||||
switch getSetting(room, "obutts") {
|
||||
case "0":
|
||||
answer = "Плагин obutts выключен"
|
||||
case "1":
|
||||
answer = "Плагин obutts включен"
|
||||
case "":
|
||||
_ = saveSetting(room, "obutts", "0")
|
||||
answer = "Плагин obutts выключен"
|
||||
default:
|
||||
answer = "Плагин obutts выключен"
|
||||
}
|
||||
|
||||
_, err = Talk.Send(xmpp.Chat{Remote: room, Type: v.Type, Text: answer}) //nolint:exhaustruct
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
case cmd == "admin obutts 1":
|
||||
if isMucAdmin(v.Remote) {
|
||||
err := saveSetting(room, "obutts", "1")
|
||||
|
||||
if err != nil {
|
||||
answer = "Плагин obutts всё ещё выключен"
|
||||
} else {
|
||||
answer = "Плагин obutts включен"
|
||||
}
|
||||
|
||||
_, err = Talk.Send(xmpp.Chat{Remote: room, Type: v.Type, Text: answer}) //nolint:exhaustruct
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
case cmd == "admin obutts 0":
|
||||
if isMucAdmin(v.Remote) {
|
||||
_ = saveSetting(room, "obutts", "0")
|
||||
answer = "Плагин obutts выключен"
|
||||
|
||||
_, err = Talk.Send(xmpp.Chat{Remote: room, Type: v.Type, Text: answer}) //nolint:exhaustruct
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
case cmd == "butt" || cmd == "booty" || cmd == "ass" || cmd == "попа" || cmd == "попка":
|
||||
if getSetting(room, "obutts") != "1" {
|
||||
return err
|
||||
}
|
||||
|
||||
message.Misc.Answer = 1
|
||||
|
||||
case cmd == "tits" || cmd == "boobs" || cmd == "tities" || cmd == "boobies" || cmd == "сиси" || cmd == "сисечки":
|
||||
if getSetting(room, "oboobs") != "1" {
|
||||
return err
|
||||
}
|
||||
|
||||
message.Misc.Answer = 1
|
||||
|
||||
// Отлавливаем команды и им проставляем message.Misc.Answer = 1
|
||||
default:
|
||||
done := false
|
||||
|
||||
// Простые команды
|
||||
for _, command := range cmds {
|
||||
if cmd == command {
|
||||
done = true
|
||||
message.Misc.Answer = 1
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Сложные команды, например, погода
|
||||
if !done {
|
||||
for _, command := range cmdsComplex {
|
||||
cmdLen := len(cmd)
|
||||
|
||||
if cmdLen > len(command) && cmd[0:len(command)] == command {
|
||||
done = true
|
||||
message.Misc.Answer = 1
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Сам себе заказал выпивку
|
||||
if !done {
|
||||
for _, command := range cmdsBartender {
|
||||
if cmd == command {
|
||||
done = true
|
||||
message.Misc.Answer = 1
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Заказал выпивку кому-то ещё
|
||||
if !done {
|
||||
re := regexp.MustCompile(" +")
|
||||
pile := re.Split(cmd, 2)
|
||||
names := getMucNames(room)
|
||||
|
||||
for _, command := range cmdsBartender {
|
||||
if pile[0] == command {
|
||||
userNick := strings.TrimSpace(pile[1])
|
||||
|
||||
if userNick != "" {
|
||||
if slices.Contains(names, userNick) {
|
||||
message.Misc.Username = userNick
|
||||
} else {
|
||||
answer := fmt.Sprintf("Я тут не вижу участника с ником %s", userNick)
|
||||
|
||||
_, err = Talk.Send(xmpp.Chat{Remote: room, Type: v.Type, Text: answer}) //nolint:exhaustruct
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
message.Misc.Answer = 1
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ну, вот мы и добрались досюда, и... нам осталось понять, не обращаются ли к боту?
|
||||
// Дело в том, что ответа требуют только команды и обращения к боту по нику
|
||||
// или jid-у, в данном случае, по нику. А всё остальное должно попадать в craniac-а и оставаться там в качестве
|
||||
// материала для бредогенератора.
|
||||
|
||||
// Попробуем выискать изменение кармы
|
||||
|
||||
// ++ или -- на конце фразы - это наш кандидат
|
||||
if msgLen > len("++") {
|
||||
if v.Text[msgLen-len("--"):msgLen] == "--" || v.Text[msgLen-len("++"):msgLen] == "++" {
|
||||
// Предполагается, что менять карму мы будем для одной фразы, то есть для 1 строки
|
||||
if len(strings.Split(v.Text, "\n")) == 1 {
|
||||
// Костыль для кармы
|
||||
message.Misc.Answer = 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Предполагается что в канале бот должен отвечать, только если к нему обратились, либо это была команда
|
||||
botNick := getBotNickFromRoomConfig(message.Chatid)
|
||||
|
||||
if regexp.MustCompile(regexp.QuoteMeta(botNick)).Match([]byte(message.Message)) {
|
||||
message.Misc.Answer = 1
|
||||
}
|
||||
|
||||
message.Message = v.Text
|
||||
data, err = json.Marshal(message)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to to serialize message for redis: %w", err)
|
||||
}
|
||||
|
||||
case "chat":
|
||||
message.Mode = "private"
|
||||
message.Chatid = v.Remote
|
||||
|
||||
// Jid у нас уникален, а вот nick может, во-первых, разниться от конфы к конфе, а во-вторых, его может взять
|
||||
// себе другой пользователь конфы, пока оригинальный владелец ника отсутствует. Но мы предполагаем, что если
|
||||
// комната анонимная, то владелец комнаты, приглашая бота, согласен на некоторые компромиссы.
|
||||
if realJID := getRealJIDfromNick(v.Remote); realJID != "" {
|
||||
message.Userid = realJID
|
||||
} else {
|
||||
message.Userid = v.Remote
|
||||
}
|
||||
|
||||
message.Misc.Answer = 1
|
||||
message.Misc.Fwdcnt = 0
|
||||
message.Misc.Csign = Config.CSign
|
||||
message.Misc.Username = message.Userid
|
||||
message.Misc.Botnick = Config.Jabber.Nick
|
||||
message.Misc.Msgformat = 0
|
||||
|
||||
data, err = json.Marshal(message)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to to serialize message for redis: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Заталкиваем наш json в редиску
|
||||
if err := RedisClient.Publish(Ctx, Config.Redis.Channel, data).Err(); err != nil {
|
||||
return fmt.Errorf("unable to send data to redis channel %s: %w", Config.Redis.Channel, err)
|
||||
}
|
||||
|
||||
log.Debugf("Sent msg to redis channel %s: %s", Config.Redis.Channel, string(data))
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
||||
645
internal/jabber/event_parser.go
Normal file
645
internal/jabber/event_parser.go
Normal file
@@ -0,0 +1,645 @@
|
||||
package jabber
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"aleesa-jabber-go/internal/log"
|
||||
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"github.com/eleksir/go-xmpp"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
// ParseEvent парсит ивенты, прилетающие из модуля xmpp (изменения presence, фразы участников, ошибки).
|
||||
func ParseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo
|
||||
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
|
||||
if v.Text != "" {
|
||||
// Чятики бывают групповые и не групповые, от этого зависит Remote, куда направлять сообщение
|
||||
switch v.Type { //nolint:wsl,whitespace
|
||||
|
||||
// Групповой чятик
|
||||
case "groupchat":
|
||||
log.Debugf("Message from public chat: %s", v.Text)
|
||||
room := strings.SplitN(v.Remote, "/", 2)[0]
|
||||
nick := strings.SplitN(v.Remote, "/", 2)[1]
|
||||
|
||||
if nick == getBotNickFromRoomConfig(room) {
|
||||
log.Debug("Skipping message from myself")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if err := cmd(v); err != nil {
|
||||
GTomb.Kill(err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
LastActivity = LastServerActivity
|
||||
|
||||
if muc, _ := strings.CutSuffix(v.Remote, "/"); muc != "" {
|
||||
LastMucActivity.Set(muc, LastServerActivity)
|
||||
}
|
||||
|
||||
// Приватный чятик.
|
||||
case "chat":
|
||||
// Здесь у нас может быть 2 вида From:
|
||||
// slackware-current@conference.jabber.ru/eleksir - это если сообщение из публичного чятика
|
||||
// eleksir@jabber.ru/array.lan - это если мы работаем через ростер
|
||||
log.Debugf("Private message: %s", v.Text)
|
||||
|
||||
if err := cmd(v); err != nil {
|
||||
GTomb.Kill(err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
LastActivity = LastServerActivity
|
||||
|
||||
// Внезапно, ошибки. По идее они должны ассоциироваться с отправляемыми сообщениями, но по ходу это
|
||||
// не реализовано, поэтому мы получаем ошибки в форме отдельного чятика.
|
||||
case "error":
|
||||
// Это ошибка со стороны сервера, видимо, в ответ на наши действия и поскольку такие вещи происходят
|
||||
// асинхронно, связать их с конкретными нашими действиями довольно сложно.
|
||||
log.Warn(spew.Sdump(e))
|
||||
}
|
||||
}
|
||||
|
||||
// IQ-сообщение - пинг, понг, что-то ещё...
|
||||
case xmpp.IQ:
|
||||
// По правилам IQ обязательно должна содержать ID, причём не пустой.
|
||||
if v.ID == "" {
|
||||
log.Info("Got an IQ stanza with empty id, discarding")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
switch v.Type {
|
||||
case xmpp.IQTypeGet:
|
||||
log.Debug("Looks like IQ get query")
|
||||
|
||||
if muc, _ := strings.CutSuffix(v.To, "/"); muc != "" {
|
||||
if slices.Contains(RoomsConnected, muc) {
|
||||
LastMucActivity.Set(muc, LastServerActivity)
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
iqStruct jabberSimpleIqGetQuery
|
||||
parseError bool
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
if id, err := Talk.IqVersionResponse(
|
||||
v,
|
||||
"aleesa-jabber-go",
|
||||
"4.с_чем-то",
|
||||
"линупс",
|
||||
); err != nil {
|
||||
err := fmt.Errorf(
|
||||
"unable to send version info to jabber server: id=%s, err=%w",
|
||||
id,
|
||||
err,
|
||||
)
|
||||
|
||||
GTomb.Kill(err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
parseError = false
|
||||
|
||||
// Нам прислали попингуй
|
||||
case "urn:xmpp:ping":
|
||||
log.Infof("Got IQ get request for pong from %s", v.From)
|
||||
|
||||
if id, err := Talk.PingResponse(v); err != nil {
|
||||
err := fmt.Errorf(
|
||||
"unable to send pong to jabber server: id=%s, err=%w",
|
||||
id,
|
||||
err,
|
||||
)
|
||||
|
||||
GTomb.Kill(err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
parseError = false
|
||||
|
||||
// У нас запросили время последней активности. В нашем случае это либо время запуска клиента, либо время
|
||||
// последней фразы в чяти. Xep-0012.
|
||||
case "jabber:iq:last":
|
||||
log.Infof("Got IQ get last activity time request from %s", v.From)
|
||||
|
||||
if id, err := Talk.JabberIqLastResponse(v, LastActivity); err != nil {
|
||||
err := fmt.Errorf(
|
||||
"unable to send last activity time to jabber server: id=%s, err=%w",
|
||||
id,
|
||||
err,
|
||||
)
|
||||
|
||||
GTomb.Kill(err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
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\" />"
|
||||
answer += "<feature var=\"jabber:iq:last\" />"
|
||||
answer += "<feature var=\"http://jabber.org/protocol/disco#info\" />"
|
||||
answer += "</query>"
|
||||
|
||||
if id, err := Talk.RawInformation(
|
||||
v.To,
|
||||
v.From,
|
||||
v.ID,
|
||||
xmpp.IQTypeResult,
|
||||
answer,
|
||||
); err != nil {
|
||||
err := fmt.Errorf(
|
||||
"unable to send disco#info to jabber server: id=%s, err=%w",
|
||||
id,
|
||||
err,
|
||||
)
|
||||
|
||||
GTomb.Kill(err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
parseError = false
|
||||
|
||||
// Спрашивают список команд, чтобы поуправлять этим клиентом (xep-0030)
|
||||
case xmpp.XMPPNS_DISCO_ITEMS:
|
||||
log.Infof("Got IQ get disco#items request from %s, answer service unavailable", v.From)
|
||||
|
||||
if _, err := Talk.ErrorServiceUnavailable(
|
||||
v,
|
||||
"http://jabber.org/protocol/disco#info",
|
||||
"http://jabber.org/protocol/commands",
|
||||
); err != nil {
|
||||
err := fmt.Errorf(
|
||||
"unable to send disco#items to jabber server: id=%s, err=%w",
|
||||
v.ID,
|
||||
err,
|
||||
)
|
||||
|
||||
GTomb.Kill(err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
parseError = false
|
||||
|
||||
// Запрашивают что-то, о чём мы не имеем представления
|
||||
default:
|
||||
log.Info("Got an unknown IQ get request, discarding")
|
||||
log.Info(spew.Sdump(e))
|
||||
|
||||
return
|
||||
}
|
||||
} else {
|
||||
parseError = true
|
||||
}
|
||||
|
||||
// Попробуем распарсить входящий запрос как urn:xmpp:time
|
||||
if parseError {
|
||||
var iqStruct jabberTimeIqGetQuery
|
||||
|
||||
if err := xml.Unmarshal(v.Query, &iqStruct); err == nil {
|
||||
switch iqStruct.Xmlns {
|
||||
case "urn:xmpp:time":
|
||||
log.Infof("Got IQ get time request from %s", v.From)
|
||||
|
||||
if id, err := Talk.UrnXMPPTimeResponse(v, "+00:00"); err != nil {
|
||||
err := fmt.Errorf(
|
||||
"unable to send urn:xmpp:time to jabber server: id=%s, err=%w",
|
||||
id,
|
||||
err,
|
||||
)
|
||||
|
||||
GTomb.Kill(err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
parseError = false
|
||||
|
||||
default:
|
||||
log.Info("Got bogus IQ get time request, discarding")
|
||||
log.Info(spew.Sdump(e))
|
||||
|
||||
return
|
||||
}
|
||||
} else {
|
||||
parseError = true
|
||||
}
|
||||
}
|
||||
|
||||
// Попробуем распарсить входящий запрос как http://jabber.org/protocol/pubsub
|
||||
if parseError {
|
||||
var iqStruct jabberPubsubIQGetQuery
|
||||
|
||||
if err := xml.Unmarshal(v.Query, &iqStruct); err == nil {
|
||||
switch iqStruct.Xmlns {
|
||||
case xmpp.XMPPNS_PUBSUB:
|
||||
log.Infof("Got IQ get pubsub request from %s, answer feature unimplemented", v.From)
|
||||
|
||||
if id, err := Talk.ErrorNotImplemented(
|
||||
v,
|
||||
"http://jabber.org/protocol/pubsub#errors",
|
||||
"subscribe",
|
||||
); err != nil {
|
||||
err := fmt.Errorf("unable to send pubsub feature unimplemented to jabber server: id=%s, err=%w",
|
||||
id,
|
||||
err,
|
||||
)
|
||||
|
||||
GTomb.Kill(err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
parseError = false
|
||||
|
||||
default:
|
||||
log.Info("Got unknown IQ get pubsub something request, discarding")
|
||||
log.Info(spew.Sdump(e))
|
||||
parseError = true //nolint:wsl
|
||||
}
|
||||
} else {
|
||||
parseError = true
|
||||
}
|
||||
}
|
||||
|
||||
// Попробуем распарсить входящий запрос как urn:xmpp:ping он приходит при MUC Self-Ping (Schrödinger's Chat)
|
||||
// Предполагается, что такие респонсы должны приходить только для пинга без серверной оптимизации.
|
||||
// Хотя идейно мы не поддерживаем работу без серверной оптимизации, но на пинг ответим, нам несложно.
|
||||
if parseError {
|
||||
var iqStruct jabberIqPing
|
||||
|
||||
if err := xml.Unmarshal(v.Query, &iqStruct); err == nil {
|
||||
log.Debugf("Got IQ get request (actually, response) for MUC Self-Ping from %s", v.From)
|
||||
|
||||
if id, err := Talk.PingResponse(v); err != nil {
|
||||
err := fmt.Errorf(
|
||||
"unable to send pong to jabber server: id=%s, err=%w",
|
||||
id,
|
||||
err,
|
||||
)
|
||||
|
||||
GTomb.Kill(err)
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
parseError = false
|
||||
}
|
||||
|
||||
// Не знаю, как парсить... залоггируем это дело и пойдём дальше
|
||||
if parseError {
|
||||
log.Infof("Does not look like parsable via iqStruct")
|
||||
log.Info(spew.Sdump(e))
|
||||
}
|
||||
|
||||
case xmpp.IQTypeResult:
|
||||
if muc, _ := strings.CutSuffix(v.To, "/"); muc != "" {
|
||||
if slices.Contains(RoomsConnected, muc) {
|
||||
LastMucActivity.Set(muc, LastServerActivity)
|
||||
}
|
||||
}
|
||||
|
||||
switch {
|
||||
// Похоже на pong от сервера (по стандарту в ответе нету query, но go-xmpp нам подсовывает это)
|
||||
case v.From == Config.Jabber.Server && v.To == Talk.JID() && string(v.Query) == "<XMLElement></XMLElement>":
|
||||
log.Debugf("Got S2C pong answer from %s to %s", v.From, v.To)
|
||||
ServerPingTimestampRx = time.Now().Unix() //nolint:wsl
|
||||
|
||||
// Похоже на понг второй стадии xep-0410 MUC-Ping-а, который у нас не реализован
|
||||
case v.To == Talk.JID() && string(v.Query) == "<XMLElement></XMLElement>":
|
||||
mucNameMatch := slices.Contains(RoomsConnected, v.From)
|
||||
|
||||
if mucNameMatch {
|
||||
log.Debugf("Got server-optimized MUC pong answer (xep-0410) from %s to %s", v.From, v.To)
|
||||
} else {
|
||||
mucNickMatch := false
|
||||
|
||||
for _, room := range RoomsConnected {
|
||||
mucNick := fmt.Sprintf("%s/%s", room, getBotNickFromRoomConfig(room))
|
||||
|
||||
if v.From == mucNick {
|
||||
mucNickMatch = true
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Поскольку никакой логики у нас на этот счёт не предусмотрено, то просто пропускаем ответ.
|
||||
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))
|
||||
}
|
||||
}
|
||||
default:
|
||||
log.Info("Got an IQ result. Dunno how deal with it, discarding")
|
||||
log.Debug(spew.Sdump(e))
|
||||
}
|
||||
|
||||
// Этот бот не управляется со стороны сервера, поэтому все попытки порулить игнорируем.
|
||||
case xmpp.IQTypeSet:
|
||||
if muc, _ := strings.CutSuffix(v.To, "/"); muc != "" {
|
||||
if slices.Contains(RoomsConnected, muc) {
|
||||
LastMucActivity.Set(muc, LastServerActivity)
|
||||
}
|
||||
}
|
||||
|
||||
log.Info("Got an IQ request for set something. Answer not implemented")
|
||||
|
||||
if id, err := Talk.ErrorNotImplemented(
|
||||
v,
|
||||
"http://jabber.org/protocol/commands",
|
||||
xmpp.IQTypeSet,
|
||||
); err != nil {
|
||||
err := fmt.Errorf(
|
||||
"unable to send set feature unimplemented to jabber server: id=%s, err=%w",
|
||||
id,
|
||||
err,
|
||||
)
|
||||
|
||||
GTomb.Kill(err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
log.Debug(spew.Sdump(e))
|
||||
|
||||
// Нам прилетело сообщение об ошибке.
|
||||
case xmpp.IQTypeError:
|
||||
// Если сервер не хочет пинговаться и отвечает ошибкой на пинг, то наверно он не умеет в пинги,
|
||||
// хотя если мы его пингуем, значит он анонсировал такой capability. Вот, засранец!
|
||||
var (
|
||||
iqPingStruct jabberIqPing
|
||||
parseError = true
|
||||
)
|
||||
|
||||
if err := xml.Unmarshal(v.Query, &iqPingStruct); err == nil {
|
||||
if iqPingStruct.Xmlns == "urn:xmpp:ping" {
|
||||
if v.From == Config.Jabber.Server {
|
||||
msg := "Server announced that it can answer c2s ping, but gives us an error to such query, "
|
||||
msg += "fallback to keepalive whitespace pings"
|
||||
log.Error(msg)
|
||||
|
||||
ServerCapsList.Set("urn:xmpp:ping", false)
|
||||
} else {
|
||||
log.Errorf("Got 'ping unsupported' message from: %s to: %s", v.From, v.To)
|
||||
}
|
||||
} else {
|
||||
log.Error("Iq parsed as ping, but does not belong to xmlns urn:xmpp:ping")
|
||||
log.Error(spew.Sdump(e))
|
||||
}
|
||||
|
||||
parseError = false
|
||||
}
|
||||
|
||||
// Это у нас пинг xep-0410 и мы не в комнате, предполагается, что надо бы заджойниться.
|
||||
if parseError {
|
||||
var iqErrorCancelNotAcceptable jabberIqErrorCancelNotAcceptable
|
||||
|
||||
if err := xml.Unmarshal(v.Query, &iqErrorCancelNotAcceptable); err == nil {
|
||||
if v.To == Talk.JID() {
|
||||
var (
|
||||
room = strings.SplitN(v.From, "/", 2)[0]
|
||||
nick = strings.SplitN(v.From, "/", 2)[1]
|
||||
)
|
||||
|
||||
if slices.Contains(RoomsConnected, iqErrorCancelNotAcceptable.By) && nick == getBotNickFromRoomConfig(room) {
|
||||
log.Errorf(
|
||||
"Got Iq error message from: %s to: %s. Looks like i'm not in MUC anymore",
|
||||
v.From, v.To,
|
||||
)
|
||||
|
||||
time.Sleep(time.Duration(Config.Jabber.MucRejoinDelay) * time.Second)
|
||||
|
||||
if _, err := Talk.JoinMUCNoHistory(iqErrorCancelNotAcceptable.By, nick); err != nil {
|
||||
err := fmt.Errorf(
|
||||
"looks like connection to server also lost err=%w",
|
||||
err,
|
||||
)
|
||||
|
||||
GTomb.Kill(err)
|
||||
|
||||
return
|
||||
}
|
||||
} else {
|
||||
log.Error(
|
||||
"looks like message parsed as jabberIqErrorCancelNotAcceptable but we're not in given room",
|
||||
)
|
||||
|
||||
log.Error(spew.Sdump(e))
|
||||
}
|
||||
} else {
|
||||
log.Error(
|
||||
"Looks like message parsed as jabberIqErrorCancelNotAcceptable but addressed not to us",
|
||||
)
|
||||
|
||||
log.Error(spew.Sdump(e))
|
||||
}
|
||||
|
||||
parseError = false
|
||||
}
|
||||
}
|
||||
|
||||
if parseError {
|
||||
log.Error("Unhandled IQ Error message")
|
||||
log.Error(spew.Sdump(e))
|
||||
}
|
||||
// Нам прилетело что-то неизвестное из семейства 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(RoomsConnected, muc) {
|
||||
LastMucActivity.Set(muc, LastServerActivity)
|
||||
}
|
||||
}
|
||||
|
||||
if v.Type == xmpp.IQTypeError {
|
||||
// Это событие происходит, когда из чятика выходит другой инстанс клиента.
|
||||
// Такое бывает, когда 2 инстанса с одинаковым ресурсом, например, начинают "драться" за возможность
|
||||
// остаться на сервере. Ситуация, которую допускать нельзя, на самом деле, потому что рано или поздно такого
|
||||
// клиента забанят из-за спама "вошёл-вышел".
|
||||
// Однако, это не значит, что такую ситуацию мы не должны корректным образом обрабатывать.
|
||||
if v.Type == "unavailable" {
|
||||
// Считаем, что мы больше не в комнате, поэтому не знаем, кто там есть.
|
||||
RoomPresences.Delete(v.From)
|
||||
|
||||
log.Error("Presence notification - looks like another instance of client leaves room")
|
||||
|
||||
if slices.Contains(RoomsConnected, v.From) {
|
||||
go joinMuc(v.From) //nolint: errcheck
|
||||
}
|
||||
} else {
|
||||
log.Errorf("Presence notification, Type: %s, From: %s, Show: %s, Status: %s",
|
||||
v.Type, v.From, v.Show, v.Status)
|
||||
log.Error(spew.Sdump(v))
|
||||
}
|
||||
} else {
|
||||
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,
|
||||
)
|
||||
|
||||
room := strings.SplitN(v.From, "/", 2)[0]
|
||||
nick := strings.SplitN(v.From, "/", 2)[1]
|
||||
|
||||
if nick == "" {
|
||||
log.Infof("Presence stanza contains incorrect from attribute: %s", v.From)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Это наш собственный Presence
|
||||
if v.Show == "" && v.Status == "" {
|
||||
if nick == Config.Jabber.Nick {
|
||||
RoomsConnected = append(RoomsConnected, room)
|
||||
// На всякий случай дедуплицируем список комнат, к которым мы заджойнились.
|
||||
sort.Strings(RoomsConnected)
|
||||
slices.Compact(RoomsConnected)
|
||||
}
|
||||
}
|
||||
|
||||
switch v.Role {
|
||||
// Участник ушёл
|
||||
case "none":
|
||||
if presenceJSONInterface, present := RoomPresences.Get(room); present {
|
||||
presenceJSONStrings := interfaceToStringSlice(presenceJSONInterface)
|
||||
|
||||
var newPresenceJSONStrings []string
|
||||
|
||||
for _, presenceJSONstring := range presenceJSONStrings {
|
||||
var p xmpp.Presence
|
||||
_ = json.Unmarshal([]byte(presenceJSONstring), &p)
|
||||
|
||||
if p.From == v.From {
|
||||
continue
|
||||
}
|
||||
|
||||
newPresenceJSONStrings = append(newPresenceJSONStrings, presenceJSONstring)
|
||||
}
|
||||
|
||||
RoomPresences.Set(room, newPresenceJSONStrings)
|
||||
}
|
||||
// Участник пришёл
|
||||
default:
|
||||
var (
|
||||
presenceJSONStrings []string
|
||||
newPresenceJSONStrings []string
|
||||
presenceJSONBytes []byte
|
||||
)
|
||||
|
||||
if presenceJSONInterface, present := RoomPresences.Get(room); present {
|
||||
presenceJSONStrings = interfaceToStringSlice(presenceJSONInterface)
|
||||
}
|
||||
|
||||
for _, presenceJSONString := range presenceJSONStrings {
|
||||
var p xmpp.Presence
|
||||
_ = json.Unmarshal([]byte(presenceJSONString), &p)
|
||||
|
||||
// Если находим, что у нас уже есть клиент с таким же From, то есть полным nick-ом (для grouchat)
|
||||
// просто замещаем его.
|
||||
if p.From == v.From {
|
||||
continue
|
||||
}
|
||||
|
||||
newPresenceJSONStrings = append(newPresenceJSONStrings, presenceJSONString)
|
||||
}
|
||||
|
||||
presenceJSONBytes, _ = json.Marshal(v) //nolint:errchkjson
|
||||
newPresenceJSONStrings = append(newPresenceJSONStrings, string(presenceJSONBytes))
|
||||
RoomPresences.Set(room, newPresenceJSONStrings)
|
||||
}
|
||||
}
|
||||
|
||||
// Ответ на запрос поддерживаемых фич, который "http://jabber.org/protocol/disco#info"
|
||||
case xmpp.DiscoResult:
|
||||
if muc, _ := strings.CutSuffix(v.To, "/"); muc != "" {
|
||||
if slices.Contains(RoomsConnected, muc) {
|
||||
LastMucActivity.Set(muc, LastServerActivity)
|
||||
}
|
||||
}
|
||||
|
||||
// Я видел 2 типа disco result и они отличались только []identities. Попробуем вытащить известный identity
|
||||
// TODO: проверять адресата v.To
|
||||
for _, ident := range v.Identities {
|
||||
switch ident.Category {
|
||||
case "server":
|
||||
// Конкретно сейчас нас интересует только поддержка c2s ping
|
||||
for _, feature := range v.Features {
|
||||
log.Debugf("Server %s announced that it supports feature: %s", v.From, feature)
|
||||
ServerCapsList.Set(feature, true)
|
||||
}
|
||||
|
||||
ServerCapsQueried = true
|
||||
|
||||
case "conference":
|
||||
mucCaps := make(map[string]bool)
|
||||
|
||||
for _, feature := range v.Features {
|
||||
log.Debugf("MUC %s announced that it supports feature: %s", v.From, feature)
|
||||
mucCaps[feature] = true //nolint:wsl
|
||||
}
|
||||
|
||||
MucCapsList.Set(v.From, mucCaps)
|
||||
|
||||
case "pubsub":
|
||||
log.Debugf("PubSub component %s reply to disco#info, skipping", v.From)
|
||||
|
||||
default:
|
||||
log.Debug("Got unknown reply to disco#info")
|
||||
log.Debug(spew.Sdump(e))
|
||||
}
|
||||
}
|
||||
|
||||
// Это что-то неизвестное, подампим событие в лог
|
||||
default:
|
||||
log.Info(spew.Sdump(e))
|
||||
}
|
||||
}
|
||||
|
||||
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
||||
90
internal/jabber/globals.go
Normal file
90
internal/jabber/globals.go
Normal file
@@ -0,0 +1,90 @@
|
||||
package jabber
|
||||
|
||||
import (
|
||||
"aleesa-jabber-go/internal/anycollection"
|
||||
"context"
|
||||
"os"
|
||||
|
||||
"github.com/cockroachdb/pebble"
|
||||
"github.com/eleksir/go-xmpp"
|
||||
"github.com/go-redis/redis/v8"
|
||||
"gopkg.in/tomb.v2"
|
||||
)
|
||||
|
||||
// Config - это у нас глобальная штука :) .
|
||||
var Config myConfig
|
||||
|
||||
// Статичные пути, по которым у нас лежат конфиги. При запуске сюда дописываются и динамические пути тоже.
|
||||
var ConfigLocations = []string{
|
||||
"~/.aleesa-jabber-go.json",
|
||||
"~/aleesa-jabber-go.json",
|
||||
"/etc/aleesa-jabber-go.json",
|
||||
}
|
||||
|
||||
// To break circular message forwarding we must set some sane default, it can be overridden via Config.
|
||||
var forwardMax int64 = 5
|
||||
|
||||
// Ставится в true, если мы получили сигнал на выключение.
|
||||
var Shutdown = false
|
||||
|
||||
// Чтобы не организовывать драку за установку коннекта.
|
||||
var connecting = false
|
||||
|
||||
// Глобальное состояние соединения.
|
||||
var IsConnected = false
|
||||
|
||||
// Канал, в который приходят уведомления для хэндлера сигналов от траппера сигналов.
|
||||
var SigChan chan os.Signal
|
||||
|
||||
// Основной инстанс xmpp-клиента.
|
||||
var Talk *xmpp.Client
|
||||
|
||||
// Опции подключения к xmpp-серверу.
|
||||
var Options *xmpp.Options
|
||||
|
||||
// Список комнат, в которых находится бот.
|
||||
var RoomsConnected []string
|
||||
|
||||
// Время последней активности, нужно для jabber:iq:last.
|
||||
var LastActivity int64
|
||||
|
||||
// Время последней активности, нужно для c2s пингов - посылаем пинги, только если давненько ничего не приходило с
|
||||
// сервера.
|
||||
var LastServerActivity int64
|
||||
|
||||
// Время последней активности MUC-ов, нужно для пингов - посылаем пинги, только если давненько ничего не приходило из
|
||||
// muc-ов.
|
||||
var LastMucActivity *anycollection.Collection
|
||||
|
||||
// Получен ли ответ на запрос disco#info к серверу.
|
||||
var ServerCapsQueried bool
|
||||
|
||||
// sync.Map-ка с капабилити сервера.
|
||||
var ServerCapsList *anycollection.Collection
|
||||
|
||||
// sync.Map-ка с комнатами и их capability.
|
||||
var MucCapsList *anycollection.Collection
|
||||
|
||||
// Время, когда был отправлен c2s ping.
|
||||
var ServerPingTimestampTx int64
|
||||
|
||||
// Время, когда был принят s2c pong.
|
||||
var ServerPingTimestampRx int64
|
||||
|
||||
// Объектик для хранения стейта утилизатора горутинок.
|
||||
var GTomb tomb.Tomb
|
||||
|
||||
// sync.Map-ка со списком участников конференций (в json-формате, согласно структуре xmpp.Presence, "room".[]json).
|
||||
var RoomPresences *anycollection.Collection
|
||||
|
||||
// Объектики клиента-редиски.
|
||||
var RedisClient *redis.Client
|
||||
var Subscriber *redis.PubSub
|
||||
|
||||
// Мапка с открытыми дескрипторами баз с настройками.
|
||||
var settingsDB = make(map[string]*pebble.DB)
|
||||
|
||||
// Main context.
|
||||
var Ctx = context.Background()
|
||||
|
||||
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
||||
455
internal/jabber/lib.go
Normal file
455
internal/jabber/lib.go
Normal file
@@ -0,0 +1,455 @@
|
||||
package jabber
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"aleesa-jabber-go/internal/log"
|
||||
|
||||
"github.com/eleksir/go-xmpp"
|
||||
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
// EstablishConnection устанавливает соединение с jabber-сервером.
|
||||
func EstablishConnection() error {
|
||||
var err error
|
||||
|
||||
if connecting && !IsConnected {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Проставляем глобальные переменные.
|
||||
connecting = true
|
||||
IsConnected = false
|
||||
RoomsConnected = make([]string, 0)
|
||||
|
||||
Talk, err = Options.NewClient()
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to connect to %s: %w", Options.Host, err)
|
||||
}
|
||||
|
||||
// По идее keepalive должен же проходить только, если мы уже на сервере, так?
|
||||
if _, err := Talk.SendKeepAlive(); err != nil {
|
||||
return fmt.Errorf("try to send initial KeepAlive, got error: %w", err)
|
||||
}
|
||||
|
||||
log.Info("Connected")
|
||||
|
||||
// Джойнимся к чятикам, но делаем это в фоне, чтобы не блочиться на ошибках, например, если бота забанили
|
||||
for _, room := range Config.Jabber.Channels {
|
||||
GTomb.Go(func() error { return joinMuc(room.Name) })
|
||||
}
|
||||
|
||||
GTomb.Go(func() error { return RotateStatus("") })
|
||||
|
||||
LastActivity = time.Now().Unix()
|
||||
connecting = false
|
||||
IsConnected = true
|
||||
|
||||
log.Debugf("Sending disco#info to %s", Config.Jabber.Server)
|
||||
|
||||
_, err = Talk.DiscoverInfo(Talk.JID(), Config.Jabber.Server)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to send disco#info to jabber server: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// joinMu джойнится к конференциям/каналам/комнатам в джаббере.
|
||||
func joinMuc(room string) error {
|
||||
log.Debugf("Sending disco#info from %s to %s", Talk.JID(), room)
|
||||
|
||||
if _, err := Talk.DiscoverInfo(Talk.JID(), room); err != nil {
|
||||
return fmt.Errorf("unable to send disco#info to MUC %s: %w", room, err)
|
||||
}
|
||||
|
||||
// Ждём, пока muc нам вернёт список фичей.
|
||||
for i := 0; i < (20 * int(Config.Jabber.ConnectionTimeout)); i++ {
|
||||
var (
|
||||
myRoom interface{}
|
||||
supported bool
|
||||
exist bool
|
||||
)
|
||||
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
|
||||
if myRoom, exist = MucCapsList.Get(room); !exist {
|
||||
// Пока не задискаверилась
|
||||
continue
|
||||
}
|
||||
|
||||
if supported, exist = myRoom.(map[string]bool)["muc_unsecured"]; exist {
|
||||
if supported {
|
||||
break
|
||||
}
|
||||
|
||||
log.Infof("Unable to join to password-protected room. Don't know how to enter passwords :)")
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Пытаемся зайти в комнату
|
||||
if _, err := Talk.JoinMUCNoHistory(room, getBotNickFromRoomConfig(room)); err != nil {
|
||||
return fmt.Errorf("unable to join to MUC: %s, %w", room, err)
|
||||
}
|
||||
|
||||
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 nil
|
||||
}
|
||||
|
||||
// Вот теперь точно можно слать статус.
|
||||
log.Infof("Joined to MUC: %s", room)
|
||||
|
||||
GTomb.Go(func() error { return RotateStatus(room) })
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ProbeServerLiveness проверяет живость соединения с сервером. Для многих серверов обязательная штука, без которой
|
||||
// они выкидывают клиента через некоторое время неактивности.
|
||||
func ProbeServerLiveness() error { //nolint:gocognit
|
||||
for {
|
||||
for {
|
||||
if Shutdown {
|
||||
return nil
|
||||
}
|
||||
|
||||
sleepTime := time.Duration(Config.Jabber.ServerPingDelay) * 1000 * time.Millisecond
|
||||
sleepTime += time.Duration(rand.Int63n(1000*Config.Jabber.PingSplayDelay)) * time.Millisecond
|
||||
time.Sleep(sleepTime)
|
||||
|
||||
if !IsConnected {
|
||||
continue
|
||||
}
|
||||
|
||||
// Пингуем, только если не было никакой активности в течение > Config.Jabber.ServerPingDelay,
|
||||
// в худшем случе это будет ~ (Config.Jabber.PingSplayDelay * 2) + Config.Jabber.PingSplayDelay
|
||||
// if (time.Now().Unix() - lastServerActivity) < (Config.Jabber.ServerPingDelay + Config.Jabber.PingSplayDelay) {
|
||||
// continue
|
||||
// }
|
||||
|
||||
if ServerCapsQueried { // Сервер ответил на disco#info
|
||||
var (
|
||||
value interface{}
|
||||
exist bool
|
||||
)
|
||||
|
||||
value, exist = ServerCapsList.Get("urn:xmpp:ping")
|
||||
|
||||
switch {
|
||||
// Сервер анонсировал, что умеет в c2s пинги
|
||||
case exist && value.(bool):
|
||||
// Таймаут c2s пинга. Возьмём сумму задержки между пингами, добавим таймаут коннекта и добавим
|
||||
// максимальную корректировку разброса.
|
||||
txTimeout := Config.Jabber.ServerPingDelay + Config.Jabber.ConnectionTimeout
|
||||
txTimeout += Config.Jabber.PingSplayDelay
|
||||
rxTimeout := txTimeout
|
||||
|
||||
rxTimeAgo := time.Now().Unix() - ServerPingTimestampRx
|
||||
|
||||
if ServerPingTimestampTx > 0 { // Первая пуля от нас ушла...
|
||||
switch {
|
||||
// Давненько мы не получали понгов от сервера, вероятно, соединение с сервером утеряно?
|
||||
case rxTimeAgo > (rxTimeout * 2):
|
||||
err := fmt.Errorf( //nolint:goerr113
|
||||
"stall connection detected. No c2s pong for %d seconds",
|
||||
rxTimeAgo,
|
||||
)
|
||||
|
||||
return err
|
||||
|
||||
// По-умолчанию, мы отправляем c2s пинг
|
||||
default:
|
||||
log.Debugf("Sending c2s ping from %s to %s", Talk.JID(), Config.Jabber.Server)
|
||||
|
||||
if err := Talk.PingC2S(Talk.JID(), Config.Jabber.Server); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
|
||||
ServerPingTimestampTx = time.Now().Unix()
|
||||
}
|
||||
|
||||
// Сервер не анонсировал, что умеет в c2s пинги
|
||||
default:
|
||||
log.Debug("Sending keepalive whitespace ping")
|
||||
|
||||
if _, err := Talk.SendKeepAlive(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else { // Сервер не ответил на disco#info
|
||||
log.Debug("Sending keepalive whitespace ping")
|
||||
|
||||
if _, err := Talk.SendKeepAlive(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ProbeMUCLiveness Пингует MUC-и, нужно для проверки, что клиент ещё находится в MUC-е.
|
||||
func ProbeMUCLiveness() { //nolint:gocognit
|
||||
for {
|
||||
for {
|
||||
for _, room := range RoomsConnected {
|
||||
var (
|
||||
exist bool
|
||||
lastActivityTS interface{}
|
||||
)
|
||||
|
||||
// Если записи про комнату нету, то пинговать её бессмысленно.
|
||||
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) {
|
||||
continue
|
||||
}
|
||||
|
||||
/* Пинг MUC-а по сценарию без серверной оптимизации мы реализовывать не будем. Это как-то не надёжно.
|
||||
go func(room string) {
|
||||
// Небольшая рандомная задержка перед пингом комнаты
|
||||
sleepTime := time.Duration(rand.Int63n(1000*Config.Jabber.PingSplayDelay)) * time.Millisecond //nolint:gosec
|
||||
time.Sleep(sleepTime)
|
||||
|
||||
if err := talk.PingS2S(talk.JID(), room+"/"+getBotNickFromRoomConfig(room)); err != nil {
|
||||
gTomb.Kill(err)
|
||||
continue
|
||||
}
|
||||
}(room)
|
||||
*/
|
||||
|
||||
var roomMap interface{}
|
||||
|
||||
roomMap, exist = MucCapsList.Get(room)
|
||||
|
||||
// Пинги комнаты проводим, только если она записана, как прошедшая disco#info и поддерживающая
|
||||
// Server Optimization.
|
||||
if exist && roomMap.(map[string]bool)["http://jabber.org/protocol/muc#self-ping-optimization"] {
|
||||
GTomb.Go(
|
||||
func() error {
|
||||
// Небольшая рандомная задержка перед пингом комнаты.
|
||||
sleepTime := time.Duration(rand.Int63n(1000*Config.Jabber.PingSplayDelay)) * time.Millisecond
|
||||
time.Sleep(sleepTime)
|
||||
|
||||
log.Debugf("Sending MUC ping from %s to %s", Talk.JID(), room)
|
||||
|
||||
if err := Talk.PingS2S(Talk.JID(), room); err != nil {
|
||||
log.Errorf("unable to ping MUC %s: %s", room, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
time.Sleep(time.Duration(Config.Jabber.MucPingDelay) * time.Second)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RotateStatus периодически изменяет статус бота в MUC-е согласно настройкам из кофига.
|
||||
func RotateStatus(room string) error {
|
||||
for {
|
||||
// TODO: Переделать на ticker-ы
|
||||
totalSleepTime := time.Duration(Config.Jabber.RuntimeStatus.RotationTime) * time.Second
|
||||
totalSleepTime += time.Duration(Config.Jabber.RuntimeStatus.RotationSplayTime) * time.Second
|
||||
|
||||
for {
|
||||
status := RandomPhrase(Config.Jabber.RuntimeStatus.Text)
|
||||
log.Debugf("Set status for MUC: %s to: %s", room, status)
|
||||
|
||||
var p xmpp.Presence
|
||||
|
||||
if room != "" {
|
||||
p = xmpp.Presence{ //nolint:exhaustruct
|
||||
To: room,
|
||||
Status: status,
|
||||
}
|
||||
} else {
|
||||
p = xmpp.Presence{ //nolint:exhaustruct
|
||||
Status: status,
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := Talk.SendPresence(p); err != nil {
|
||||
return fmt.Errorf("unable to send presence to MUC %s: %w", room, err)
|
||||
}
|
||||
|
||||
// Если мы не хотим ротировать, то цикл нам тут не нужен, просто выходим.
|
||||
if Config.Jabber.RuntimeStatus.RotationTime == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
time.Sleep(totalSleepTime)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RandomPhrase Выдаёт одну рандомную фразу из даденного списка фраз.
|
||||
func RandomPhrase(list []string) string {
|
||||
phrase := ""
|
||||
|
||||
if listLen := len(list); listLen > 0 {
|
||||
phrase = list[rand.Intn(listLen)]
|
||||
}
|
||||
|
||||
return phrase
|
||||
}
|
||||
|
||||
// interfaceToStringSlice превращает данный интерфейс в слайс строк.
|
||||
// Если может, конечно :) .
|
||||
func interfaceToStringSlice(iface interface{}) []string {
|
||||
var mySlice []string
|
||||
|
||||
// А теперь мы начинаем дурдом, нам надо превратить ёбанный interface{} в []string
|
||||
// Поскольку interface{} может быть чем угодно, перестрахуемся
|
||||
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)))
|
||||
}
|
||||
}
|
||||
|
||||
return mySlice
|
||||
}
|
||||
|
||||
// getRealJIDfromNick достаёт из запомненных presence-ов по даденному nick-у real jid с resource-ом. Nick должен
|
||||
// содержать имя конфы, откуда участник.
|
||||
func getRealJIDfromNick(fullNick string) string {
|
||||
var p xmpp.Presence
|
||||
|
||||
room := (strings.SplitN(fullNick, "/", 2))[0]
|
||||
|
||||
// Достанем presence участника
|
||||
presenceJSONInterface, present := RoomPresences.Get(room)
|
||||
|
||||
// Никого нет дома
|
||||
if !present {
|
||||
return ""
|
||||
}
|
||||
|
||||
presenceJSONStrings := interfaceToStringSlice(presenceJSONInterface)
|
||||
|
||||
for _, presepresenceJSONString := range presenceJSONStrings {
|
||||
_ = json.Unmarshal([]byte(presepresenceJSONString), &p)
|
||||
|
||||
if p.From == fullNick {
|
||||
return p.JID
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// isMucAdmin возращает true если указанный nick является овнером или админом в чяти.
|
||||
func isMucAdmin(fullNick string) bool {
|
||||
var p xmpp.Presence
|
||||
|
||||
room := (strings.SplitN(fullNick, "/", 2))[0]
|
||||
|
||||
// Достанем presence участника.
|
||||
presenceJSONInterface, present := RoomPresences.Get(room)
|
||||
|
||||
// Никого нет дома
|
||||
if !present {
|
||||
return false
|
||||
}
|
||||
|
||||
presenceJSONStrings := interfaceToStringSlice(presenceJSONInterface)
|
||||
|
||||
for _, presepresenceJSONString := range presenceJSONStrings {
|
||||
_ = json.Unmarshal([]byte(presepresenceJSONString), &p)
|
||||
|
||||
if p.From == fullNick {
|
||||
if p.Affiliation == "owner" || p.Affiliation == "admin" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// getMucNames возвращает список участников конференции.
|
||||
func getMucNames(room string) []string {
|
||||
var (
|
||||
names []string
|
||||
p xmpp.Presence
|
||||
)
|
||||
|
||||
// Достанем presence участников
|
||||
presenceJSONInterface, present := RoomPresences.Get(room)
|
||||
|
||||
if !present {
|
||||
return names
|
||||
}
|
||||
|
||||
// Достанем presence участника
|
||||
presenceJSONStrings := interfaceToStringSlice(presenceJSONInterface)
|
||||
|
||||
for _, presepresenceJSONString := range presenceJSONStrings {
|
||||
_ = json.Unmarshal([]byte(presepresenceJSONString), &p)
|
||||
|
||||
names = append(names, p.From)
|
||||
}
|
||||
|
||||
return names
|
||||
}
|
||||
|
||||
// getBotNickFromRoomConfig достаёт (короткий) ник бота из настроек чата.
|
||||
func getBotNickFromRoomConfig(room string) string {
|
||||
for _, roomStruct := range Config.Jabber.Channels {
|
||||
if roomStruct.Name == room {
|
||||
return roomStruct.Nick
|
||||
}
|
||||
}
|
||||
|
||||
return Config.Jabber.Nick
|
||||
}
|
||||
|
||||
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
||||
249
internal/jabber/read_config.go
Normal file
249
internal/jabber/read_config.go
Normal file
@@ -0,0 +1,249 @@
|
||||
package jabber
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"aleesa-jabber-go/internal/log"
|
||||
|
||||
"github.com/hjson/hjson-go"
|
||||
)
|
||||
|
||||
// ReadConfig читает и валидирует конфиг, а также выставляет некоторые default-ы, если значений для параметров в конфиге
|
||||
// нет.
|
||||
func ReadConfig() error { //nolint:gocognit,gocyclo
|
||||
var (
|
||||
err error
|
||||
configLoaded = false
|
||||
)
|
||||
|
||||
for _, location := range ConfigLocations {
|
||||
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)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
buf, err := os.ReadFile(location)
|
||||
|
||||
// Не удалось прочитать, попробуем следующего кандидата.
|
||||
if err != nil {
|
||||
log.Warnf("Skip reading config file %s: %s", location, err)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
// Исходя из документации, hjson какбы умеет парсить "кривой" json, но парсит его в map-ку.
|
||||
// Интереснее на выходе получить структурку: то есть мы вначале конфиг преобразуем в map-ку, затем эту map-ку
|
||||
// сериализуем в json, а потом json превращаем в структурку. Не очень эффективно, но он и нечасто требуется.
|
||||
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)
|
||||
|
||||
// Не удалось преобразовать map-ку в json
|
||||
if err != nil {
|
||||
log.Warnf("Skip parsing config file %s: %s", location, err)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(tmpJSON, &sampleConfig); err != nil {
|
||||
log.Warnf("Skip parsing config file %s: %s", location, err)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
// Валидируем значения из конфига
|
||||
if sampleConfig.Redis.Server == "" {
|
||||
sampleConfig.Redis.Server = "localhost"
|
||||
|
||||
log.Infof("Redis server is not defined in config, using localhost")
|
||||
}
|
||||
|
||||
if sampleConfig.Redis.Port == 0 {
|
||||
sampleConfig.Redis.Port = 6379
|
||||
}
|
||||
|
||||
if sampleConfig.Redis.Channel == "" {
|
||||
return fmt.Errorf("channel field in config file %s must be set", location) //nolint:goerr113
|
||||
}
|
||||
|
||||
if sampleConfig.Redis.MyChannel == "" {
|
||||
return fmt.Errorf("my_channel field in config file %s must be set", location) //nolint:goerr113
|
||||
}
|
||||
|
||||
// Значения для Jabber-клиента
|
||||
if sampleConfig.Jabber.Server == "" {
|
||||
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 = 5222
|
||||
|
||||
if sampleConfig.Jabber.Ssl {
|
||||
if !sampleConfig.Jabber.StartTLS {
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !sampleConfig.Jabber.Ssl {
|
||||
sampleConfig.Jabber.StartTLS = false
|
||||
}
|
||||
|
||||
if !sampleConfig.Jabber.Ssl || !sampleConfig.Jabber.StartTLS {
|
||||
sampleConfig.Jabber.SslVerify = false
|
||||
}
|
||||
|
||||
// sampleConfig.Jabber.InsecureAllowUnencryptedAuth = false, если не задан
|
||||
|
||||
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 == "" {
|
||||
return errors.New("jabber nick is not defined in config, quitting") //nolint:goerr113
|
||||
}
|
||||
|
||||
if sampleConfig.Jabber.Resource == "" {
|
||||
sampleConfig.Jabber.Resource = "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 не задан, то авторизации не будет
|
||||
|
||||
// Если не задано ни одного мастера, то бот сам себе мастер
|
||||
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
|
||||
}
|
||||
|
||||
for n, channel := range sampleConfig.Jabber.Channels {
|
||||
if channel.Name == "" {
|
||||
return fmt.Errorf("no \"name\" entry in jabber channel config") //nolint: err113
|
||||
}
|
||||
|
||||
if channel.Nick == "" {
|
||||
sampleConfig.Jabber.Channels[n].Nick = sampleConfig.Jabber.Nick
|
||||
}
|
||||
|
||||
// channel.Password может быть пустым, тогда пароля нет.
|
||||
} //nolint:wsl
|
||||
|
||||
// Если список фраз с которыми стартует бот пустой, вносим в него 1 запись с пустой строкой
|
||||
if len(sampleConfig.Jabber.StartupStatus) == 0 {
|
||||
sampleConfig.Jabber.StartupStatus[0] = ""
|
||||
}
|
||||
|
||||
// Если список статусов, с которыми работает бот пустой, вносим в него 1 запись с пустой строкой
|
||||
if len(sampleConfig.Jabber.RuntimeStatus.Text) == 0 {
|
||||
sampleConfig.Jabber.RuntimeStatus.Text[0] = ""
|
||||
}
|
||||
|
||||
// Если sampleConfig.Jabber.RuntimeStatus.RotationTime не задан, то он равен 0
|
||||
// Если sampleConfig.Jabber.RuntimeStatus.RotationSplayTime не задан, то он равен 0
|
||||
|
||||
if sampleConfig.ForwardsMax == 0 {
|
||||
sampleConfig.ForwardsMax = forwardMax
|
||||
}
|
||||
|
||||
if sampleConfig.DataDir == "" {
|
||||
return fmt.Errorf("data_dir field in config file %s must be set", location) //nolint:goerr113
|
||||
}
|
||||
|
||||
if sampleConfig.CSign == "" {
|
||||
sampleConfig.CSign = "!"
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
if !configLoaded {
|
||||
return fmt.Errorf("config was not loaded!") //nolint:goerr113,revive,stylecheck
|
||||
}
|
||||
|
||||
return err //nolint:wrapcheck
|
||||
}
|
||||
145
internal/jabber/redis.go
Normal file
145
internal/jabber/redis.go
Normal file
@@ -0,0 +1,145 @@
|
||||
package jabber
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"aleesa-jabber-go/internal/log"
|
||||
|
||||
"github.com/eleksir/go-xmpp"
|
||||
"github.com/go-redis/redis/v8"
|
||||
)
|
||||
|
||||
// RedisLoop вычитывает сообщения из редиски.
|
||||
func RedisLoop(redisMsgChan <-chan *redis.Message) error {
|
||||
// Обработчик событий от редиски
|
||||
for msg := range redisMsgChan {
|
||||
if Shutdown {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := redisMsgParser(msg.Payload); err != nil {
|
||||
log.Warnf("%s", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// redisMsgParser парсит json-чики прилетевшие из REDIS-ки.
|
||||
func redisMsgParser(msg string) error {
|
||||
var err error
|
||||
|
||||
if Shutdown {
|
||||
// Если мы завершаем работу программы, то нам ничего обрабатывать не надо.
|
||||
return err
|
||||
}
|
||||
|
||||
var j rMsg
|
||||
|
||||
log.Debugf("Incoming raw json: %s", msg)
|
||||
|
||||
if err := json.Unmarshal([]byte(msg), &j); err != nil {
|
||||
log.Warnf("Unable to to parse message from redis channel: %s", err)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate our j
|
||||
if exist := j.From; exist == "" {
|
||||
log.Warnf("Incorrect msg from redis, no from field: %s", msg)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if exist := j.Chatid; exist == "" {
|
||||
log.Warnf("Incorrect msg from redis, no chatid field: %s", msg)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if exist := j.Userid; exist == "" {
|
||||
log.Warnf("Incorrect msg from redis, no userid field: %s", msg)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if exist := j.Message; exist == "" {
|
||||
log.Warnf("Incorrect msg from redis, no message field: %s", msg)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if exist := j.Plugin; exist == "" {
|
||||
log.Warnf("Incorrect msg from redis, no plugin field: %s", msg)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if exist := j.Mode; exist == "" {
|
||||
log.Warnf("Incorrect msg from redis, no mode field: %s", msg)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// j.Misc.Answer может и не быть, тогда ответа на такое сообщение не будет
|
||||
if j.Misc.Answer == 0 {
|
||||
log.Debug("Field Misc->Answer = 0, skipping message")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// j.Misc.BotNick тоже можно не передавать, тогда будет записана пустая строка
|
||||
// j.Misc.CSign если нам его не передали, возьмём значение из конфига
|
||||
if exist := j.Misc.Csign; exist == "" {
|
||||
j.Misc.Csign = Config.CSign
|
||||
}
|
||||
|
||||
// j.Misc.FwdCnt если нам его не передали, то будет 0
|
||||
if exist := j.Misc.Fwdcnt; exist == 0 {
|
||||
j.Misc.Fwdcnt = 1
|
||||
}
|
||||
|
||||
// j.Misc.GoodMorning может быть быть 1 или 0, по-умолчанию 0
|
||||
// j.Misc.MsgFormat может быть быть 1 или 0, по-умолчанию 0
|
||||
// j.Misc.Username можно не передавать, тогда будет пустая строка
|
||||
|
||||
// Отвалидировались, теперь вернёмся к нашим баранам.
|
||||
if j.Misc.Answer == 1 {
|
||||
switch j.Mode {
|
||||
case "public":
|
||||
// Отправляем сообщение в чятик, а не тому, кто прислал нам исходное сообщение
|
||||
if _, err := Talk.Send(
|
||||
xmpp.Chat{ //nolint:exhaustruct
|
||||
Remote: j.Chatid,
|
||||
Type: "groupchat",
|
||||
Text: j.Message,
|
||||
},
|
||||
); err != nil {
|
||||
err = fmt.Errorf("unable to send phrase to room %s: %w", j.Chatid, err)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
case "private":
|
||||
// Отправляем сообщение тому, кто прислал сообщение
|
||||
if _, err := Talk.Send(
|
||||
xmpp.Chat{ //nolint:exhaustruct
|
||||
Remote: j.From,
|
||||
Type: "chat",
|
||||
Text: j.Message,
|
||||
},
|
||||
); err != nil {
|
||||
err = fmt.Errorf("unable to send phrase to jid %s: %w", j.From, err)
|
||||
|
||||
return err
|
||||
}
|
||||
default:
|
||||
log.Infof("Got message from redis that neither public or private: %s", msg)
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
||||
127
internal/jabber/settings-db-util.go
Normal file
127
internal/jabber/settings-db-util.go
Normal file
@@ -0,0 +1,127 @@
|
||||
package jabber
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
|
||||
"aleesa-jabber-go/internal/log"
|
||||
|
||||
"github.com/cockroachdb/errors/oserror"
|
||||
"github.com/cockroachdb/pebble"
|
||||
)
|
||||
|
||||
// StoreKV сохраняет в указанной бд ключ и значение.
|
||||
func StoreKV(db *pebble.DB, key string, value string) error {
|
||||
var (
|
||||
kArray = []byte(key)
|
||||
vArray = []byte(value)
|
||||
)
|
||||
|
||||
err := db.Set(kArray, vArray, pebble.Sync)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// FetchV достаёт значение по ключу.
|
||||
func FetchV(db *pebble.DB, key string) (string, error) {
|
||||
var (
|
||||
kArray = []byte(key)
|
||||
vArray []byte
|
||||
valueString string
|
||||
)
|
||||
|
||||
vArray, closer, err := db.Get(kArray)
|
||||
|
||||
if err != nil {
|
||||
return valueString, err
|
||||
}
|
||||
|
||||
valueString = string(vArray)
|
||||
err = closer.Close()
|
||||
|
||||
return valueString, err
|
||||
}
|
||||
|
||||
// Достанем настройку из БД с настройками.
|
||||
func getSetting(chatID string, setting string) string {
|
||||
var (
|
||||
err error
|
||||
chatHash = sha256.Sum256([]byte(chatID))
|
||||
database = fmt.Sprintf("settings_db/%x", chatHash)
|
||||
)
|
||||
|
||||
// Если БД не открыта, откроем её
|
||||
if _, ok := settingsDB[database]; !ok {
|
||||
var options pebble.Options
|
||||
// По дефолту ограничение ставится на мегабайты данных, а не на количество файлов, поэтому с дефолтными
|
||||
// настройками порождается огромное количество файлов. Умолчальное ограничение на количество файлов - 500 штук,
|
||||
// что нас не устраивает, поэтому немного снизим эту цифру до более приемлемых значений
|
||||
options.L0CompactionFileThreshold = 8
|
||||
settingsDB[database], err = pebble.Open(Config.DataDir+"/"+database, &options)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("Unable to open settings db, %s: %s\n", database, err)
|
||||
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
value, err := FetchV(settingsDB[database], setting)
|
||||
|
||||
// Если из базы ничего не вынулось, по каким-то причинам, то просто вернём пустую строку
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, pebble.ErrNotFound):
|
||||
log.Debugf("Unable to get value for %s: no record found in db %s", setting, database)
|
||||
|
||||
case errors.Is(err, fs.ErrNotExist):
|
||||
log.Debugf("Unable to get value for %s: db dir %s does not exist", setting, database)
|
||||
|
||||
case errors.Is(err, oserror.ErrNotExist):
|
||||
log.Debugf("Unable to get value for %s: db dir %s does not exist", setting, database)
|
||||
|
||||
default:
|
||||
log.Errorf("Unable to get value for %s in db dir %s: %s", setting, database, err)
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
// Сохраним настройку в БД с настройками.
|
||||
func saveSetting(chatID string, setting string, value string) error {
|
||||
var (
|
||||
chatHash = sha256.Sum256([]byte(chatID))
|
||||
database = fmt.Sprintf("settings_db/%x", chatHash)
|
||||
err error
|
||||
)
|
||||
|
||||
// Если БД не открыта, откроем её
|
||||
if _, ok := settingsDB[database]; !ok {
|
||||
var options pebble.Options
|
||||
// По дефолту ограничение ставится на мегабайты данных, а не на количество файлов, поэтому с дефолтными
|
||||
// настройками порождается огромное количество файлов. Умолчальное ограничение на количество файлов - 500 штук,
|
||||
// что нас не устраивает, поэтому немного снизим эту цифру до более приемлемых значений
|
||||
options.L0CompactionFileThreshold = 8
|
||||
|
||||
settingsDB[database], err = pebble.Open(Config.DataDir+"/"+database, &options)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("Unable to open settings db, %s: %s\n", database, err)
|
||||
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := StoreKV(settingsDB[database], setting, value); err != nil {
|
||||
log.Errorf("Unable to save setting %s in database %s: %s", setting, database, err)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
||||
86
internal/jabber/sighandler.go
Normal file
86
internal/jabber/sighandler.go
Normal file
@@ -0,0 +1,86 @@
|
||||
package jabber
|
||||
|
||||
import (
|
||||
"aleesa-jabber-go/internal/log"
|
||||
"os"
|
||||
"syscall"
|
||||
|
||||
"github.com/eleksir/go-xmpp"
|
||||
)
|
||||
|
||||
// SigHandler Хэндлер сигналов закрывает все бд, все сетевые соединения и сваливает из приложения.
|
||||
func SigHandler() error {
|
||||
log.Debug("Installing signal handler")
|
||||
|
||||
for s := range SigChan {
|
||||
switch s {
|
||||
case syscall.SIGINT:
|
||||
log.Info("Got SIGINT, quitting")
|
||||
case syscall.SIGTERM:
|
||||
log.Info("Got SIGTERM, quitting")
|
||||
case syscall.SIGQUIT:
|
||||
log.Info("Got SIGQUIT, quitting")
|
||||
|
||||
// Заходим на новую итерацию, если у нас "неинтересный" сигнал.
|
||||
default:
|
||||
continue
|
||||
}
|
||||
|
||||
var err error
|
||||
|
||||
// Чтобы не срать в логи ошибками, проставим shutdown state приложения в true.
|
||||
Shutdown = true
|
||||
|
||||
// Отпишемся от всех каналов и закроем коннект к редиске
|
||||
if err = Subscriber.Unsubscribe(Ctx); err != nil {
|
||||
log.Errorf("Unable to unsubscribe from redis channels cleanly: %s", err)
|
||||
} else {
|
||||
log.Debug("Unsubscribe from all redis channels")
|
||||
}
|
||||
|
||||
if err = Subscriber.Close(); err != nil {
|
||||
log.Errorf("Unable to close redis connection cleanly: %s", err)
|
||||
} else {
|
||||
log.Debug("Close redis connection")
|
||||
}
|
||||
|
||||
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
|
||||
To: room,
|
||||
Status: "Offline",
|
||||
Type: "unavailable",
|
||||
},
|
||||
); err != nil {
|
||||
log.Infof("Unable to send presence to jabber server: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
// И закрываем соединение.
|
||||
log.Info("Closing connection to jabber server")
|
||||
|
||||
if err := Talk.Close(); err != nil {
|
||||
log.Infof("Unable to close connection to jabber server: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(settingsDB) > 0 {
|
||||
log.Debug("Closing runtime jabber room settings db")
|
||||
|
||||
for _, db := range settingsDB {
|
||||
_ = db.Close()
|
||||
}
|
||||
}
|
||||
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
||||
143
internal/jabber/types.go
Normal file
143
internal/jabber/types.go
Normal file
@@ -0,0 +1,143 @@
|
||||
package jabber
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
)
|
||||
|
||||
// myConfig прототип структурки с конфигом.
|
||||
type myConfig struct {
|
||||
Redis struct {
|
||||
Server string `json:"server,omitempty"`
|
||||
Port int `json:"port,omitempty"`
|
||||
Channel string `json:"channel,omitempty"`
|
||||
MyChannel string `json:"my_channel,omitempty"`
|
||||
} `json:"redis"`
|
||||
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"`
|
||||
Channels []struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Nick string `json:"nick,omitempty"`
|
||||
Password string `json:"password,omitempty"`
|
||||
} `json:"channels,omitempty"`
|
||||
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"`
|
||||
} `json:"jabber,omitempty"`
|
||||
|
||||
DataDir string `json:"data_dir,omitempty"`
|
||||
ForwardsMax int64 `json:"forwards_max,omitempty"`
|
||||
CSign string `json:"csign,omitempty"`
|
||||
Loglevel string `json:"loglevel,omitempty"`
|
||||
Log string `json:"log,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"`
|
||||
Xmlns string `xml:"xmlns,attr"`
|
||||
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"`
|
||||
Xmlns string `xml:"xmlns,attr"`
|
||||
Items struct {
|
||||
Text string `xml:",chardata"`
|
||||
Node string `xml:"node,attr"`
|
||||
MaxItems string `xml:"max_items,attr"`
|
||||
} `xml:"items"`
|
||||
}
|
||||
|
||||
// jabberTimeIqGetQuery прототип структурки для разбора IQ запросов на локальное время клиента,
|
||||
// https://xmpp.org/extensions/xep-0202.html
|
||||
type jabberTimeIqGetQuery struct {
|
||||
// <time xmlns="urn:xmpp:time"/>
|
||||
XMLName xml.Name `xml:"time"`
|
||||
Text string `xml:",chardata"`
|
||||
Xmlns string `xml:"xmlns,attr"`
|
||||
}
|
||||
|
||||
// jabberIqPing прототип структурки для разбора IQ запросов на пинг клиента, https://xmpp.org/extensions/xep-0199.html
|
||||
type jabberIqPing struct {
|
||||
XMLName xml.Name `xml:"ping"`
|
||||
Text string `xml:",chardata"`
|
||||
Xmlns string `xml:"xmlns,attr"`
|
||||
}
|
||||
|
||||
// jabberIqErrorCancelNotAcceptable прототип структурки для разбора IQ ответов, когда сервис (сервер, клиент, etc) не
|
||||
// может или не хочет принимать наш iq-запрос https://xmpp.org/extensions/xep-0099.html
|
||||
type jabberIqErrorCancelNotAcceptable 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"`
|
||||
}
|
||||
|
||||
// Входящее сообщение из pubsub-канала redis-ки.
|
||||
type rMsg struct {
|
||||
From string `json:"from,omitempty"`
|
||||
Chatid string `json:"chatid,omitempty"`
|
||||
Userid string `json:"userid,omitempty"`
|
||||
Threadid string `json:"threadid,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Plugin string `json:"plugin,omitempty"`
|
||||
Mode string `json:"mode,omitempty"`
|
||||
Misc struct {
|
||||
Answer int64 `json:"answer,omitempty"`
|
||||
Botnick string `json:"bot_nick,omitempty"`
|
||||
Csign string `json:"csign,omitempty"`
|
||||
Fwdcnt int64 `json:"fwd_cnt,omitempty"`
|
||||
GoodMorning int64 `json:"good_morning,omitempty"`
|
||||
Msgformat int64 `json:"msg_format,omitempty"`
|
||||
Username string `json:"username,omitempty"`
|
||||
} `json:"Misc"`
|
||||
}
|
||||
|
||||
// Исходящее сообщение в pubsub-канал redis-ки.
|
||||
type sMsg struct {
|
||||
From string `json:"from"`
|
||||
Chatid string `json:"chatid"`
|
||||
Userid string `json:"userid"`
|
||||
Threadid string `json:"threadid"`
|
||||
Message string `json:"message"`
|
||||
Plugin string `json:"plugin"`
|
||||
Mode string `json:"mode"`
|
||||
Misc struct {
|
||||
Answer int64 `json:"answer"`
|
||||
Botnick string `json:"bot_nick"`
|
||||
Csign string `json:"csign"`
|
||||
Fwdcnt int64 `json:"fwd_cnt"`
|
||||
GoodMorning int64 `json:"good_morning"`
|
||||
Msgformat int64 `json:"msg_format"`
|
||||
Username string `json:"username"`
|
||||
} `json:"misc"`
|
||||
}
|
||||
|
||||
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
||||
158
internal/log/log.go
Normal file
158
internal/log/log.go
Normal file
@@ -0,0 +1,158 @@
|
||||
package log
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Handler exposes slog handler struct.
|
||||
var Handler slog.Handler
|
||||
var Ctx = context.Background()
|
||||
var Writer io.Writer
|
||||
var reader io.Reader
|
||||
|
||||
// Init setup logger stuff.
|
||||
// level can be error, warn, info, debug if something other supplied info level selected.
|
||||
// fileDescriptor should be opened before supplying it to Init().
|
||||
func Init(level string, fileDescriptor *os.File) {
|
||||
var loglevel slog.Level
|
||||
|
||||
// no panic, no trace.
|
||||
switch level {
|
||||
case "debug":
|
||||
loglevel = slog.LevelDebug
|
||||
|
||||
case "info":
|
||||
loglevel = slog.LevelInfo
|
||||
|
||||
case "warn":
|
||||
loglevel = slog.LevelWarn
|
||||
|
||||
case "error":
|
||||
loglevel = slog.LevelError
|
||||
|
||||
default:
|
||||
loglevel = slog.LevelInfo
|
||||
}
|
||||
|
||||
opts := &slog.HandlerOptions{ //nolint: exhaustruct
|
||||
// Use the ReplaceAttr function on the handler options
|
||||
// to be able to replace any single attribute in the log output
|
||||
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr { //nolint: revive
|
||||
// check that we are handling the time key
|
||||
if a.Key != slog.TimeKey {
|
||||
return a
|
||||
}
|
||||
|
||||
t := a.Value.Time()
|
||||
|
||||
// change the value from a time.Time to a String
|
||||
// where the string has the correct time format.
|
||||
a.Value = slog.StringValue(t.Format(time.DateTime))
|
||||
|
||||
return a
|
||||
},
|
||||
|
||||
Level: loglevel,
|
||||
}
|
||||
|
||||
Handler = slog.NewTextHandler(fileDescriptor, opts)
|
||||
|
||||
slog.SetDefault(
|
||||
slog.New(Handler),
|
||||
)
|
||||
|
||||
reader, Writer = io.Pipe()
|
||||
|
||||
go readIoPipe()
|
||||
}
|
||||
|
||||
// readIoPipe reads pipe and writes it to slog.
|
||||
func readIoPipe() {
|
||||
scanner := bufio.NewScanner(reader)
|
||||
|
||||
for {
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text() // Get the current line as a string
|
||||
slog.Debug(line)
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
// io.EOF is expected when done reading, e.g. on exit.
|
||||
if errors.Is(err, io.EOF) {
|
||||
return
|
||||
}
|
||||
|
||||
// something weird happen, we should it log to guess later what it was.
|
||||
Errorf("Error during scanning log writer pipe: %v\n", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetLevel returns current loglevel.
|
||||
func GetLevel() string {
|
||||
switch {
|
||||
case Handler.Enabled(Ctx, slog.LevelDebug):
|
||||
return "debug"
|
||||
|
||||
case Handler.Enabled(Ctx, slog.LevelInfo):
|
||||
return "info"
|
||||
|
||||
case Handler.Enabled(Ctx, slog.LevelWarn):
|
||||
return "warn"
|
||||
|
||||
case Handler.Enabled(Ctx, slog.LevelError):
|
||||
return "error"
|
||||
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
// Error passes message directly to slog.Error().
|
||||
func Error(message string) {
|
||||
slog.Error(message)
|
||||
}
|
||||
|
||||
// Errorf logs message on error log level and allows to supply arguments in printf() style.
|
||||
func Errorf(format string, a ...any) {
|
||||
slog.Error(fmt.Sprintf(format, a...))
|
||||
}
|
||||
|
||||
// Warn passes message directly to slog.Warn().
|
||||
func Warn(message string) {
|
||||
slog.Warn(message)
|
||||
}
|
||||
|
||||
// Warnf logs message on warn log level and allows to supply arguments in printf() style.
|
||||
func Warnf(format string, a ...any) {
|
||||
slog.Warn(fmt.Sprintf(format, a...))
|
||||
}
|
||||
|
||||
// Info passes message directly to slog.Info().
|
||||
func Info(message string) {
|
||||
slog.Info(message)
|
||||
}
|
||||
|
||||
// Infof logs message on info log level and allows to supply arguments in printf() style.
|
||||
func Infof(format string, a ...any) {
|
||||
slog.Info(fmt.Sprintf(format, a...))
|
||||
}
|
||||
|
||||
// Debug passes message directly to slog.Debug().
|
||||
func Debug(message string) {
|
||||
slog.Debug(message)
|
||||
}
|
||||
|
||||
// Debugf logs message on debug log level and allows to supply arguments in printf() style.
|
||||
func Debugf(format string, a ...any) {
|
||||
slog.Debug(fmt.Sprintf(format, a...))
|
||||
}
|
||||
|
||||
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
||||
Reference in New Issue
Block a user