[WIP] Move to go project layout, remove logrus & co from vendored libs
This commit is contained in:
502
internal/tg/admin.go
Normal file
502
internal/tg/admin.go
Normal file
@@ -0,0 +1,502 @@
|
||||
package tg
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
|
||||
"github.com/NicoNex/echotron/v3"
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
)
|
||||
|
||||
// Admin парсит команды семейства admin. Возвращает true, если команда была опознана как admin.
|
||||
func Admin(msg *echotron.Update) (bool, error) {
|
||||
var (
|
||||
c = msg.Message.Text[len(Config.Csign):]
|
||||
answer string
|
||||
)
|
||||
|
||||
switch {
|
||||
case c == "admin" || c == "админ":
|
||||
answer = "```\n"
|
||||
answer += fmt.Sprintf("%sadmin censor - показать список состояния типов сообщений\n", Config.Csign)
|
||||
answer += fmt.Sprintf("%sадмин ценз - показать список состояния типов сообщений\n", Config.Csign)
|
||||
answer += fmt.Sprintf("%sadmin censor type # - где 1 - вкл, 0 - выкл цензуры для означенного типа сообщений\n", Config.Csign)
|
||||
answer += fmt.Sprintf("%sадмин ценз тип # - где 1 - вкл, 0 - выкл цензуры для означенного типа сообщений\n", Config.Csign)
|
||||
answer += fmt.Sprintf("%sadmin fortune - показываем ли с утра фортунку для чата\n", Config.Csign)
|
||||
answer += fmt.Sprintf("%sadmin фортунка - показываем ли с утра фортунку для чата\n", Config.Csign)
|
||||
answer += fmt.Sprintf("%sadmin fortune # - где 1 - вкл, 0 - выкл фортунку с утра\n", Config.Csign)
|
||||
answer += fmt.Sprintf("%sadmin фортунка # - где 1 - вкл, 0 - выкл фортунку с утра\n", Config.Csign)
|
||||
answer += fmt.Sprintf("%sadmin greet - приветствуем ли новых участников чата\n", Config.Csign)
|
||||
answer += fmt.Sprintf("%sadmin приветствие - приветствуем ли новых участников чата\n", Config.Csign)
|
||||
answer += fmt.Sprintf("%sadmin greet # - где 1 - вкл, 0 - выкл приветствия новых участников чата\n", Config.Csign)
|
||||
answer += fmt.Sprintf("%sadmin приветствие # - где 1 - вкл, 0 - выкл приветствия новых участников чата\n", Config.Csign)
|
||||
answer += fmt.Sprintf("%sadmin goodbye - прощаемся ли с ушедшими участниками чата\n", Config.Csign)
|
||||
answer += fmt.Sprintf("%sadmin прощание - прощаемся ли с ушедшими участниками чата\n", Config.Csign)
|
||||
answer += fmt.Sprintf("%sadmin goodbye # - где 1 - вкл, 0 - выкл прощания с ушедшими участниками чата\n", Config.Csign)
|
||||
answer += fmt.Sprintf("%sadmin прощание # - где 1 - вкл, 0 - выкл прощания с ушедшими участниками чата\n", Config.Csign)
|
||||
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,
|
||||
)
|
||||
answer += fmt.Sprintf("%sadmin chan_msg - оставляем ли сообщения присланные от имени (других) каналов\n", Config.Csign)
|
||||
answer += fmt.Sprintf("%sadmin chan_msg # - где 1 - оставляем, 0 - удаляем\n", Config.Csign)
|
||||
answer += fmt.Sprintf(
|
||||
"%sadmin ban userid sec - выдаём ban указанному user-у на указанное количество секунд (от 30 сек до 1 года), доступно только создателю чата\n",
|
||||
Config.Csign,
|
||||
)
|
||||
answer += fmt.Sprintf(
|
||||
"%sadmin mute userid sec - выдаём mute указанному user-у на указанное количество секунд (от 30 сек до 1 года), доступно только создателю чата\n",
|
||||
Config.Csign,
|
||||
)
|
||||
answer += fmt.Sprintf(
|
||||
"%sadmin admin mute - разрешено ли обычным админам мьютить участников чата через бота (если бот - админ), (создатель чата всегда может попросить бота-админа замьютить обычного участника чата)\n",
|
||||
Config.Csign,
|
||||
)
|
||||
answer += fmt.Sprintf("%sadmin admin mute # - где 1 - разрешено, 0 - не разрешено\n", Config.Csign)
|
||||
answer += "\n"
|
||||
answer += "Типы сообщений:\naudio voice photo video video_note animation sticker dice game poll document\n"
|
||||
answer += "```"
|
||||
|
||||
case regexp.MustCompile("^(admin|админ)[[:space:]](censor|ценз)$").MatchString(c):
|
||||
if GetSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "VoiceMsg") == "1" {
|
||||
answer += "Тип сообщения voice удаляется\n"
|
||||
} else {
|
||||
answer += "Тип сообщения voice не удаляется\n"
|
||||
}
|
||||
|
||||
if GetSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "AudioMsg") == "1" {
|
||||
answer += "Тип сообщения audio удаляется\n"
|
||||
} else {
|
||||
answer += "Тип сообщения audio не удаляется\n"
|
||||
}
|
||||
|
||||
if GetSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "PhotoMsg") == "1" {
|
||||
answer += "Тип сообщения photo удаляется\n"
|
||||
} else {
|
||||
answer += "Тип сообщения photo не удаляется\n"
|
||||
}
|
||||
|
||||
if GetSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "VideoMsg") == "1" {
|
||||
answer += "Тип сообщения video удаляется\n"
|
||||
} else {
|
||||
answer += "Тип сообщения video не удаляется\n"
|
||||
}
|
||||
|
||||
if GetSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "VideoNoteMsg") == "1" {
|
||||
answer += "Тип сообщения video_note удаляется\n"
|
||||
} else {
|
||||
answer += "Тип сообщения video_note не удаляется\n"
|
||||
}
|
||||
|
||||
if GetSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "AnimationMsg") == "1" {
|
||||
answer += "Тип сообщения animation удаляется\n"
|
||||
} else {
|
||||
answer += "Тип сообщения animation не удаляется\n"
|
||||
}
|
||||
|
||||
if GetSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "StickerMsg") == "1" {
|
||||
answer += "Тип сообщения sticker удаляется\n"
|
||||
} else {
|
||||
answer += "Тип сообщения sticker не удаляется\n"
|
||||
}
|
||||
|
||||
if GetSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "DiceMsg") == "1" {
|
||||
answer += "Тип сообщения dice удаляется\n"
|
||||
} else {
|
||||
answer += "Тип сообщения dice не удаляется\n"
|
||||
}
|
||||
|
||||
if GetSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "GameMsg") == "1" {
|
||||
answer += "Тип сообщения game удаляется\n"
|
||||
} else {
|
||||
answer += "Тип сообщения game не удаляется\n"
|
||||
}
|
||||
|
||||
if GetSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "PollMsg") == "1" {
|
||||
answer += "Тип сообщения poll удаляется\n"
|
||||
} else {
|
||||
answer += "Тип сообщения poll не удаляется\n"
|
||||
}
|
||||
|
||||
if GetSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "DocumentMsg") == "1" {
|
||||
answer += "Тип сообщения document удаляется\n"
|
||||
} else {
|
||||
answer += "Тип сообщения document не удаляется\n"
|
||||
}
|
||||
|
||||
case regexp.MustCompile("^(admin|админ)[[:space:]](censor|ценз)[[:space:]].+$").MatchString(c):
|
||||
// Снова регулярка, это не оч эффективно, но зато просто.
|
||||
// Выхватываем тип сообщения и согласно типу ставим настройку.
|
||||
cmdArray := regexp.MustCompile("[[:space:]]").Split(c, 4)
|
||||
|
||||
switch cmdArray[2] {
|
||||
case "voice":
|
||||
switch cmdArray[3] {
|
||||
case "1":
|
||||
if err := SaveSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "VoiceMsg", "1"); err != nil {
|
||||
return true, err
|
||||
}
|
||||
|
||||
answer += "Теперь сообщения с voice будут оставаться"
|
||||
|
||||
case "0":
|
||||
if err := SaveSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "VoiceMsg", "0"); err != nil {
|
||||
return true, err
|
||||
}
|
||||
|
||||
answer += "Теперь сообщения с voice будут удаляться"
|
||||
|
||||
default:
|
||||
answer += "Не понимаю о чём ты."
|
||||
}
|
||||
|
||||
case "audio":
|
||||
switch cmdArray[3] {
|
||||
case "1":
|
||||
if err := SaveSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "AudioMsg", "1"); err != nil {
|
||||
return true, err
|
||||
}
|
||||
|
||||
answer += "Теперь сообщения с audio будут оставаться"
|
||||
|
||||
case "0":
|
||||
if err := SaveSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "AudioMsg", "0"); err != nil {
|
||||
return true, err
|
||||
}
|
||||
|
||||
answer += "Теперь сообщения с audio будут удаляться"
|
||||
|
||||
default:
|
||||
answer += "Не понимаю о чём ты."
|
||||
}
|
||||
|
||||
case "photo":
|
||||
switch cmdArray[3] {
|
||||
case "1":
|
||||
if err := SaveSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "PhotoMsg", "1"); err != nil {
|
||||
return true, err
|
||||
}
|
||||
|
||||
answer += "Теперь сообщения с photo будут оставаться"
|
||||
|
||||
case "0":
|
||||
if err := SaveSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "PhotoMsg", "0"); err != nil {
|
||||
return true, err
|
||||
}
|
||||
|
||||
answer += "Теперь сообщения с photo будут удаляться"
|
||||
|
||||
default:
|
||||
answer += "Не понимаю о чём ты."
|
||||
}
|
||||
|
||||
case "video":
|
||||
switch cmdArray[3] {
|
||||
case "1":
|
||||
if err := SaveSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "VideoMsg", "1"); err != nil {
|
||||
return true, err
|
||||
}
|
||||
|
||||
answer += "Теперь сообщения с video будут оставаться"
|
||||
|
||||
case "0":
|
||||
if err := SaveSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "VideoMsg", "0"); err != nil {
|
||||
return true, err
|
||||
}
|
||||
|
||||
answer += "Теперь сообщения с video будут удаляться"
|
||||
|
||||
default:
|
||||
answer += "Не понимаю о чём ты."
|
||||
}
|
||||
|
||||
case "video_note":
|
||||
switch cmdArray[3] {
|
||||
case "1":
|
||||
if err := SaveSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "VideoNoteMsg", "1"); err != nil {
|
||||
return true, err
|
||||
}
|
||||
|
||||
answer += "Теперь сообщения с video_note будут оставаться"
|
||||
|
||||
case "0":
|
||||
if err := SaveSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "VideoNoteMsg", "0"); err != nil {
|
||||
return true, err
|
||||
}
|
||||
|
||||
answer += "Теперь сообщения с video_note будут удаляться"
|
||||
|
||||
default:
|
||||
answer += "Не понимаю о чём ты."
|
||||
}
|
||||
|
||||
case "animation":
|
||||
switch cmdArray[3] {
|
||||
case "1":
|
||||
if err := SaveSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "AnimationMsg", "1"); err != nil {
|
||||
return true, err
|
||||
}
|
||||
|
||||
answer += "Теперь сообщения с animation будут оставаться"
|
||||
|
||||
case "0":
|
||||
if err := SaveSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "AnimationMsg", "0"); err != nil {
|
||||
return true, err
|
||||
}
|
||||
|
||||
answer += "Теперь сообщения с animation будут удаляться"
|
||||
|
||||
default:
|
||||
answer += "Не понимаю о чём ты."
|
||||
}
|
||||
|
||||
case "sticker":
|
||||
switch cmdArray[3] {
|
||||
case "1":
|
||||
if err := SaveSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "StickerMsg", "1"); err != nil {
|
||||
return true, err
|
||||
}
|
||||
|
||||
answer += "Теперь сообщения с sticker будут оставаться"
|
||||
|
||||
case "0":
|
||||
if err := SaveSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "StickerMsg", "0"); err != nil {
|
||||
return true, err
|
||||
}
|
||||
|
||||
answer += "Теперь сообщения с sticker будут удаляться"
|
||||
|
||||
default:
|
||||
answer += "Не понимаю о чём ты."
|
||||
}
|
||||
|
||||
case "dice":
|
||||
switch cmdArray[3] {
|
||||
case "1":
|
||||
if err := SaveSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "DiceMsg", "1"); err != nil {
|
||||
return true, err
|
||||
}
|
||||
|
||||
answer += "Теперь сообщения с dice будут оставаться"
|
||||
|
||||
case "0":
|
||||
if err := SaveSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "diceMsg", "0"); err != nil {
|
||||
return true, err
|
||||
}
|
||||
|
||||
answer += "Теперь сообщения с dice будут удаляться"
|
||||
|
||||
default:
|
||||
answer += "Не понимаю о чём ты."
|
||||
}
|
||||
|
||||
case "game":
|
||||
switch cmdArray[3] {
|
||||
case "1":
|
||||
if err := SaveSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "GameMsg", "1"); err != nil {
|
||||
return true, err
|
||||
}
|
||||
|
||||
answer += "Теперь сообщения с game будут оставаться"
|
||||
|
||||
case "0":
|
||||
if err := SaveSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "GameMsg", "0"); err != nil {
|
||||
return true, err
|
||||
}
|
||||
|
||||
answer += "Теперь сообщения с game будут удаляться"
|
||||
|
||||
default:
|
||||
answer += "Не понимаю о чём ты."
|
||||
}
|
||||
|
||||
case "poll":
|
||||
switch cmdArray[3] {
|
||||
case "1":
|
||||
if err := SaveSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "PollMsg", "1"); err != nil {
|
||||
return true, err
|
||||
}
|
||||
|
||||
answer += "Теперь сообщения с poll будут оставаться"
|
||||
|
||||
case "0":
|
||||
if err := SaveSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "PollMsg", "0"); err != nil {
|
||||
return true, err
|
||||
}
|
||||
|
||||
answer += "Теперь сообщения с poll будут удаляться"
|
||||
|
||||
default:
|
||||
answer += "Не понимаю о чём ты."
|
||||
}
|
||||
|
||||
case "document":
|
||||
switch cmdArray[3] {
|
||||
case "1":
|
||||
if err := SaveSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "DocumentMsg", "1"); err != nil {
|
||||
return true, err
|
||||
}
|
||||
|
||||
answer += "Теперь сообщения с document будут оставаться"
|
||||
|
||||
case "0":
|
||||
if err := SaveSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "DocumentMsg", "0"); err != nil {
|
||||
return true, err
|
||||
}
|
||||
|
||||
answer += "Теперь сообщения с document будут удаляться"
|
||||
|
||||
default:
|
||||
answer += "Не понимаю о чём ты."
|
||||
}
|
||||
|
||||
default:
|
||||
answer += "Не понимаю о чём ты."
|
||||
}
|
||||
|
||||
case regexp.MustCompile("^(admin|админ)[[:space:]](fortune|фортунка)$").MatchString(c):
|
||||
if GetSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "FortuneMsg") == "1" {
|
||||
answer += "Фортунки с утра показываются"
|
||||
} else {
|
||||
answer += "Фортунки с утра не показываются"
|
||||
}
|
||||
|
||||
case regexp.MustCompile("^(admin|админ)[[:space:]](fortune|фортунка)[[:space:]].*$").MatchString(c):
|
||||
// Снова регулярка, это не оч эффективно, но зато просто.
|
||||
cmdArray := regexp.MustCompile("[[:space:]]").Split(c, 3)
|
||||
|
||||
// TODO: На самом деле надо сохранять список чятиков, в которые надо отправлять сообщения. Из этой базы
|
||||
// мы не сможем вытащить список чятиков.
|
||||
switch cmdArray[2] {
|
||||
case "1":
|
||||
if err := SaveSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "FortuneMsg", "1"); err != nil {
|
||||
return true, err
|
||||
}
|
||||
|
||||
answer += "Теперь сообщения с фортункой будут отправляться с утра"
|
||||
|
||||
case "0":
|
||||
if err := SaveSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "FortuneMsg", "0"); err != nil {
|
||||
return true, err
|
||||
}
|
||||
|
||||
answer += "Теперь сообщения с фортункой не будут отправляться с утра"
|
||||
|
||||
default:
|
||||
answer += "Не понимаю о чём ты."
|
||||
}
|
||||
|
||||
case regexp.MustCompile("^(admin|админ)[[:space:]](greet|приветствие)$").MatchString(c):
|
||||
if GetSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "GreetMsg") == "1" {
|
||||
answer += "Приветствуем вновьприбывших участников чатика"
|
||||
} else {
|
||||
answer += "Не приветствуем вновьприбывших участников чатика"
|
||||
}
|
||||
|
||||
case regexp.MustCompile("^(admin|админ)[[:space:]](greet|приветствие)[[:space:]].*$").MatchString(c):
|
||||
// Снова регулярка, это не оч эффективно, но зато просто.
|
||||
cmdArray := regexp.MustCompile("[[:space:]]").Split(c, 3)
|
||||
|
||||
// TODO: На самом деле надо сохранять список чятиков, в которые надо отправлять сообщения. Из этой базы
|
||||
// мы не сможем вытащить список чятиков.
|
||||
switch cmdArray[2] {
|
||||
case "1":
|
||||
if err := SaveSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "GreetMsg", "1"); err != nil {
|
||||
return true, err
|
||||
}
|
||||
|
||||
answer += "Будем приветствовать вновьприбывших участников чата"
|
||||
|
||||
case "0":
|
||||
if err := SaveSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "GreetMsg", "0"); err != nil {
|
||||
return true, err
|
||||
}
|
||||
|
||||
answer += "Не будем приветствовать вновьприбывших участников чата"
|
||||
|
||||
default:
|
||||
answer += "Не понимаю, о чём ты."
|
||||
}
|
||||
|
||||
case regexp.MustCompile("^(admin|админ)[[:space:]](goodbye|прощание)$").MatchString(c):
|
||||
if GetSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "GoodbyeMsg") == "1" {
|
||||
answer += "Прощаемся с ушедшими участниками чатика"
|
||||
} else {
|
||||
answer += "Не прощаемся с ушедшими участниками чатика"
|
||||
}
|
||||
|
||||
case regexp.MustCompile("^(admin|админ)[[:space:]](goodbye|прощание)[[:space:]].*$").MatchString(c):
|
||||
// Снова регулярка, это не оч эффективно, но зато просто.
|
||||
cmdArray := regexp.MustCompile("[[:space:]]").Split(c, 3)
|
||||
|
||||
switch cmdArray[2] {
|
||||
case "1":
|
||||
if err := SaveSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "GoodbyeMsg", "1"); err != nil {
|
||||
return true, err
|
||||
}
|
||||
|
||||
answer += "Будем прощаться с ушедшими участниками чата"
|
||||
|
||||
case "0":
|
||||
if err := SaveSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "GoodbyeMsg", "0"); err != nil {
|
||||
return true, err
|
||||
}
|
||||
|
||||
answer += "Не будем прощаться с ушедшими участниками чата"
|
||||
|
||||
default:
|
||||
answer += "Не понимаю, о чём ты."
|
||||
}
|
||||
|
||||
case regexp.MustCompile("^(admin|админ)[[:space:]](chan_msg)[[:space:]].*$").MatchString(c):
|
||||
// Снова регулярка, это не оч эффективно, но зато просто.
|
||||
cmdArray := regexp.MustCompile("[[:space:]]").Split(c, 3)
|
||||
|
||||
switch cmdArray[2] {
|
||||
case "1":
|
||||
if err := SaveSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "ChanMsg", "1"); err != nil {
|
||||
return true, err
|
||||
}
|
||||
|
||||
answer += "Будем удалять сообщения, написанные от имени канала"
|
||||
|
||||
case "0":
|
||||
if err := SaveSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "ChanMsg", "0"); err != nil {
|
||||
return true, err
|
||||
}
|
||||
|
||||
answer += "Не будем удалять сообщения, написанные от имени канала"
|
||||
|
||||
default:
|
||||
answer += "Не понимаю, о чём ты."
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := tg.SendMessage(
|
||||
answer,
|
||||
msg.Message.Chat.ID,
|
||||
&echotron.MessageOptions{ParseMode: "MarkdownV2"},
|
||||
)
|
||||
|
||||
if err != nil || !resp.Ok {
|
||||
// TODO: поддержать миграцию группы в супергруппу, поддержать вариант, когда бот замьючен.
|
||||
// Красиво оформить ошибку, с полями итд, как tracedump, только ошибка.
|
||||
// N.B. тут может быть сообщение о том, что группа превратилась в супергруппу, или что бот не имеет прав писать
|
||||
// сообщения в чятик. Это надо бы отслеживать и хэндлить.
|
||||
return true, fmt.Errorf(
|
||||
"unable to send message to telegram api: %w\nResponse dump: %s",
|
||||
err,
|
||||
spew.Sdump(resp),
|
||||
)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
||||
121
internal/tg/censor.go
Normal file
121
internal/tg/censor.go
Normal file
@@ -0,0 +1,121 @@
|
||||
package tg
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/NicoNex/echotron/v3"
|
||||
)
|
||||
|
||||
// Censor парсит сообщения в поисках непотребных данных и если он их находит, то сообщение удаляется.
|
||||
// Непотребными могут быть аудиосообщения, аудиофайлы, видеосообщения, сообщения от имени других каналов итп.
|
||||
// Это могут настроить админы чятика через команду !admin censor.
|
||||
func Censor(msg *echotron.Update) bool {
|
||||
result := false
|
||||
|
||||
switch {
|
||||
case GetSetting(fmt.Sprintf("%d", msg.ChatID()), "VoiceMsg") == "1":
|
||||
result = true
|
||||
|
||||
// Предполагаем, что у voice-ов здесь всегда не ноль.
|
||||
if msg.Message.Voice.Duration != 0 {
|
||||
delMsg(msg)
|
||||
|
||||
result = true
|
||||
}
|
||||
|
||||
case GetSetting(fmt.Sprintf("%d", msg.ChatID()), "AudioMsg") == "1":
|
||||
// Предполагаем, что у аудио здесь всегда не ноль.
|
||||
if msg.Message.Audio.Duration != 0 {
|
||||
delMsg(msg)
|
||||
|
||||
result = true
|
||||
}
|
||||
|
||||
case GetSetting(fmt.Sprintf("%d", msg.ChatID()), "PhotoMsg") == "1":
|
||||
// Обычное сообщение не содержит фоток.
|
||||
if len(msg.Message.Photo) != 0 {
|
||||
delMsg(msg)
|
||||
|
||||
result = true
|
||||
}
|
||||
|
||||
case GetSetting(fmt.Sprintf("%d", msg.ChatID()), "VideoMsg") == "1":
|
||||
// Предполагаем, что у видео здесь всегда не ноль.
|
||||
if msg.Message.Video.Duration != 0 {
|
||||
delMsg(msg)
|
||||
|
||||
result = true
|
||||
}
|
||||
|
||||
case GetSetting(fmt.Sprintf("%d", msg.ChatID()), "VideoNoteMsg") == "1":
|
||||
// Предполагаем, что у видео-заметки здесь всегда не ноль.
|
||||
if msg.Message.VideoNote.Duration != 0 {
|
||||
delMsg(msg)
|
||||
|
||||
result = true
|
||||
}
|
||||
|
||||
case GetSetting(fmt.Sprintf("%d", msg.ChatID()), "AnimationMsg") == "1":
|
||||
// Предполагаем, что у анимации здесь всегда не ноль.
|
||||
if msg.Message.Animation.Duration != 0 {
|
||||
delMsg(msg)
|
||||
|
||||
result = true
|
||||
}
|
||||
|
||||
case GetSetting(fmt.Sprintf("%d", msg.ChatID()), "StickerMsg") == "1":
|
||||
// Предполагаем, что FileID не пустое только у стикера.
|
||||
if msg.Message.Sticker.FileID != "" {
|
||||
delMsg(msg)
|
||||
|
||||
result = true
|
||||
}
|
||||
|
||||
case GetSetting(fmt.Sprintf("%d", msg.ChatID()), "DiceMsg") == "1":
|
||||
// Предполагаем, что Value > 0 только у дайса.
|
||||
if msg.Message.Dice.Value != 0 {
|
||||
delMsg(msg)
|
||||
|
||||
result = true
|
||||
}
|
||||
|
||||
case GetSetting(fmt.Sprintf("%d", msg.ChatID()), "GameMsg") == "1":
|
||||
// Предполагаем, что title только у game-а.
|
||||
if msg.Message.Game.Title != "" {
|
||||
delMsg(msg)
|
||||
|
||||
result = true
|
||||
}
|
||||
|
||||
case GetSetting(fmt.Sprintf("%d", msg.ChatID()), "PollMsg") == "1":
|
||||
// Предполагаем, что title только у game-а.
|
||||
if msg.Message.Poll.Question != "" {
|
||||
delMsg(msg)
|
||||
|
||||
result = true
|
||||
}
|
||||
|
||||
case GetSetting(fmt.Sprintf("%d", msg.ChatID()), "DocumentMsg") == "1":
|
||||
// Предполагаем, что FileID есть только у document-а.
|
||||
if msg.Message.Document.FileID != "" {
|
||||
delMsg(msg)
|
||||
|
||||
result = true
|
||||
}
|
||||
|
||||
// Некоторые рекламные товарищи пытаются срать своими каналами в чятик это тоже можно зацензурить ботом и это
|
||||
// пидорство он будет удалять asap.
|
||||
// 136817688 - это специальный id пользователя, который принимает облик канала, на него можно нажать и попасть
|
||||
// на рекламируемый канал.
|
||||
case GetSetting(fmt.Sprintf("%d", msg.ChatID()), "ChanMsg") == "1":
|
||||
if msg.Message.From.ID == 136817688 {
|
||||
delMsg(msg)
|
||||
|
||||
result = true
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
||||
144
internal/tg/cmdParser.go
Normal file
144
internal/tg/cmdParser.go
Normal file
@@ -0,0 +1,144 @@
|
||||
package tg
|
||||
|
||||
import (
|
||||
"aleesa-telegram-go/internal/log"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"regexp"
|
||||
|
||||
"github.com/NicoNex/echotron/v3"
|
||||
)
|
||||
|
||||
// cmdParser разбирает сообщение как команду и засылает его в роутер, попутно обкладывая корректными значениями
|
||||
// параметров. Возвращает true если это была команда.
|
||||
func cmdParser(me echotron.APIResponseUser, cmd *echotron.Update) (bool, error) {
|
||||
var (
|
||||
err error
|
||||
smsg sMsg
|
||||
bingo bool
|
||||
)
|
||||
|
||||
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):]
|
||||
|
||||
switch {
|
||||
case command == "помощь" || command == "help":
|
||||
return Help(cmd)
|
||||
|
||||
// Хэндлер команд admin и admin *.
|
||||
case regexp.MustCompile("^(admin|админ)(.+)?$").MatchString(command):
|
||||
return Admin(cmd)
|
||||
}
|
||||
|
||||
// Команды в одно слово.
|
||||
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", "weather", "погода", "w", "п", "погодка", "погадка",
|
||||
}
|
||||
|
||||
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 //nolint: gosimple
|
||||
}
|
||||
}
|
||||
|
||||
// Если у нас супергруппа с тредами, надо проставить 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
|
||||
}
|
||||
|
||||
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
||||
45
internal/tg/globals.go
Normal file
45
internal/tg/globals.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package tg
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
|
||||
"github.com/NicoNex/echotron/v3"
|
||||
"github.com/cockroachdb/pebble"
|
||||
"github.com/go-redis/redis/v8"
|
||||
)
|
||||
|
||||
// Config - это у нас глобальная штука.
|
||||
var Config myConfig
|
||||
|
||||
// To break circular message forwarding we must set some sane default, it can be overridden via config.
|
||||
var forwardMax int64 = 5
|
||||
|
||||
// Объектики клиента-редиски.
|
||||
var RedisClient *redis.Client
|
||||
var Subscriber *redis.PubSub
|
||||
|
||||
// Main context.
|
||||
var ctx = context.Background()
|
||||
|
||||
// Ставится в true, если мы получили сигнал на выключение.
|
||||
var Shutdown = false
|
||||
|
||||
// Канал, в который приходят уведомления для хэндлера сигналов от траппера сигналов.
|
||||
var SigChan = make(chan os.Signal, 1)
|
||||
|
||||
// Мапка с открытыми дескрипторами баз с настройками.
|
||||
var settingsDB = make(map[string]*pebble.DB)
|
||||
|
||||
var tg echotron.API
|
||||
|
||||
// Приветсвенные фразы для новых участников чата.
|
||||
var introduceGreet = [...]string{
|
||||
"Дратути",
|
||||
"Дарована",
|
||||
"Доброе утро, день или вечер",
|
||||
"Добро пожаловать в наше скромное коммунити",
|
||||
"Наше вам с кисточкой тут, на канальчике",
|
||||
}
|
||||
|
||||
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
||||
69
internal/tg/help.go
Normal file
69
internal/tg/help.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package tg
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/NicoNex/echotron/v3"
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
)
|
||||
|
||||
// Help Выводит в чат сообщение с основными командами бота.
|
||||
func Help(cmd *echotron.Update) (bool, error) {
|
||||
help := "```\n"
|
||||
help += fmt.Sprintf("%shelp | %sпомощь - список команд", Config.Csign, Config.Csign)
|
||||
help += fmt.Sprintf("%sanek | %sанек | %sанекдот - рандомный анекдот с anekdot.ru", Config.Csign, Config.Csign, Config.Csign)
|
||||
help += fmt.Sprintf("%sbuni - рандомный стрип hapi buni", Config.Csign)
|
||||
help += fmt.Sprintf("%sbunny | %srabbit | %sкролик - кролик", Config.Csign, Config.Csign, Config.Csign)
|
||||
help += fmt.Sprintf("%scat | %sкис - кошечка", Config.Csign, Config.Csign)
|
||||
help += fmt.Sprintf("%scoin | %sмонетка - подбросить монетку - орёл или решка?", Config.Csign, Config.Csign)
|
||||
help += fmt.Sprintf("%sdig | %sкопать - заняться археологией", Config.Csign, Config.Csign)
|
||||
help += fmt.Sprintf("%sdrink | %sпраздник - какой сегодня праздник?", Config.Csign, Config.Csign)
|
||||
help += fmt.Sprintf("%sfish | %sрыба | %sрыбка - порыбачить", Config.Csign, Config.Csign, Config.Csign)
|
||||
help += fmt.Sprintf("%sfishing | %sрыбалка - порыбачить", Config.Csign, Config.Csign)
|
||||
help += fmt.Sprintf("%sf | %sф - рандомная фраза из сборника цитат fortune_mod", Config.Csign, Config.Csign)
|
||||
help += fmt.Sprintf("%sfortune | %sфортунка - рандомная фраза из сборника цитат fortune_mod", Config.Csign, Config.Csign)
|
||||
help += fmt.Sprintf("%sfox | %sлис - лисичка", Config.Csign, Config.Csign)
|
||||
help += fmt.Sprintf("%sfriday | %sпятница - а не пятница ли сегодня?", Config.Csign, Config.Csign)
|
||||
help += fmt.Sprintf("%sfrog | %sлягушка - лягушка", Config.Csign, Config.Csign)
|
||||
help += fmt.Sprintf("%shorse | %sлошадка - лошадка", Config.Csign, Config.Csign)
|
||||
help += fmt.Sprintf("%slat | %sлат - сгенерить фразу из крылатых латинских выражений", Config.Csign, Config.Csign)
|
||||
help += fmt.Sprintf("%smonkeyuser - рандомный стрип MonkeyUser", Config.Csign)
|
||||
help += fmt.Sprintf("%sowl | %sсова | %sсыч - сова", Config.Csign, Config.Csign, Config.Csign)
|
||||
help += fmt.Sprintf("%sproverb | %sпословица - рандомная русская пословица", Config.Csign, Config.Csign)
|
||||
help += fmt.Sprintf("%sping | %sпинг - попинговать бота", Config.Csign, Config.Csign)
|
||||
help += fmt.Sprintf("%sroll | %sdice | %sкости - бросить кости", Config.Csign, Config.Csign, Config.Csign)
|
||||
help += fmt.Sprintf("%ssnail | %sулитка - улитка", Config.Csign, Config.Csign)
|
||||
help += fmt.Sprintf("%sver | %sversion | %sверсия - что-то про версию ПО", Config.Csign, Config.Csign, Config.Csign)
|
||||
help += fmt.Sprintf("%sw <город> | %sп <город> - погода в указанном городе", Config.Csign, Config.Csign)
|
||||
help += fmt.Sprintf("%sweather <город> - погода в указанном городе", Config.Csign)
|
||||
help += fmt.Sprintf("%sпогода <город> - погода в указанном городе", Config.Csign)
|
||||
help += fmt.Sprintf("%sпогодка <город> - погода в указанном городе", Config.Csign)
|
||||
help += fmt.Sprintf("%sпогадка <город> - погода в указанном городе", Config.Csign)
|
||||
help += fmt.Sprintf("%sxkcd - рандомный стрип с сайта xkcd.ru", Config.Csign)
|
||||
help += fmt.Sprintf("%skarma фраза | %sкарма фраза - посмотреть карму фразы", Config.Csign, Config.Csign)
|
||||
help += "```\n"
|
||||
help += "Но на самом деле я бот больше для общения, чем для исполнения команд.\n"
|
||||
help += "Поговоришь со мной?"
|
||||
|
||||
resp, err := tg.SendMessage(
|
||||
help,
|
||||
cmd.Message.Chat.ID,
|
||||
&echotron.MessageOptions{ParseMode: "MarkdownV2"},
|
||||
)
|
||||
|
||||
if err != nil || !resp.Ok {
|
||||
// TODO: поддержать миграцию группы в супергруппу, поддержать вариант, когда бот замьючен.
|
||||
// Красиво оформить ошибку, с полями итд, как tracedump, только ошибка.
|
||||
// N.B. тут может быть сообщение о том, что группа превратилась в супергруппу, или что бот не имеет прав писать
|
||||
// сообщения в чятик. Это надо бы отслеживать и хэндлить.
|
||||
return true, fmt.Errorf(
|
||||
"unable to send message to telegram api: %w\nResponse dump: %s",
|
||||
err,
|
||||
spew.Sdump(resp),
|
||||
)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
||||
112
internal/tg/parseConfig.go
Normal file
112
internal/tg/parseConfig.go
Normal file
@@ -0,0 +1,112 @@
|
||||
package tg
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"aleesa-telegram-go/internal/log"
|
||||
|
||||
"github.com/hjson/hjson-go"
|
||||
)
|
||||
|
||||
// ParseConfig разбирает и валидирует даденный конфиг.
|
||||
func ParseConfig(path string) (myConfig, error) {
|
||||
fileInfo, err := os.Stat(path)
|
||||
|
||||
// Предполагаем, что файла либо нет, либо мы не можем его прочитать, второе надо бы логгировать, но пока забьём.
|
||||
if err != nil {
|
||||
return myConfig{}, err
|
||||
}
|
||||
|
||||
// Конфиг-файл длинноват для конфига, попробуем следующего кандидата.
|
||||
if fileInfo.Size() > 65535 {
|
||||
err := fmt.Errorf("config file %s is too long for config, skipping", path)
|
||||
|
||||
return myConfig{}, err
|
||||
}
|
||||
|
||||
buf, err := os.ReadFile(path)
|
||||
|
||||
// Не удалось прочитать.
|
||||
if err != nil {
|
||||
return myConfig{}, err
|
||||
}
|
||||
|
||||
// Исходя из документации, hjson какбы умеет парсить "кривой" json, но парсит его в map-ку.
|
||||
// Интереснее на выходе получить структурку: то есть мы вначале конфиг преобразуем в map-ку, затем эту map-ку
|
||||
// сериализуем в json, а потом json превращааем в структурку. Не очень эффективно, но он и не часто требуется.
|
||||
var (
|
||||
sampleConfig myConfig
|
||||
tmp map[string]interface{}
|
||||
)
|
||||
|
||||
err = hjson.Unmarshal(buf, &tmp)
|
||||
|
||||
// Не удалось распарсить.
|
||||
if err != nil {
|
||||
return myConfig{}, err
|
||||
}
|
||||
|
||||
tmpjson, err := json.Marshal(tmp)
|
||||
|
||||
// Не удалось преобразовать map-ку в json.
|
||||
if err != nil {
|
||||
return myConfig{}, err
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(tmpjson, &sampleConfig); err != nil {
|
||||
return myConfig{}, err
|
||||
}
|
||||
|
||||
// Валидируем значения из конфига.
|
||||
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 == "" {
|
||||
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)
|
||||
|
||||
return myConfig{}, err
|
||||
}
|
||||
|
||||
if sampleConfig.Telegram.Token == "" {
|
||||
return myConfig{}, fmt.Errorf("telegram.token field in config file %s must be set", path)
|
||||
}
|
||||
|
||||
if sampleConfig.Loglevel == "" {
|
||||
sampleConfig.Loglevel = "info"
|
||||
}
|
||||
|
||||
// sampleConfig.Log = "" if not set
|
||||
|
||||
if sampleConfig.Csign == "" {
|
||||
err := fmt.Errorf("csign field in config file %s must be set", path)
|
||||
|
||||
return myConfig{}, err
|
||||
}
|
||||
|
||||
if sampleConfig.ForwardsMax == 0 {
|
||||
sampleConfig.ForwardsMax = forwardMax
|
||||
}
|
||||
|
||||
if sampleConfig.DataDir == "" {
|
||||
return myConfig{}, fmt.Errorf("data_dir field in config file %s must be set", path)
|
||||
}
|
||||
|
||||
return sampleConfig, err
|
||||
}
|
||||
|
||||
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
||||
112
internal/tg/redisMsgParser.go
Normal file
112
internal/tg/redisMsgParser.go
Normal file
@@ -0,0 +1,112 @@
|
||||
package tg
|
||||
|
||||
import (
|
||||
"aleesa-telegram-go/internal/log"
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
|
||||
"github.com/NicoNex/echotron/v3"
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
)
|
||||
|
||||
// RedisMsgParser парсит json-чики прилетевшие из REDIS-ки, причём json-чики должны быть относительно валидными.
|
||||
func RedisMsgParser(msg string) {
|
||||
if Shutdown {
|
||||
// Если мы завершаем работу программы, то нам ничего обрабатывать не надо.
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// Validate our j
|
||||
if exist := j.From; exist == "" {
|
||||
log.Warnf("Incorrect msg from redis, no from field: %s", msg)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if exist := j.Chatid; exist == "" {
|
||||
log.Warnf("Incorrect msg from redis, no chatid field: %s", msg)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if exist := j.Userid; exist == "" {
|
||||
log.Warnf("Incorrect msg from redis, no userid field: %s", msg)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if exist := j.Message; exist == "" {
|
||||
log.Warnf("Incorrect msg from redis, no message field: %s", msg)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if exist := j.Plugin; exist == "" {
|
||||
log.Warnf("Incorrect msg from redis, no plugin field: %s", msg)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if exist := j.Mode; exist == "" {
|
||||
log.Warnf("Incorrect msg from redis, no mode field: %s", msg)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// j.Misc.Answer может и не быть, тогда ответа на такое сообщение не будет
|
||||
if j.Misc.Answer == 0 {
|
||||
log.Debug("Field Misc->Answer = 0, skipping message")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// 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 можно не передавать, тогда будет пустая строка
|
||||
|
||||
// Отвалидировались, теперь вернёмся к нашим баранам.
|
||||
|
||||
var opts *echotron.MessageOptions
|
||||
|
||||
chatid, err := strconv.ParseInt(j.Chatid, 10, 64)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("unable to parse message from redis, incorrect chatid field: %s", err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := tg.SendMessage(j.Message, chatid, opts)
|
||||
|
||||
if err != nil || !resp.Ok {
|
||||
// TODO: поддержать миграцию группы в супергруппу, поддержать вариант, когда бот замьючен.
|
||||
// Красиво оформить ошибку, с полями итд, как tracedump, только ошибка.
|
||||
// N.B. тут может быть сообщение о том, что группа превратилась в супергруппу, или что бот не имеет прав писать
|
||||
// сообщения в чятик. Это надо бы отслеживать и хэндлить.
|
||||
log.Errorf("Unable to send message to telegram api: %s", err)
|
||||
log.Errorf("Response dump: %s", spew.Sdump(resp))
|
||||
}
|
||||
}
|
||||
|
||||
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
||||
124
internal/tg/settings-db-util.go
Normal file
124
internal/tg/settings-db-util.go
Normal file
@@ -0,0 +1,124 @@
|
||||
package tg
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
|
||||
"aleesa-telegram-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 = ""
|
||||
)
|
||||
|
||||
vArray, closer, err := db.Get(kArray)
|
||||
|
||||
if err != nil {
|
||||
return valueString, err
|
||||
}
|
||||
|
||||
valueString = string(vArray)
|
||||
err = closer.Close()
|
||||
|
||||
return valueString, err
|
||||
}
|
||||
|
||||
// GetSetting достаёт настройку из БД с настройками.
|
||||
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
|
||||
}
|
||||
|
||||
// SaveSetting сохраняет настройку в БД с настройками.
|
||||
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: */
|
||||
78
internal/tg/sigHandler.go
Normal file
78
internal/tg/sigHandler.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package tg
|
||||
|
||||
import (
|
||||
"os"
|
||||
"syscall"
|
||||
|
||||
"aleesa-telegram-go/internal/log"
|
||||
)
|
||||
|
||||
// SigHandler хэндлер сигналов закрывает все бд, все сетевые соединения и сваливает из приложения.
|
||||
func SigHandler() {
|
||||
var err error
|
||||
|
||||
for {
|
||||
var s = <-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
|
||||
}
|
||||
|
||||
// Чтобы не срать в логи ошибками от редиски, проставим 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")
|
||||
}
|
||||
|
||||
// close all telego stuff.
|
||||
resp, err := tg.LogOut()
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("Unable to send LogOut() to Telegram Bot API server")
|
||||
}
|
||||
|
||||
if !resp.Ok {
|
||||
log.Errorf("Telegram Bot API returns an error on LogOut(): %d, %s", resp.ErrorCode, resp.Description)
|
||||
}
|
||||
|
||||
resp, err = tg.Close()
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("Unable to send Close() to Telegram Bot API server")
|
||||
}
|
||||
|
||||
if !resp.Ok {
|
||||
log.Errorf("Telegram Bot API returns an error on Close(): %d, %s", resp.ErrorCode, resp.Description)
|
||||
}
|
||||
|
||||
if len(settingsDB) > 0 {
|
||||
log.Debug("Closing runtime bot settings db")
|
||||
|
||||
for _, db := range settingsDB {
|
||||
_ = db.Close()
|
||||
}
|
||||
}
|
||||
|
||||
os.Exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
||||
161
internal/tg/telegram.go
Normal file
161
internal/tg/telegram.go
Normal file
@@ -0,0 +1,161 @@
|
||||
package tg
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"aleesa-telegram-go/internal/log"
|
||||
|
||||
"github.com/NicoNex/echotron/v3"
|
||||
)
|
||||
|
||||
// Telega основная горутинка, реализующая бота.
|
||||
func Telega(c myConfig) {
|
||||
tg = echotron.NewAPI(c.Telegram.Token)
|
||||
|
||||
for u := range echotron.PollingUpdates(c.Telegram.Token) {
|
||||
telegramMsgParser(u)
|
||||
}
|
||||
}
|
||||
|
||||
func delMsg(msg *echotron.Update) {
|
||||
if res, err := tg.DeleteMessage(msg.ChatID(), msg.Message.ID); err != nil {
|
||||
chat := ConstructFullChatName(msg.Message.SenderChat)
|
||||
user := ConstructFullUserName(msg.Message.From)
|
||||
|
||||
log.Errorf("Unable to delete message %d in chat %s from %s via telegram api: %s", msg.Message.ID, chat, user, err)
|
||||
} else if !res.Ok {
|
||||
chat := ConstructFullChatName(msg.Message.SenderChat)
|
||||
user := ConstructFullUserName(msg.Message.From)
|
||||
|
||||
log.Errorf("Unable to delete message %d in chat %s from %s via telegram api: %s", msg.ID, chat, user, res.Description)
|
||||
}
|
||||
}
|
||||
|
||||
// ConstructFullUserName выковыривает из сообщения полный username, в формате @username FirstName LastName (id).
|
||||
func ConstructFullUserName(u *echotron.User) string {
|
||||
user := fmt.Sprintf("(%d)", u.ID)
|
||||
|
||||
if u.LastName != "" {
|
||||
user = fmt.Sprintf("%s %s", u.LastName, user)
|
||||
}
|
||||
|
||||
if u.FirstName != "" {
|
||||
user = fmt.Sprintf("%s %s", u.FirstName, user)
|
||||
}
|
||||
|
||||
if u.Username != "" {
|
||||
user = fmt.Sprintf("@%s %s", u.Username, user)
|
||||
}
|
||||
|
||||
return user
|
||||
}
|
||||
|
||||
// ConstructFullChatName выковыривает из сообщения полный username чата, в формате @username FirstName LastName (id).
|
||||
func ConstructFullChatName(c *echotron.Chat) string {
|
||||
chat := fmt.Sprintf("(%d)", c.ID)
|
||||
|
||||
if c.LastName != "" {
|
||||
chat = fmt.Sprintf("%s %s", c.LastName, chat)
|
||||
}
|
||||
|
||||
if c.FirstName != "" {
|
||||
chat = fmt.Sprintf("%s %s", c.FirstName, chat)
|
||||
}
|
||||
|
||||
if c.Username != "" {
|
||||
chat = fmt.Sprintf("@%s %s", c.Username, chat)
|
||||
}
|
||||
|
||||
return chat
|
||||
}
|
||||
|
||||
// ConstructPartialUserUsername пытается найти и вытащить username, если такового нет, вытаскивает First/Last Name, если
|
||||
// такового нет, то возвращает ID.
|
||||
func ConstructPartialUserUsername(u *echotron.User) string {
|
||||
switch {
|
||||
case u.Username != "":
|
||||
return fmt.Sprintf("@%s", u.Username)
|
||||
|
||||
case u.FirstName != "" && u.LastName != "":
|
||||
return fmt.Sprintf("%s %s", u.FirstName, u.LastName)
|
||||
|
||||
case u.FirstName != "":
|
||||
return u.FirstName
|
||||
|
||||
case u.LastName != "":
|
||||
return u.LastName
|
||||
|
||||
default:
|
||||
return fmt.Sprintf("%d", u.ID)
|
||||
}
|
||||
}
|
||||
|
||||
// ConstructPartialUserUsername пытается найти и вытащить username, если такового нет, вытаскивает First/Last Name, если
|
||||
// такового нет, то возвращает ID.
|
||||
func ConstructPartialChatUsername(c *echotron.Chat) string {
|
||||
switch {
|
||||
case c.Username != "":
|
||||
return fmt.Sprintf("@%s", c.Username)
|
||||
|
||||
case c.FirstName != "" && c.LastName != "":
|
||||
return fmt.Sprintf("%s %s", c.FirstName, c.LastName)
|
||||
|
||||
case c.FirstName != "":
|
||||
return c.FirstName
|
||||
|
||||
case c.LastName != "":
|
||||
return c.LastName
|
||||
|
||||
default:
|
||||
return fmt.Sprintf("%d", c.ID)
|
||||
}
|
||||
}
|
||||
|
||||
// ConstructUserFirstLastName Пытается найти и вытащить first name и last name пользователя, если не получается, то
|
||||
// вначале пытается фоллбэчится на first name, потом на last name, потом на username.
|
||||
func ConstructUserFirstLastName(u *echotron.User) string {
|
||||
var user string
|
||||
|
||||
switch {
|
||||
case u.FirstName != "" && u.LastName != "":
|
||||
user = fmt.Sprintf("%s %s", u.FirstName, u.LastName)
|
||||
case u.FirstName != "":
|
||||
user = u.FirstName
|
||||
case u.LastName != "":
|
||||
user = u.LastName
|
||||
case u.Username != "":
|
||||
user = fmt.Sprintf("@%s", u.Username)
|
||||
default:
|
||||
user = fmt.Sprintf("%d", u.ID)
|
||||
}
|
||||
|
||||
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: */
|
||||
349
internal/tg/telegramMsgParser.go
Normal file
349
internal/tg/telegramMsgParser.go
Normal file
@@ -0,0 +1,349 @@
|
||||
package tg
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math/rand/v2"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"aleesa-telegram-go/internal/log"
|
||||
|
||||
"github.com/NicoNex/echotron/v3"
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
)
|
||||
|
||||
// telegramMsgParser парсит ивент, прилетевший из bot api.
|
||||
func telegramMsgParser(msg *echotron.Update) {
|
||||
if Shutdown {
|
||||
// Если мы завершаем работу программы, то нам ничего обрабатывать не надо.
|
||||
return
|
||||
}
|
||||
|
||||
// Сообщение о том, что этот чятик изменился, например, превратился в супергруппу.
|
||||
if (msg.Message.MigrateFromChatID < 0) && (msg.Message.MigrateToChatID < 0) { //nolint: revive,staticcheck
|
||||
// TODO: поддержать миграцию настроек чата на новый chatId.
|
||||
// TODO: подумать, что можно сделать с настройками бэкэндов. Кажись, ничего, но надо глянуть.
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Здесь начинается парсинг, подразумевающий какие-то сообщения бота в чат, поэтому с этой точки можно/нужно
|
||||
// получить информацию о самом боте.
|
||||
// TODO: пораскинуть, можно ли как-то кэшировать эту информацию, чтобы не дёргать api по чём зря.
|
||||
var (
|
||||
rmsg rMsg
|
||||
errorOccured bool
|
||||
)
|
||||
|
||||
me, err := tg.GetMe()
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("Unable to get info about myself, %s", err)
|
||||
|
||||
errorOccured = true
|
||||
}
|
||||
|
||||
// Типа, при некоторых обстоятельствах, мы можем получить более внятное сообщение. Но это надо проверять.
|
||||
if !me.Ok {
|
||||
log.Errorf("Unable to get info about myself: %d %s", me.ErrorCode, me.Description)
|
||||
|
||||
errorOccured = true
|
||||
}
|
||||
|
||||
if errorOccured {
|
||||
return
|
||||
}
|
||||
|
||||
// Люди пришли в чят.
|
||||
if msg.Message.NewChatMembers != nil { //nolint: revive,staticcheck
|
||||
// TODO: проверить, надо ли приветствовать, выбрать одну из приветственных фраз, потянуть время, типа, мы пишем
|
||||
// текст и поприветствовать вновь прибывшего.
|
||||
if GetSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "GreetMsg") == "1" {
|
||||
// Пользователи, которых мы приветсвуем, одной строкой.
|
||||
var users string
|
||||
|
||||
if len(msg.Message.NewChatMembers) > 1 {
|
||||
lastOne := msg.Message.NewChatMembers[len(msg.Message.NewChatMembers)-1]
|
||||
theRest := msg.Message.NewChatMembers[:len(msg.Message.NewChatMembers)-1]
|
||||
var usersSlice []string
|
||||
|
||||
for _, user := range theRest {
|
||||
usersSlice = append(usersSlice, ConstructTelegramHighlightName(user))
|
||||
}
|
||||
|
||||
users = strings.Join(usersSlice, ", ")
|
||||
users += fmt.Sprintf(" and %s", ConstructTelegramHighlightName(lastOne))
|
||||
} else {
|
||||
users = ConstructTelegramHighlightName(msg.Message.NewChatMembers[0])
|
||||
}
|
||||
|
||||
phrase := fmt.Sprintf(
|
||||
"%s, %s. Представьтес, пожалуйста, и расскажите, что вас сюда привело.",
|
||||
introduceGreet[rand.IntN(len(introduceGreet))],
|
||||
users,
|
||||
)
|
||||
|
||||
// Потянуть время, изобразить настоящего человека.
|
||||
resp, err := tg.SendChatAction(
|
||||
echotron.Typing,
|
||||
msg.Message.From.ID,
|
||||
&echotron.ChatActionOptions{MessageThreadID: int(msg.Message.ThreadID)},
|
||||
)
|
||||
|
||||
// TODO: вероятно, ошибку надо засылать в обработчик ошибок, так как в ней может быть миграция чятика или
|
||||
// что-то, что тоже надо хэндлить.
|
||||
if err != nil || !resp.Ok {
|
||||
log.Errorf(
|
||||
"unable to send message to telegram api: %s\nResponse dump: %s",
|
||||
err,
|
||||
spew.Sdump(resp),
|
||||
)
|
||||
}
|
||||
|
||||
resp1, err := tg.SendMessage(
|
||||
phrase,
|
||||
msg.Message.Chat.ID,
|
||||
&echotron.MessageOptions{ParseMode: "MarkdownV2", MessageThreadID: int64(msg.Message.ThreadID)},
|
||||
)
|
||||
|
||||
// TODO: вероятно, ошибку надо засылать в обработчик ошибок, так как в ней может быть миграция чятика или
|
||||
// что-то, что тоже надо хэндлить.
|
||||
if err != nil || !resp1.Ok {
|
||||
log.Errorf(
|
||||
"unable to send message to telegram api: %s\nResponse dump: %s",
|
||||
err,
|
||||
spew.Sdump(resp1),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Человек ушёл из чата.
|
||||
if msg.Message.LeftChatMember != nil {
|
||||
if GetSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "GoodbyeMsg") == "1" {
|
||||
user := ConstructUserFirstLastName(msg.Message.From)
|
||||
|
||||
goodbye := fmt.Sprintf("Прощаемся с %s", user)
|
||||
|
||||
opts := echotron.MessageOptions{DisableNotification: true, ParseMode: "MarkdownV2"}
|
||||
|
||||
if msg.Message.ThreadID != 0 {
|
||||
opts.MessageThreadID = int64(msg.Message.ThreadID)
|
||||
}
|
||||
|
||||
if res, err := tg.SendMessage(goodbye, msg.Message.Chat.ID, &opts); err != nil {
|
||||
log.Errorf("Unable to send message to telegram api: %s", err)
|
||||
} else if !res.Ok {
|
||||
log.Errorf("Unable to send message to telegram api: %s", res.Description)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Если сообщение было зацензурено, то дальше его обрабатывать не надо.
|
||||
if Censor(msg) {
|
||||
return
|
||||
}
|
||||
|
||||
switch msg.Message.Chat.Type {
|
||||
case "private":
|
||||
// Всё что можно было похэндлить, что не содержало текста, считаем, что обработали.
|
||||
if msg.Message.Text == "" {
|
||||
return
|
||||
}
|
||||
|
||||
// Считаем, что приватное сообщение всегда нуждается в ответе, иначе, зачем писать боту?
|
||||
rmsg.Misc.Answer = 1
|
||||
|
||||
// Засылаем фразу в misc-канал (в роутер).
|
||||
rmsg.Chatid = fmt.Sprintf("%d", msg.Message.Chat.ID)
|
||||
rmsg.Userid = fmt.Sprintf("%d", msg.Message.From.ID)
|
||||
rmsg.Message = msg.Message.Text
|
||||
rmsg.Mode = "private"
|
||||
rmsg.Plugin = "telegram"
|
||||
rmsg.From = "telegram"
|
||||
rmsg.Misc.Csign = Config.Csign
|
||||
rmsg.Misc.Fwdcnt = 1
|
||||
rmsg.Misc.Botnick = ConstructPartialUserUsername(me.Result)
|
||||
rmsg.Misc.Username = ConstructPartialUserUsername(msg.Message.From)
|
||||
rmsg.Misc.GoodMorning = 0
|
||||
rmsg.Misc.Msgformat = 0
|
||||
|
||||
// Форматировать что-то всё-таки надо, но благо, это только команды, поэтому попробуем распознать сообщение как
|
||||
// команду.
|
||||
r, err := cmdParser(me, msg)
|
||||
|
||||
if err != nil {
|
||||
log.Infof(
|
||||
"Unable to parse message as command in private conversation with %s, message was: %s",
|
||||
ConstructFullUserName(msg.Message.From),
|
||||
msg.Message.Text,
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Если сообщение не было опознанно как команда, засылаем его "как есть" туда, в качель.
|
||||
if !r {
|
||||
data, err := json.Marshal(rmsg)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf("Unable to to serialize message for redis: %s", err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Заталкиваем наш 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))
|
||||
}
|
||||
}
|
||||
|
||||
case "group", "supergroup":
|
||||
// Обрабатываем сообщения для групп и супергрупп. По сути это примерно одинаковые вещи/сущности.
|
||||
// Цензор удаляет сообщения из чятика, если они "неправильные" - от имени других каналов, аудиосообщения,
|
||||
// содержат картинки, видео аудио итп, это настраивается через команду !admin censor.
|
||||
if GetSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "censor") == "1" {
|
||||
// Если цензор отработал, больше делать нечего, сообщения уже нету.
|
||||
if Censor(msg) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Если фраза менее 3-х букв, просто игнорируем её.
|
||||
if len(msg.Message.Text) <= 3 {
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: если человек ответил на авто-приветствие, это надо обнаруживать и пропускать.
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// Здесь же, если фраза обращена к боту (ник или имя), выставляем флажок, что надо ответить
|
||||
quotedBotUsername := regexp.QuoteMeta(me.Result.Username)
|
||||
quotedBotFirstName := regexp.QuoteMeta(me.Result.FirstName)
|
||||
quotedBotLastName := regexp.QuoteMeta(me.Result.LastName)
|
||||
|
||||
if messageContainsBotUsername, err := regexp.MatchString(quotedBotUsername, msg.Message.Text); err != nil {
|
||||
log.Errorf("An error occured while matching message with bot username: %s", err)
|
||||
|
||||
return
|
||||
} else if messageContainsBotUsername {
|
||||
rmsg.Misc.Answer = 1
|
||||
m := regexp.MustCompile(quotedBotUsername)
|
||||
msg.Message.Text = m.ReplaceAllString(msg.Message.Text, "")
|
||||
}
|
||||
|
||||
// TODO: обращение может содержать знаки препинания, пробелы перед, пробелы после имени бота. Надо бы это поддержать.
|
||||
// TODO: унести вырезание имени бота в отдельную процедуру.
|
||||
switch {
|
||||
// Если у нас есть и имя и фамилия бота, то попробуем их слепить вместе, через "1 и более" пробел и вырезать
|
||||
// полученное из входящей фразы.
|
||||
case quotedBotFirstName != "" && quotedBotLastName != "":
|
||||
messageContainsBotName, err := regexp.MatchString(
|
||||
quotedBotFirstName+"[[:space:]]+"+quotedBotLastName,
|
||||
msg.Message.Text,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("An error occured while matching message with bot firstname and lastname: %s", err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if messageContainsBotName {
|
||||
rmsg.Misc.Answer = 1
|
||||
m := regexp.MustCompile(quotedBotFirstName + "[[:space:]]+" + quotedBotLastName)
|
||||
msg.Message.Text = m.ReplaceAllString(msg.Message.Text, "")
|
||||
}
|
||||
// Если у нас есть только имя, то попробуем его вырезать из входящей фразы.
|
||||
case quotedBotFirstName != "":
|
||||
messageContainsBotName, err := regexp.MatchString(quotedBotFirstName, msg.Message.Text)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("An error occured while matching message with bot firstname: %s", err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if messageContainsBotName {
|
||||
rmsg.Misc.Answer = 1
|
||||
m := regexp.MustCompile(quotedBotFirstName)
|
||||
msg.Message.Text = m.ReplaceAllString(msg.Message.Text, "")
|
||||
}
|
||||
// Если у нас есть только фамилия, то попробуем его вырезать из входящей фразы.
|
||||
case quotedBotLastName != "":
|
||||
messageContainsBotName, err := regexp.MatchString(quotedBotLastName, msg.Message.Text)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("An error occured while matching message with bot lastname: %s", err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if messageContainsBotName {
|
||||
rmsg.Misc.Answer = 1
|
||||
m := regexp.MustCompile(quotedBotLastName)
|
||||
msg.Message.Text = m.ReplaceAllString(msg.Message.Text, "")
|
||||
}
|
||||
}
|
||||
|
||||
// Засылаем фразу в misc-канал (в роутер).
|
||||
rmsg.Chatid = fmt.Sprintf("%d", msg.Message.Chat.ID)
|
||||
rmsg.Userid = fmt.Sprintf("%d", msg.Message.From.ID)
|
||||
rmsg.Message = msg.Message.Text
|
||||
rmsg.Mode = "public"
|
||||
rmsg.Plugin = "telegram"
|
||||
rmsg.From = "telegram"
|
||||
rmsg.Misc.Csign = Config.Csign
|
||||
rmsg.Misc.Fwdcnt = 1
|
||||
|
||||
rmsg.Misc.Botnick = ConstructPartialUserUsername(me.Result)
|
||||
rmsg.Misc.Username = ConstructPartialUserUsername(msg.Message.From)
|
||||
|
||||
rmsg.Misc.GoodMorning = 0
|
||||
// Форматирование нужно только для вывода некоторых ответов на команды, команды мы ловим выше по тексту, так что
|
||||
// смело ставим тут 0.
|
||||
rmsg.Misc.Msgformat = 0
|
||||
|
||||
data, err := json.Marshal(rmsg)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf("Unable to to serialize message for redis: %s", err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Заталкиваем наш 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))
|
||||
}
|
||||
|
||||
// case "channel":
|
||||
// Тут мы ничего сделать не можем.
|
||||
} //nolint:wsl
|
||||
}
|
||||
|
||||
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
||||
68
internal/tg/types.go
Normal file
68
internal/tg/types.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package tg
|
||||
|
||||
// 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"`
|
||||
|
||||
Telegram struct {
|
||||
Token string `json:"token,omitempty"`
|
||||
}
|
||||
|
||||
Loglevel string `json:"loglevel,omitempty"`
|
||||
Log string `json:"log,omitempty"`
|
||||
Csign string `json:"csign,omitempty"`
|
||||
ForwardsMax int64 `json:"forwards_max,omitempty"`
|
||||
DataDir string `json:"data_dir,omitempty"`
|
||||
}
|
||||
|
||||
// rMsg структура, описывающая формат входящего сообщения из 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"`
|
||||
// В этом поле мы пишем 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"`
|
||||
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"`
|
||||
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: */
|
||||
Reference in New Issue
Block a user