From 75e337e64e7ed3e3ab91c7a834765c050ba4845c Mon Sep 17 00:00:00 2001 From: eleksir Date: Wed, 24 Jul 2024 10:37:32 +0300 Subject: [PATCH] [WIP] Implement cmdParser(), fix some linter warnings --- config.go | 10 +-- globals.go | 2 +- msg-parsers.go | 179 ++++++++++++++++++++++++++++++++++++++++---- settings-db-util.go | 24 ++++-- types.go | 39 ++++++---- 5 files changed, 210 insertions(+), 44 deletions(-) diff --git a/config.go b/config.go index a8c5ade..2d41379 100644 --- a/config.go +++ b/config.go @@ -20,7 +20,7 @@ func parseConfig(path string) (myConfig, error) { // Конфиг-файл длинноват для конфига, попробуем следующего кандидата. if fileInfo.Size() > 65535 { - err := fmt.Errorf("Config file %s is too long for config, skipping", path) + err := fmt.Errorf("config file %s is too long for config, skipping", path) return myConfig{}, err } @@ -70,13 +70,13 @@ func parseConfig(path string) (myConfig, error) { } if sampleConfig.Redis.Channel == "" { - err := fmt.Errorf("Channel field in config file %s must be set", path) + err := fmt.Errorf("channel field in config file %s must be set", path) return myConfig{}, err } if sampleConfig.Redis.MyChannel == "" { - err := fmt.Errorf("My_channel field in config file %s must be set", path) + err := fmt.Errorf("my_channel field in config file %s must be set", path) return myConfig{}, err } @@ -92,7 +92,7 @@ func parseConfig(path string) (myConfig, error) { // sampleConfig.Log = "" if not set if sampleConfig.Csign == "" { - err := fmt.Errorf("Csign field in config file %s must be set", path) + err := fmt.Errorf("csign field in config file %s must be set", path) return myConfig{}, err } @@ -102,7 +102,7 @@ func parseConfig(path string) (myConfig, error) { } if sampleConfig.DataDir == "" { - return myConfig{}, fmt.Errorf("Data_dir field in config file %s must be set", path) + return myConfig{}, fmt.Errorf("data_dir field in config file %s must be set", path) } return sampleConfig, err diff --git a/globals.go b/globals.go index 9f521b6..5372ed9 100644 --- a/globals.go +++ b/globals.go @@ -22,7 +22,7 @@ var subscriber *redis.PubSub // Main context. var ctx = context.Background() -// Ставится в true, если мы получили сигнал на выключение +// Ставится в true, если мы получили сигнал на выключение. var shutdown = false // Канал, в который приходят уведомления для хэндлера сигналов от траппера сигналов. diff --git a/msg-parsers.go b/msg-parsers.go index 1b95355..2eb8972 100644 --- a/msg-parsers.go +++ b/msg-parsers.go @@ -237,15 +237,20 @@ func telegramMsgParser(msg *echotron.Update) { // TODO: если человек ответил на авто-приветствие, это надо обнаруживать и пропускать. - // Если фраза является командой - засылаем её в парсер команд. - if msg.Message.Text[0:len(config.Csign)] == config.Csign { - if err := cmdParser(msg.Message.Text); err != nil { - log.Errorf( - "Unable to parse message from telegram api as a command. Message was: %s, error: %s", - msg.Message.Text, - err, - ) - } + // Pасылаем сообщение в парсер команд. + res, err := cmdParser(me, msg) + + if err != nil { + log.Errorf( + "Unable to parse message from telegram api as a command. Message was: %s, error: %s", + msg.Message.Text, + err, + ) + } + + // Если сообщение - команда, то на этом наши приключения закончились. + if res { + return } // Здесь же, если фраза обращена к боту (ник или имя), выставляем флажок, что надо ответить @@ -354,10 +359,130 @@ func telegramMsgParser(msg *echotron.Update) { } //nolint:wsl } -func cmdParser(cmd string) error { - var err error +// cmdParser разбирает сообщение как команду и засылает его в роутер, попутно обкладывая корректными значениями +// параметров. Возвращает true если это была команда. +func cmdParser(me echotron.APIResponseUser, cmd *echotron.Update) (bool, error) { + var ( + err error + smsg sMsg + bingo bool + ) - return err + smsg.From = "telegram" + smsg.Plugin = "telegram" + smsg.Chatid = fmt.Sprintf("%d", cmd.Message.Chat.ID) + smsg.Userid = fmt.Sprintf("%d", cmd.Message.From.ID) + smsg.Message = cmd.Message.Text + smsg.Misc.Answer = 1 + + // Тоже самое можно проставить выковырнув значение из cmd.Message.Chat.Type, где group/supergroup/channel - это + // public, а остальное - private, но по id проще/быстрее, хоть это и хак. + if cmd.Message.Chat.ID >= 0 { + smsg.Mode = "private" + } else { + smsg.Mode = "public" + } + + smsg.Misc.Botnick = ConstructPartialUserUsername(me.Result) + + // Предполагаем, что длина cmd.Message.Text всегда больше или равна длине config.Csign. + if cmd.Message.Text[0:len(config.Csign)] == config.Csign { + // Повторно проверяем, что текст является простой командой. + + command := cmd.Message.Text[len(config.Csign):] + + // TODO: поддержать команды ver version версия, help, admin и admin *. + + // Команды в одно слово. + 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", "улитка", "tits", "boobs", "tities", "boobies", + "сиси", "сисечки", "butt", "booty", "ass", "попа", "попка", "xkcd", "dig", "копать", "fish", "fishing", + "рыба", "рыбка", "рыбалка", "karma", "карма", "fuck", + } + + for _, c := range cmds { + if command == c { + bingo = true + + break + } + } + + // Не нашлось команды в одно слово. Поищем команды с одним аргументом. + if !bingo { + cmds = []string{ + "w", "п", "weather", "погода", "погодка", "погадка", "karma", "карма", + } + + for _, c := range cmds { + cRe := fmt.Sprintf("^%s ", c) + r := regexp.MustCompile(cRe) + + if r.MatchString(command) { + bingo = true + + break + } + } + } + + // Что-то странное пришло. Залоггируем и ничего делать не будем. Просто свалим. + if !bingo { + log.Infof( + "Strange command from %s %s: %s", + ConstructFullChatName(&cmd.Message.Chat), + ConstructFullUserName(cmd.Message.From), + cmd.Message.Text, + ) + + return true, err + } + + // Для некоторых команд надо подсвечивать имя пользователя в ответе. + cmds = []string{ + "dig", "копать", "fish", "fishing", "рыба", "рыбка", "рыбалка", + } + + for _, c := range cmds { + // В vscode возникает ошибка при использовании if-а. + switch { + case command == c: + smsg.Misc.Username = ConstructTelegramHighlightName(cmd.Message.From) + smsg.Misc.Msgformat = 1 + + break + } + } + + // Если у нас супергруппа с тредами, надо проставить thread id. + if cmd.Message.Chat.Type == "supergroup" && cmd.Message.IsTopicMessage && cmd.Message.ThreadID != 0 { + smsg.ThreadID = fmt.Sprintf("%d", cmd.Message.ThreadID) + } + + // Отправляем сообщение в редиску. + data, err := json.Marshal(smsg) + + if err != nil { + log.Warnf("Unable to to serialize message for redis: %s", err) + + return true, err + } + + // Заталкиваем наш json в редиску. + if err := redisClient.Publish(ctx, config.Redis.Channel, data).Err(); err != nil { + log.Warnf("Unable to send data to redis channel %s: %s", config.Redis.Channel, err) + } else { + log.Debugf("Sent msg to redis channel %s: %s", config.Redis.Channel, string(data)) + } + + return true, err + } + + return false, err } // Censor парсит сообщения в поисках непотребных данных и если он их находит, то сообщение удаляется. @@ -386,7 +511,7 @@ func Censor(msg *echotron.Update) bool { return result } -// ConstructFullUserName выковыривает из сообщения полный username, в формате @username FirstName LastName (id) +// ConstructFullUserName выковыривает из сообщения полный username, в формате @username FirstName LastName (id). func ConstructFullUserName(u *echotron.User) string { user := fmt.Sprintf("(%d)", u.ID) @@ -405,7 +530,7 @@ func ConstructFullUserName(u *echotron.User) string { return user } -// ConstructFullChatName выковыривает из сообщения полный username чата, в формате @username FirstName LastName (id) +// ConstructFullChatName выковыривает из сообщения полный username чата, в формате @username FirstName LastName (id). func ConstructFullChatName(c *echotron.Chat) string { chat := fmt.Sprintf("(%d)", c.ID) @@ -487,4 +612,30 @@ func ConstructUserFirstLastName(u *echotron.User) string { return user } +// ConstructTelegramHighlightName генерирует имя пользователя, которое триггерит на стороне клиент ивент меншена +// определённого пользователя. +func ConstructTelegramHighlightName(u *echotron.User) string { + var ( + username string + link string + ) + + link = fmt.Sprintf("tg://user?id=%d", u.ID) + + // Тут мы предполагаем, что как минимум либо firstname либо lastname либо username всегда есть. По сути так оно и + // должно быть. + switch { + case u.FirstName != "" && u.LastName != "": + username = fmt.Sprintf("%s %s", u.FirstName, u.LastName) + case u.FirstName != "": + username = u.FirstName + case u.LastName != "": + username = u.LastName + default: + username = u.Username + } + + return fmt.Sprintf("[%s](%s)", username, link) +} + /* vim: set ft=go noet ai ts=4 sw=4 sts=4: */ diff --git a/settings-db-util.go b/settings-db-util.go index 50def61..5c6e40b 100644 --- a/settings-db-util.go +++ b/settings-db-util.go @@ -13,8 +13,10 @@ import ( // StoreKV сохраняет в указанной бд ключ и значение. func StoreKV(db *pebble.DB, key string, value string) error { - var kArray = []byte(key) - var vArray = []byte(value) + var ( + kArray = []byte(key) + vArray = []byte(value) + ) err := db.Set(kArray, vArray, pebble.Sync) @@ -23,10 +25,12 @@ func StoreKV(db *pebble.DB, key string, value string) error { // FetchV достаёт значение по ключу. func FetchV(db *pebble.DB, key string) (string, error) { - var kArray = []byte(key) + var ( + kArray = []byte(key) - var vArray []byte - var valueString = "" + vArray []byte + valueString = "" + ) vArray, closer, err := db.Get(kArray) @@ -58,6 +62,7 @@ func GetSetting(chatID string, setting string) string { if err != nil { log.Errorf("Unable to open settings db, %s: %s\n", database, err) + return "" } } @@ -84,9 +89,11 @@ func GetSetting(chatID string, setting string) string { // saveSetting сохраняет настройку в БД с настройками. func GaveSetting(chatID string, setting string, value string) error { - var chatHash = sha256.Sum256([]byte(chatID)) - var database = fmt.Sprintf("settings_db/%x", chatHash) - var err error + var ( + chatHash = sha256.Sum256([]byte(chatID)) + database = fmt.Sprintf("settings_db/%x", chatHash) + err error + ) // Если БД не открыта, откроем её. if _, ok := settingsDB[database]; !ok { @@ -100,6 +107,7 @@ func GaveSetting(chatID string, setting string, value string) error { if err != nil { log.Errorf("Unable to open settings db, %s: %s\n", database, err) + return err } } diff --git a/types.go b/types.go index b118ce8..3dbe4f9 100644 --- a/types.go +++ b/types.go @@ -22,32 +22,39 @@ type myConfig struct { // rMsg структура, описывающая формат входящего сообщения из pubsub-канала redis-ки. type rMsg struct { - From string `json:"from,omitempty"` - Chatid string `json:"chatid,omitempty"` - Userid string `json:"userid,omitempty"` - Message string `json:"message,omitempty"` - Plugin string `json:"plugin,omitempty"` - Mode string `json:"mode,omitempty"` - Misc 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"` + // В этом поле мы пишем 1, если бэкэнд должен в ответе форматировать сообщение в MarkdownV2, что какбэ + // не оч хорошо, так как мы выносим особенности реализации бэкэнда сюда. + Msgformat int64 `json:"msg_format,omitempty"` + // Судя по перловой реализации, сюда всегда попадает [$fullname](tg://user?id=$userid). + // Согласно докуметации, и перловой реализации бота, мы проставляем это поле тогда, когда мы точно знаем, что в + // бэкэнде оно используется. + Username string `json:"username,omitempty"` } `json:"Misc"` } // sMsg структура, описывающая исходящее сообщение в pubsub-канал redis-ки. type sMsg struct { - From string `json:"from"` - Chatid string `json:"chatid"` - Userid string `json:"userid"` - Message string `json:"message"` - Plugin string `json:"plugin"` - Mode string `json:"mode"` - Misc struct { + From string `json:"from"` + Chatid string `json:"chatid"` + Userid string `json:"userid"` + ThreadID string `json:"threadid,omitempty"` + 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"`