Implement help, помощь, rehash commands, restrict presence collection precedure

This commit is contained in:
eleksir
2023-09-03 14:52:35 +03:00
parent 91eac5cb6d
commit ca384b5165
7 changed files with 322 additions and 54 deletions

View File

@@ -1,6 +1,7 @@
#!/usr/bin/env gmake -f
BUILDOPTS=-ldflags="-s -w" -a -gcflags=all=-l -trimpath -pgo=auto
FILELIST=collection.go types.go globals.go lib.go event_parser.go buny.go commands.go main.go
all: clean build
@@ -9,15 +10,15 @@ ifeq ($(OS),Windows_NT)
# powershell
ifeq ($(SHELL),sh.exe)
SET CGO_ENABLED=0
go build ${BUILDOPTS} -o "buny-jabber-bot" collection.go types.go globals.go lib.go event_parser.go buny.go main.go
go build ${BUILDOPTS} -o "buny-jabber-bot" ${FILELIST}
else
# jetbrains golang
CGO_ENABLED=0
go build ${BUILDOPTS} -o "buny-jabber-bot" collection.go types.go globals.go lib.go event_parser.go buny.go main.go
go build ${BUILDOPTS} -o "buny-jabber-bot" ${FILELIST}
endif
# bash/git bash
else
CGO_ENABLED=0 go build ${BUILDOPTS} -o "buny-jabber-bot" collection.go types.go globals.go lib.go event_parser.go buny.go main.go
CGO_ENABLED=0 go build ${BUILDOPTS} -o "buny-jabber-bot" ${FILELIST}
endif
clean:

141
commands.go Normal file
View File

@@ -0,0 +1,141 @@
package main
import (
"fmt"
"strings"
"github.com/davecgh/go-spew/spew"
"github.com/eleksir/go-xmpp"
log "github.com/sirupsen/logrus"
)
func cmd(v xmpp.Chat) error {
var err error
switch {
case v.Text == fmt.Sprintf("%shelp", config.CSign) || v.Text == fmt.Sprintf("%sпомощь", config.CSign):
var (
answer string
chosenOneTalks = false
)
realJID := getRealJIDfromNick(v.Remote)
for _, master := range config.Jabber.BotMasters {
if (strings.SplitN(realJID, "/", 2))[0] == master {
chosenOneTalks = true
}
}
if chosenOneTalks {
answer = fmt.Sprintf("%sпомощь - этот список команд\n", config.CSign)
answer += fmt.Sprintf("%shelp - this commands list\n", config.CSign)
answer += fmt.Sprintf("%srehash - reload white and black lists", config.CSign)
} else {
answer = "Ничем помочь не могу. Луна не светит на тебя."
}
dest := v.Remote
if v.Type == "groupchat" {
dest = (strings.SplitN(v.Remote, "/", 2))[0]
}
if _, err := talk.Send(
xmpp.Chat{ //nolint:exhaustruct
Remote: dest,
Text: strings.TrimSpace(answer),
Type: v.Type,
},
); err != nil {
err = fmt.Errorf("unable to send message to %s: %w", v.Remote, err)
return err
}
case v.Text == fmt.Sprintf("%srehash", config.CSign):
/* Эта команда может прилетать из приватной беседы с realjid-ом, из приватной беседы с chat-nick-ом, а также из
* чятика.
* Groupchat мы можем определить по v.Type (groupchat) и тогда понятно что делать.
* А вот, например, приватные притязания определить несколько сложно-вато. Но можно попробовать, например,
* пытаться по списку presence-ов находить персонажа и его real jid и уже из этого определять master он или нет.
*/
// Попробуем определить каков алгоритм наших действий, в зависимости от типа сообщения
switch v.Type {
case "groupchat":
// Проверим, является ли "командир" избранным или это самозванец?
var (
chosenOneTalks = false
listsLoaded = true
)
realJID := getRealJIDfromNick(v.Remote)
for _, master := range config.Jabber.BotMasters {
if (strings.SplitN(realJID, "/", 2))[0] == master {
chosenOneTalks = true
}
}
if !chosenOneTalks {
log.Infof(
"Command %srehash given by non-bot_master user %s(%s), ignoring",
config.CSign,
realJID,
v.Remote,
)
return err
}
if err := readWhitelist(); err != nil {
listsLoaded = false
var msg xmpp.Chat
msg.Remote = v.Remote
msg.Type = "chat" // private message
msg.Text = fmt.Sprint(err)
if _, err := talk.Send(msg); err != nil {
return err
}
}
if err := readBlacklist(); err != nil {
listsLoaded = false
var msg xmpp.Chat
msg.Remote = v.Remote
msg.Type = "chat" // private message
msg.Text = fmt.Sprint(err)
if _, err := talk.Send(msg); err != nil {
return err
}
}
if listsLoaded {
room := (strings.SplitN(v.Remote, "/", 2))[0]
var msg xmpp.Chat
msg.Remote = room
msg.Type = v.Type
msg.Text = "Сделано"
if _, err := talk.Send(msg); err != nil {
return err
}
}
case "chat":
default:
log.Infof("Rehash command from outer space: %s", spew.Sdump(v))
}
return err
default:
return err
}
return err
}

View File

@@ -3,46 +3,80 @@
"jabber": {
# Если не задан, будет localhost
"server": "jabber.tld",
# Если не задано - 5222 для нешифрованного соединения или для starttls и 5223 для голого (старорежимного) ssl
"port": 5222,
# Включить шифрование
"ssl": true,
# Это более современный протокол и как раз на нём работет большинство jabber-серверов, порт 5222,
# jabber.ru по этому протоколу как-то странно работает, если его включить, я получаю
# tls: first record does not look like a TLS handshake
"starttls" : true,
# Не проверяем сертификаты по-умолчанию.
# Вам правда хочется всё это проверять? Там по идее могут быть всякие Lets Encrypt, которые хоть и работают, но
# создают кучу проблем
"ssl_verify": false,
# Позволять авторизацию логин-паролем по незашифрованному каналу связи
"insecureallowunencryptedauth": false,
# Таймаут установления соединения с сервером, в секундах. 10 секунд, если не задано.
"connection_timeout": 10,
# Задержка перед пере-коннекчиванием. 3 секунды, если не задано.
"reconnect_delay": 3,
# Промежуток между пингами сервера, для определения его живости. По-умолчанию 60 секунд.
# Пинги посылаются, если в течение этого времени не было активности со стороны сервера.
"server_ping_delay": 60,
# Промежуток между пингами muc-ов. По-умолчанию 900 секунд. (Согласно рекомендации в xep-0410.)
# Пинги посылаются, если в течение этого времени не было активности со стороны muc-а.
"muc_ping_delay": 900,
# Сколько ждать перед переджоиванием к комнате. По-умолчанию, 3 секунды.
"muc_rejoin_delay": 3,
# Ping-to-ping splay delay. Варьируемая задержка между пингами, чтобы не набирать высокий message rate.
# По-умолчанию, 3 секунды. Под капотом преобразуются в миллисекунды. Важна для пингов MUC-ов, особенно,
# если их МНОГО.
"ping_splay_delay": 3,
# Ник бота, с которым он будет заходить в комнаты
"nick": "buny",
# Уникальная часть jid-а, специфичная для клиента
"resource" : "Лунная призма",
# Имя пользователя для авторизации на сервере
"user": "buny@jabber.tld",
# Пароль для авторизации
"password": "secret",
# Список JID-ов, которым позволено управлять ботом (например, давать отмашку на rehash).
# В списке jid-ы без resource-а.
# Если не задано ни одного мастера, то бот сам себе мастер.
"bot_masters": [
"luna@earth.tld"
],
# Jabber-каналы, к которым бот попробует присоединиться при старте
"channels": [
"my_channel@conference.jabber.tld",
"another_channel@conference.jabber.tld"
],
# Статус, с которым бот запускается. (если всё нормально, этот статус будет видать только в логах сервера и
# на долю секунды он промелькнёт у всех в ростере, у кого бот в знакомых. Может отсуствовать, тогда статус будет
# пустой строкой
"startup_status": [
"bunny bot is starting"
],
# Статус, который демонстрируется во время работы бота, как xa
# (по задумке - extended away, на практике, к away обычно отношения не имеет)
"runtime_status": {
@@ -62,15 +96,19 @@
"Звезда нашей надежды никогда не исчезнет. Пока она горит, всё будет в порядке. Мы не сможем проиграть",
"Голова с хвостиками… Не вмешивайся в наши дела! Играть в войну — это опасно"
],
# Период смены статуса, сек. Если параметра нет или он выставлен в 0, ротации не будет
# 3 дня
"rotation_time": 259200,
# Рандомная составляющая, добавляемая к периоду смены статуса, сек.
# 4 дня
"rotation_splay_time": 345600
},
# Произносим ли что-то пафосное в момоент бана
"ban_phrases_enable": false,
# Пафосные фраза, произносимые перед баном
"ban_phrases": [
"Во имя Луны, я накажу тебя!",
@@ -103,18 +141,12 @@
"Мы сражаемся за мир и равенство!",
"Мы — надежда для всех, кто нуждается в защите!",
"Мы — воительницы справедливости, и ничто не остановит нас!"
],
# Имя пользователя для авторизации на сервере
"user": "buny@jabber.tld",
# Пароль для авторизации
"password": "secret",
# Jabber-каналы, к которым бот попробует присоединиться при старте
"channels": [
"my_channel@conference.jabber.tld",
"another_channel@conference.jabber.tld"
]
},
# command sign символ-суффикс команды, например !rehash или !help. По-умолчанию "!".
"csign" : "!",
# Многословность логов. Если не задано, то info. Debug - ДЕЙСТВИТЕЛЬНО вербозный уровень логгирования.
# Есть ещё trace :)
"loglevel": "debug",

View File

@@ -42,6 +42,12 @@ func parseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo
return
}
if err := cmd(v); err != nil {
gTomb.Kill(err)
return
}
lastActivity = lastServerActivity
if muc, _ := strings.CutSuffix(v.Remote, "/"); muc != "" {
@@ -55,6 +61,12 @@ func parseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo
// eleksir@jabber.ru/array.lan - это если мы работаем через ростер
log.Debugf("Private message: %s", v.Text)
if err := cmd(v); err != nil {
gTomb.Kill(err)
return
}
lastActivity = lastServerActivity
// Внезапно, ошибки. По идее они должны ассоциироваться с отправляемыми сообщениями, но по ходу это
@@ -539,7 +551,7 @@ func parseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo
var p xmpp.Presence
_ = json.Unmarshal([]byte(presenceJSONstring), &p)
if p.JID == v.JID {
if p.From == v.From {
continue
}
@@ -551,15 +563,29 @@ func parseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo
// Участник пришёл
default:
var presenceJSONStrings []string
var newPresenceJSONStrings []string
var presenceJSONBytes []byte
if presenceJSONInterface, present := roomPresences.Get(room); present {
presenceJSONStrings = interfaceToStringSlice(presenceJSONInterface)
}
presenceJSONBytes, _ = json.Marshal(v)
presenceJSONStrings = append(presenceJSONStrings, string(presenceJSONBytes))
roomPresences.Set(room, presenceJSONStrings)
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)
}
// Проверяем, а не злодей ли зашёл? Сделать это мы можем, только если мы находимся в комнате.

123
lib.go
View File

@@ -2,11 +2,13 @@ package main
import (
"encoding/json"
"errors"
"fmt"
"math/rand"
"os"
"path/filepath"
"reflect"
"strings"
"syscall"
"time"
@@ -68,14 +70,19 @@ func sigHandler() {
// readConfig читает и валидирует конфиг, а также выставляет некоторые default-ы, если значений для параметров в конфиге
// нет.
func readConfig() { //nolint:gocognit,gocyclo
configLoaded := false
executablePath, err := os.Executable()
func readConfig() error { //nolint:gocognit,gocyclo
var (
err error
executablePath string
configLoaded = false
)
executablePath, err = os.Executable()
if err != nil {
log.Errorf("Unable to get current executable path: %s", err)
err := fmt.Errorf("unable to get current executable path: %w", err)
os.Exit(1)
return err
}
configJSONPath := fmt.Sprintf("%s/data/config.json", filepath.Dir(executablePath))
@@ -210,8 +217,7 @@ func readConfig() { //nolint:gocognit,gocyclo
}
if sampleConfig.Jabber.Nick == "" {
log.Errorf("Jabber nick is not defined in config, quitting")
os.Exit(1)
return errors.New("jabber nick is not defined in config, quitting") //nolint:goerr113
}
if sampleConfig.Jabber.Resource == "" {
@@ -227,13 +233,15 @@ func readConfig() { //nolint:gocognit,gocyclo
}
// Если sampleConfig.Jabber.Password не задан, то авторизации не будет
// Если sampleConfig.Jabber.Sasl не задан, то авторизация происходит через NickServ
// Если sampleConfig.Jabber.BanPhrasesEnable не задан, то он false
// Если не задано ни одного мастера, то бот сам себе мастер
if len(sampleConfig.Jabber.BotMasters) == 0 {
sampleConfig.Jabber.BotMasters[0] = sampleConfig.Jabber.User
}
// Если список фраз, с которыми банят пустой, то вносим в него одну позицию с пустой строкой
if len(sampleConfig.Jabber.BanPhrases) == 0 {
sampleConfig.Jabber.BanPhrases[0] = ""
// Нам бот нужен в каких-то чат-румах, а не "просто так"
if len(sampleConfig.Jabber.Channels) < 1 {
return errors.New("no jabber channels/rooms defined in config, quitting") //nolint:goerr113
}
// Если список фраз с которыми стартует бот пустой, вносим в него 1 запись с пустой строкой
@@ -249,10 +257,15 @@ func readConfig() { //nolint:gocognit,gocyclo
// Если sampleConfig.Jabber.RuntimeStatus.RotationTime не задан, то он равен 0
// Если sampleConfig.Jabber.RuntimeStatus.RotationSplayTime не задан, то он равен 0
// Нам бот нужен в каких-то чат-румах, а не "просто так"
if len(sampleConfig.Jabber.Channels) < 1 {
log.Errorf("No jabber channels/rooms defined in config, quitting")
os.Exit(1)
// Если sampleConfig.Jabber.BanPhrasesEnable не задан, то он false
// Если список фраз, с которыми банят пустой, то вносим в него одну позицию с пустой строкой
if len(sampleConfig.Jabber.BanPhrases) == 0 {
sampleConfig.Jabber.BanPhrases[0] = ""
}
if sampleConfig.CSign == "" {
sampleConfig.CSign = "!"
}
if sampleConfig.Loglevel == "" {
@@ -271,19 +284,26 @@ func readConfig() { //nolint:gocognit,gocyclo
}
if !configLoaded {
log.Error("Config was not loaded! Refusing to start.")
os.Exit(1)
return errors.New("config was not loaded!") //nolint:goerr113
}
return err //nolint:wrapcheck
}
// readWhitelist читает и валидирует белые списки пользователей.
func readWhitelist() {
whitelistLoaded := false
executablePath, err := os.Executable()
func readWhitelist() error {
var (
whitelistLoaded = false
err error
executablePath string
)
executablePath, err = os.Executable()
if err != nil {
log.Errorf("Unable to get current executable path: %s", err)
os.Exit(1)
err = fmt.Errorf("unable to get current executable path: %w", err)
return err
}
whitelistJSONPath := fmt.Sprintf("%s/data/whitelist.json", filepath.Dir(executablePath))
@@ -360,21 +380,26 @@ func readWhitelist() {
}
if !whitelistLoaded {
log.Error("Whitelist was not loaded! Refusing to start.")
os.Exit(1)
return errors.New("whitelist was not loaded!") //nolint:goerr113
}
return err
}
// readBlacklist читает и валидирует чёрные списки пользователей.
func readBlacklist() {
blacklistLoaded := false
executablePath, err := os.Executable()
func readBlacklist() error {
var (
blacklistLoaded = false
err error
executablePath string
)
executablePath, err = os.Executable()
if err != nil {
log.Errorf("Unable to get current executable path: %s", err)
err = fmt.Errorf("unable to get current executable path: %w", err)
os.Exit(1)
return err
}
whitelistJSONPath := fmt.Sprintf("%s/data/blacklist.json", filepath.Dir(executablePath))
@@ -451,10 +476,10 @@ func readBlacklist() {
}
if !blacklistLoaded {
log.Error("Blacklist was not loaded! Refusing to start.")
os.Exit(1)
return errors.New("blacklist was not loaded!")
}
return err
}
// establishConnection устанавливает соединение с jabber-сервером.
@@ -582,7 +607,7 @@ func joinMuc(room string) {
go RotateStatus(room)
// Время прoверить участников на предмет злобности
// Время проверить участников на предмет злобности
namesInterface, present := roomPresences.Get(room)
// Если room есть в списке presence-ов, то фигачим. room там должен быть, просто обязан.
@@ -855,4 +880,32 @@ func interfaceToStringSlice(iface interface{}) []string {
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 ""
}
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */

19
main.go
View File

@@ -24,7 +24,11 @@ func init() {
TimestampFormat: "2006-01-02 15:04:05",
})
readConfig()
if err := readConfig(); err != nil {
log.Error(err)
os.Exit(1)
}
// no panic
switch config.Loglevel {
@@ -67,8 +71,17 @@ func main() {
}
// Самое время вгрузить белые и чёрные списки, если что - получим ошибку в лог.
readWhitelist()
readBlacklist()
if err := readWhitelist(); err != nil {
log.Error(err)
os.Exit(1)
}
if err := readBlacklist(); err != nil {
log.Error(err)
os.Exit(1)
}
// github.com/mattn/go-xmpp пишет в stdio, нам этого не надо, ловим выхлоп его в logrus с уровнем trace
xmpp.DebugWriter = log.WithFields(log.Fields{"logger": "stdlib"}).WriterLevel(log.TraceLevel)

View File

@@ -23,6 +23,7 @@ type myConfig struct {
Resource string `json:"resource,omitempty"`
User string `json:"user,omitempty"`
Password string `json:"password,omitempty"`
BotMasters []string `json:"bot_masters"`
Channels []string `json:"channels"`
StartupStatus []string `json:"startup_status,omitempty"`
RuntimeStatus struct {
@@ -34,6 +35,7 @@ type myConfig struct {
BanPhrases []string `json:"ban_phrases,omitempty"`
} `json:"jabber,omitempty"`
CSign string `json:"csign,omitempty"`
Loglevel string `json:"loglevel,omitempty"`
Log string `json:"log,omitempty"`
}