From 8c24a1d8c68f0c0f06b38d941daaac2ba120b410 Mon Sep 17 00:00:00 2001 From: eleksir Date: Sun, 30 Mar 2025 15:41:46 +0300 Subject: [PATCH] Implement grabbing +B flag, connect command sequence fixes * Implement i-am-bot, +B, flag grabbing. * Move collections to internal packages. * Postpone nickserv login sequence to 376 and 422 events, as it should be after motd message * Postpone join channels to 376 and 422 events, as it should be after motd message --- Makefile | 3 +- aleesa-irc-go-lib.go | 116 +++++++++++++++--- globals.go | 11 +- .../anycollection/anycollection.go | 30 ++--- internal/boolcollection/boolcollection.go | 58 +++++++++ msg-parsers.go | 4 - util.go | 2 +- 7 files changed, 184 insertions(+), 40 deletions(-) rename collection.go => internal/anycollection/anycollection.go (66%) create mode 100644 internal/boolcollection/boolcollection.go diff --git a/Makefile b/Makefile index 5dd994b..aa8157f 100644 --- a/Makefile +++ b/Makefile @@ -6,8 +6,7 @@ BUILDOPTS=-ldflags="-s -w" -a -gcflags=all=-l -trimpath all: clean build build: - ${GOOPTS} go build ${BUILDOPTS} -o aleesa-irc-go types.go globals.go collection.go irc-user-mode.go \ - settings-db-util.go util.go msg-parsers.go aleesa-irc-go-lib.go main.go + ${GOOPTS} go build ${BUILDOPTS} -o aleesa-irc-go . clean: go clean diff --git a/aleesa-irc-go-lib.go b/aleesa-irc-go-lib.go index d8f2afa..2fdf786 100644 --- a/aleesa-irc-go-lib.go +++ b/aleesa-irc-go-lib.go @@ -11,6 +11,7 @@ import ( irc "github.com/thoj/go-ircevent" ) +// TODO: Implement 005 REPL_BOUNCE // ircClientInit горутинка для работы с протоколом irc func ircClientRun() { for { @@ -47,7 +48,33 @@ func ircClientRun() { ircClient.UseSASL = true } - // Навесим коллбэков на некоторые ответы сервера на наши запросы + // Навесим коллбэков на некоторые ответы сервера на наши запросы. + + // 001 RPL_WELCOME уже есть в github.com/thoj/go-ircevent/irc_callback.go. + // Сделаем уже что-то полезное! Motd нам уже прислали и теперь можно авторизоваться и джойниться + ircClient.AddCallback("004", func(e *irc.Event) { + // Из это строки мы можем узнать, какие флаги для user MODE и channem MODE можно навешивать. + // Как минимум, эта строка нам нужна, чтобы выяснить, можем ли мы взять себе +B, мы же бот :). + log.Debugf("004 RPL_MYINFO, %s", e.Raw) + + e.Connection.Lock() + + // Формат строки с MODE-ами https://datatracker.ietf.org/doc/html/rfc2812#section-5.1 . + log.Debugf("Add %s to list of available user modes", e.Arguments[3]) + + for _, mode := range strings.Split(e.Arguments[3], "") { + availableUserModes.Set(mode, true) + } + + log.Debugf("Add %s to list available channel modes", e.Arguments[4]) + + for _, mode := range strings.Split(e.Arguments[4], "") { + availableChanModes.Set(mode, true) + } + + e.Connection.Unlock() + }) + ircClient.AddCallback("319", func(e *irc.Event) { /* Это одна из строк с данными, прилетающая в ответ на запрос whois на определённого юзера * Из этой строки нас интересует, на каких каналах пользователь op (то есть с префиксом @) или имеет voice @@ -83,9 +110,12 @@ func ircClientRun() { } }) + // 433 ERR_NICKNAMEINUSE уже есть в github.com/thoj/go-ircevent/irc_callback.go. + ircClient.AddCallback("353", func(e *irc.Event) { // Это одна из строк данных, прилетающая в ответ на запрос names, на канале log.Debugf("353 RPL_NAMREPLY, %s", e.Raw) + e.Connection.Lock() namesString := e.Arguments[3] channel := e.Arguments[2] @@ -114,6 +144,7 @@ func ircClientRun() { break } } + e.Connection.Unlock() }) ircClient.AddCallback("352", func(e *irc.Event) { @@ -122,6 +153,39 @@ func ircClientRun() { log.Warnf("352 RPL_WHOREPLY, %s", e.Raw) }) + // Если сервер не может прочитать MOTD, то он может вернуть 422 ERR_NOMOTD, тоде самое навесим и туда тоже. + ircClient.AddCallback("376", func(e *irc.Event) { + log.Debugf("376 RPL_ENDOFMOTD %s", e.Raw) + + // Если у нас есть доступный +B возьмём его себе, мы же бот. + botFlag, ok := availableUserModes.Get("B") + + if ok && botFlag { + log.Info("Grabbing +B flag to mark me as bot") + ircClient.Mode(ircClient.GetNick(), "+B") + } else { + if ok { + log.Info("Skip +B flag, server does not support it, noone will know that I am bot") + } else { + log.Info("Skip +B flag, server did not announce modes (yet?), noone will know that I am bot") + } + } + + if !config.Irc.Sasl && config.Irc.Password != "" && !nickIsUsed { + log.Info("Identifying via NickServ") + message := fmt.Sprintf("identify %s %s", config.Irc.Nick, config.Irc.Password) + imChan <- iMsg{ChatId: "NickServ", Text: message} + } + // TODO: wait until +R flag been set? could be implemented with waiting for channel message. + + log.Debug("Trying to join to preconfigured channels") + + for _, channel := range config.Irc.Channels { + log.Infof("Joining to %s channel", channel) + ircClient.Join(channel) + } + }) + // Навесим коллбэков на все возможные и невозможные error status code, которые мы можем получить и сдампим это // дело в лог. https://datatracker.ietf.org/doc/html/rfc1459 и https://datatracker.ietf.org/doc/html/rfc2812 ircClient.AddCallback("401", func(e *irc.Event) { @@ -157,6 +221,39 @@ func ircClientRun() { log.Errorf("421 ERR_UNKNOWNCOMMAND, %s", e.Raw) }) + // Аналогичный коллбэк висит на 376 RPL_ENDOFMOTD. + ircClient.AddCallback("422", func(e *irc.Event) { + log.Debugf("422 ERR_NOMOTD %s", e.Raw) + + // Если у нас есть доступный +B возьмём его себе, мы же бот. + botFlag, ok := availableUserModes.Get("B") + + if ok && botFlag { + log.Info("Grabbing +B flag to mark me as bot") + ircClient.Mode(ircClient.GetNick(), "+B") + } else { + if ok { + log.Info("Skip +B flag, server does not support it, noone will know that I am bot") + } else { + log.Info("Skip +B flag, server did not announce modes (yet?), noone will know that I am bot") + } + } + + if !config.Irc.Sasl && config.Irc.Password != "" && !nickIsUsed { + log.Info("Identifying via NickServ") + message := fmt.Sprintf("identify %s %s", config.Irc.Nick, config.Irc.Password) + imChan <- iMsg{ChatId: "NickServ", Text: message} + } + // TODO: wait until +R flag been set? could be implemented with waiting for channel message. + + log.Debug("Trying to join to preconfigured channels") + + for _, channel := range config.Irc.Channels { + log.Infof("Joining to %s channel", channel) + ircClient.Join(channel) + } + }) + ircClient.AddCallback("431", func(e *irc.Event) { log.Errorf("431 ERR_NONICKNAMEGIVEN, %s", e.Raw) }) @@ -179,6 +276,7 @@ func ircClientRun() { log.Errorf("436 ERR_NICKCOLLISION, %s", e.Raw) }) + // 437: ERR_UNAVAILRESOURCE уже есть в github.com/thoj/go-ircevent/irc_callback.go. ircClient.AddCallback("437", func(e *irc.Event) { log.Errorf("437 ERR_UNAVAILRESOURCE, %s", e.Raw) }) @@ -294,22 +392,6 @@ func ircClientRun() { log.Errorf("502 ERR_USERSDONTMATCH, %s", e.Raw) }) - // Сделаем уже что-то полезное! Motd нам уже прислали и теперь можно авторизоваться и джойниться - ircClient.AddCallback("001", func(e *irc.Event) { - if !config.Irc.Sasl && config.Irc.Password != "" && !nickIsUsed { - log.Info("Identifying via NickServ") - message := fmt.Sprintf("identify %s %s", config.Irc.Nick, config.Irc.Password) - imChan <- iMsg{ChatId: "NickServ", Text: message} - } - - log.Debug("Trying to join to preconfigured channels") - - for _, channel := range config.Irc.Channels { - log.Infof("Joining to %s channel", channel) - ircClient.Join(channel) - } - }) - // Навесим коллбэков на другие, интересные нам события ircClient.AddCallback("KICK", func(e *irc.Event) { dstNick := e.Arguments[1] diff --git a/globals.go b/globals.go index e951187..4b97883 100644 --- a/globals.go +++ b/globals.go @@ -4,6 +4,9 @@ import ( "context" "os" + "aleesa-irc-go/internal/anycollection" + "aleesa-irc-go/internal/boolcollection" + "github.com/cockroachdb/pebble" "github.com/go-redis/redis/v8" irc "github.com/thoj/go-ircevent" @@ -45,6 +48,12 @@ var msgBucket bucket var settingsDB = make(map[string]*pebble.DB) // "Базюлька" с MODE-ами пользователей на каналах -var userMode = NewCollection() +var userMode = anycollection.NewCollection() + +// "Базюлька" с доступными MODE-ами пользователя +var availableUserModes = boolcollection.NewCollection() + +// "Базюлька" c с доступными MODE-ами каналов +var availableChanModes = boolcollection.NewCollection() /* vim: set ft=go noet ai ts=4 sw=4 sts=4: */ diff --git a/collection.go b/internal/anycollection/anycollection.go similarity index 66% rename from collection.go rename to internal/anycollection/anycollection.go index 9805c97..210a1ef 100644 --- a/collection.go +++ b/internal/anycollection/anycollection.go @@ -1,21 +1,21 @@ -package main +package anycollection import ( "sync" ) -// Collection это sync.Map-ка, хранящая в себе произвольные итемы +// Collection это sync.Map-ка, хранящая в себе произвольные итемы. type Collection struct { items sync.Map close chan struct{} } -// Item представляет собой интерфейс для хранения произвольных данных +// Item представляет собой интерфейс для хранения произвольных данных. type item struct { - data interface{} + data any } -// NewCollection создаёт инстанс коллекции +// NewCollection создаёт инстанс коллекции. func NewCollection() *Collection { c := &Collection{ close: make(chan struct{}), @@ -24,8 +24,8 @@ func NewCollection() *Collection { return c } -// Get достаёт данные по заданному ключу из коллекции -func (collection *Collection) Get(key interface{}) (interface{}, bool) { +// Get достаёт данные по заданному ключу из коллекции. +func (collection *Collection) Get(key any) (any, bool) { obj, exists := collection.items.Load(key) if !exists { @@ -37,16 +37,16 @@ func (collection *Collection) Get(key interface{}) (interface{}, bool) { return item.data, true } -// Set сохраняет данные с заданным ключом в коллекцию -func (collection *Collection) Set(key interface{}, value interface{}) { +// 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 interface{}) bool) { - fn := func(key, value interface{}) bool { +// 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) @@ -55,12 +55,12 @@ func (collection *Collection) Range(f func(key, value interface{}) bool) { collection.items.Range(fn) } -// Delete удаляет ключ и значение из коллекции данных -func (collection *Collection) Delete(key interface{}) { +// Delete удаляет ключ и значение из коллекции данных. +func (collection *Collection) Delete(key any) { collection.items.Delete(key) } -// Close очищает и высвобождает ресурсы, занятые коллекцией +// Close очищает и высвобождает ресурсы, занятые коллекцией. func (collection *Collection) Close() { collection.close <- struct{}{} collection.items = sync.Map{} diff --git a/internal/boolcollection/boolcollection.go b/internal/boolcollection/boolcollection.go new file mode 100644 index 0000000..e2654b9 --- /dev/null +++ b/internal/boolcollection/boolcollection.go @@ -0,0 +1,58 @@ +package boolcollection + +import ( + "sync" +) + +// Collection это sync.Map-ка, хранящая в себе итемы типа bool. +type Collection struct { + items sync.Map + close chan struct{} +} + +// Item представляет собой интерфейс для хранения данных типа bool. +type item struct { + data bool +} + +// NewCollection создаёт инстанс коллекции. +func NewCollection() *Collection { + c := &Collection{ + close: make(chan struct{}), + } + + return c +} + +// Get достаёт данные по заданному ключу из коллекции. +func (collection *Collection) Get(key string) (bool, bool) { + obj, exists := collection.items.Load(key) + + if !exists { + return false, false + } + + item := obj.(item) + + return item.data, true +} + +// Set сохраняет данные с заданным ключом в коллекцию. +func (collection *Collection) Set(key string, value bool) { + collection.items.Store(key, item{ + data: value, + }) +} + +// Delete удаляет ключ и значение из коллекции данных. +func (collection *Collection) Delete(key string) { + 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: */ diff --git a/msg-parsers.go b/msg-parsers.go index cc07f91..6c6bf01 100644 --- a/msg-parsers.go +++ b/msg-parsers.go @@ -361,8 +361,6 @@ func ircMsgParser(channel string, nick string, user string, source string, msg s log.Debugf("Sent msg to redis channel %s: %s", config.Redis.Channel, string(data)) } } - - return } // redisMsgParser парсит json-чики прилетевшие из REDIS-ки, причём, json-чики должны быть относительно валидными @@ -443,8 +441,6 @@ func redisMsgParser(msg string) { imChan <- iMsg{ChatId: j.Chatid, Text: message} } } - - return } /* vim: set ft=go noet ai ts=4 sw=4 sts=4: */ diff --git a/util.go b/util.go index 2e39366..35d9ee9 100644 --- a/util.go +++ b/util.go @@ -108,7 +108,7 @@ func readConfig() { log.Infof("Irc port is not defined in config, using 6667") } - if sampleConfig.Irc.Ssl != true { + if !sampleConfig.Irc.Ssl { sampleConfig.Irc.SslVerify = false }