0

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
This commit is contained in:
eleksir
2025-03-30 15:41:46 +03:00
parent 4ca2fffb33
commit 8c24a1d8c6
7 changed files with 184 additions and 40 deletions

View File

@@ -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

View File

@@ -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]

View File

@@ -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: */

View File

@@ -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{}

View File

@@ -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: */

View File

@@ -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: */

View File

@@ -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
}