Шаблон телеграмм бота на go
- суббота, 17 февраля 2024 г. в 00:00:17
Добрый день, сегодня я поделюсь с вами, на мой взгляд, довольно удачным шаблоном для телеграмм ботов на go
В нем заложена большая часть популярных сценариев работы и его расширение не должно вызывать проблем.
Функционал шаблона
Выполнение запросов в Горутинах
Выполнение команд из консоли
Хранение контекста во время выполнения
Вывод клавиатур и сообщений с локализацией
Возвращение к предыдущему меню
Для тех кому не терпеться взглянуть добро пожаловать в GitHub.
Заранее прошу меня простить если покорёжит от стиля кода, шаблон я составил на 2й день изучения языка и ещё не успел впитать в себя его дух.
Шаблон основан на идеи единой архитектуры системы обработки сообщений, где функционал можно добавлять виде модулей.
Все возможные действия которые может выполнять бот заключены в одну абстрактную функцию Run (Run go ! Run !).
Для хранения контекста запуска этих функций создана структура CallStack
type Run func(CallStack) CallStack
type CallStack struct {
ChatID int64
Bot *tgBotAPI.BotAPI
Update *tgBotAPI.Update
Action Run
IsPrint bool
Parent *CallStack
Data string
}
var userRuns = map[int64]CallStack{}
stack := userRuns[ID]
if stack.Action != nil {
stack.Update = &update
userRuns[ID] = userRuns[ID].Action(stack)
} else {
if update.Message != nil {
userRuns[ID] = RunTemplate(CallStack{
ChatID: ID,
Bot: bot,
Update: &update,
IsPrint: true,
})
}
}
В BotLoop реализован готовый цикл обработки сообщений.
Для разделения стеков контекстов выполнения используется map, где ключ это ID чата, но Вы можете выбрать свой, например логин пользователя.
Если в хранилище есть готовый стек контекста, передаем ему обновление и запускаем Run, который вернёт контекст для следующей обработки.
Если действие не задано возвращаем пользователя в начальную точку.
RunTemplate это шаблон реализации Run, он заключает в себе сразу 3 вещи:
Вывод интереса для пользователя.
Обработка сообщений.
Инициализация нового контекста.
Это делает его самодостаточным модулем для бота, который можно переносить из проекта в проект.
Далее повествование уходит в код:
Это общий шаблон для всех Run
func RunTemplate(stack CallStack) CallStack {
Когда Run получает свой контекст он выставляет в нём себя в качетве
выполняемого действия.
Далее практически всё происходит на его основе контекста
stack.Action = RunTemplate
Информация о пользователе
data := userDatas[stack.ChatID]
if stack.IsPrint {
Если нужно что-то передать пользователю пишем это тут
Если передачу нужно повторять при повсторном заходе в Run
Уберите это снятие флага, но обработку сообщений тогда нужно будет вынести из else
stack.IsPrint = false
Тут пример вывода локализованного форматированого сообщения с прикреплёнными кнопками
Не пишите текст прямо в Run, заносите шаблоны для вывода в MessageTemplates
msg := tgBotAPI.NewMessage(stack.ChatID, fmt.Sprintf(SelectTemplate("RunTemplate", data.languageСode),
data.firstName,
))
mainMenuInlineKeyboard := tgBotAPI.NewInlineKeyboardMarkup(
tgBotAPI.NewInlineKeyboardRow(
tgBotAPI.NewInlineKeyboardButtonData(SelectTemplate("back", data.languageСode), "back"),
),
)
msg.ReplyMarkup = mainMenuInlineKeyboard
_, _ = stack.Bot.Send(msg)
Тут отчищается прошлый набор кнопок
Также можно задать свой набор для отображания
mainMenuKeyboard := tgBotAPI.NewRemoveKeyboard(true)
msg = tgBotAPI.NewMessage(stack.ChatID, "")
msg.ReplyMarkup = mainMenuKeyboard
_, _ = stack.Bot.Send(msg)
И так Run подготовил интерфейс и возвратил контекст выполнения для его сохранения
Исходя из этого, следующеt сообщение пользователя будет обработанно в этом же Run
return stack
} else {
И так, пользователь ввел сообщение, вызвал комманду или другим образом попытался сломать бота )
Сообщения
if stack.Update.Message != nil {
switch stack.Update.Message.Text {
case "back":
{
return ReturnOnParent(stack)
}
}
}
Кнопки с inline
if stack.Update != nil {
// Processing a message
if stack.Update.CallbackQuery != nil {
switch stack.Update.CallbackQuery.Data {
case "back":
{
stack.Data = stack.Update.CallbackQuery.Data
return ReturnOnParent(stack)
}
}
}
}
Комманды
if update.Message.IsCommand() {
switch update.Message.Command() {
case "back":
{
return ReturnOnParent(stack)
}
}
}
и т.д.
}
Если произошло что-то не предвиденное и код пришел сюда
можно просто отправить этот же контекст для следующей обработки
return stack
Или применить Затычку
return Chop(stack)
}
func Chop(stack CallStack) CallStack {
Затычка выведет сообшение ввиде картинки, которая сообщает что
пользователь достиг не проработанной конечной точки
photo := tgBotAPI.NewPhoto(stack.ChatID, chopFile)
photo.ReplyMarkup = tgBotAPI.NewRemoveKeyboard(true)
_, _ = stack.Bot.Send(photo)
И попытается вернуть пользователя назад
return ReturnOnParent(stack)
}
func ReturnOnParent(stack CallStack) CallStack {
if stack.Parent != nil {
stack.Parent.IsPrint = true
return stack.Parent.Action(*stack.Parent)
}
return RunTemplate(CallStack{
IsPrint: true,
ChatID: stack.ChatID,
Bot: stack.Bot,
})
}
Это достаточно гибкая архитектура, изменяя Run можно добиться множество интересных эффектов.
Меню ввода с возвращением информации предыдущему Run.
Сквозной запуск последовательностей Run.
Сохранение стека контекста при остановке и восстановление при включении.
Если добавить рефлексии можно сделать бота полностью конфигурируемого внешними файлами.
Изменение последовательности выполнения Run-ов во время работы.