Архитектура telegram-бота. На горутинах и каналах
Это мой первый telegram-бот и моя первая разработка на go. В качестве базы данных используются google-таблицы, благодаря чему большая часть логики находиться в них. Но к сути это отношения имеет мало. Основной причиной написания этой статьи стало то, что проект который я разрабатывал, уже технически реализованный, отложили что бы маркетингом "навести суету" по схемам десятилетней давности. Полагаю проект всё равно будет заброшен, по крайней мере у меня уже нет сил его "продавливать". Хоть я и не могу опубликовать исходный код (полностью), но могу поделиться той схемой к которой пришёл. Возможно это кому то пригодиться.
Архитектура
Есть основная структура в которую упаковываются запросы и ответы:
type tgRequest struct { // Структура для хранения сообщения и ответов на него
MessageText string // Текст сообщения
KeyData string // Нажатая кнопка
TextRequest string // При заполнении структуры, этому полю присваивается значение дублирующее текст сообщения или нажатую кнопку
ClientUserName string // Имя пользователя сделавшего запрос
ChatID int64 // Идентификатор чата с ним
MessageReplyID int // Идентификатор сообщения на которое нужно ответить
TimeRequest float64 // Время регистрации запроса. Предназначено для того что бы уведомлять пользователя о задержке, и предотвращать повторный запрос.
Responses []string // Массив ответов на запрос. request.Responses = append(request.Responses, "Ещё один ответ на запрос.")
tgKeyboard tgbotapi.InlineKeyboardMarkup // Кнопки телеграмма для ответа
CloseRequest bool // Присвоение True означает что запрос следует удалить из листа ожидания.
StopGo bool // Для остановки горутины
TimeUNIXtoWaitNotification int64 // Время UNIX после которого произойдёт оповещение пользователя о том что его запрос задерживается
DisableNotification bool // Отключение уведомления
DialogChan chan<- tgRequest // Возвращаемый канал для диалога
IncomingFileURL string // вложенный файл, URL для загрузки
SaveFilePath string // Имя файла который был сохранён
SaveFileName string // Имя файла
DownloadPathFile string // Путь к файлу на локальном накопителе, который нужно отправить пользователю
RenameDownlodFile string // Нужно ли переименовать файл. Пока что не используется.
DownloadFileIsMedia bool // Означает что файл надо отправить не как "документ", а как например фото.
}
Заполняется эта структура разбором var updates tgbotapi.UpdatesChannel
в секции select.
После чего запросы распределяются между горутинами. Есть несколько горутин.
Одна по очереди принимает и обрабатывает те запросы которые нельзя обрабатывать одновременно.
Для всех запросов которые не входят в ограниченный список для которых запрещена параллельная обработка, запускаются отдельные горутины.
Все запросы посланные в предыдущие две, дублируются в горутину для уведомлений, она регистрирует запросы и если через неё в течении нескольких секунд не проходит ответ, то она посылает пользователю уведомление "подождите".
Есть горутина для приёма отправляемых файлов.
Ещё есть глобальная карта, в которой ключом является ChatID, а значением указатель на канал. Есть в карте есть значение !=nil
то запросы направляются в этот канал, а не в упомянутые выше горутины.
// Создание каналов для общения с потоками
syncRequestChan := make(chan tgRequest, 1000) // Канал для синхронных запросов
waitQueueList := make(chan tgRequest, 1000) // Канал для листа ожидания. Смысл горутины его использующей, в том что бы всем пользователям ожидающим больше некоторого времени, посылать успокаивающее сообщение
notificationLogStop := make(chan tgRequest, 10) // Канал для остановки уведомлений администраторов о журналах
answersChan := make(chan tgRequest, 1000) // Канал для ответов
loadFilesChan := make(chan tgRequest, 1000) // Канал для загрузки файлов
stopNotificationOp := make(chan tgRequest, 1000) // Канал для остановки уведомлений операторов
// Запуск потоков
go waitQueueListRequest(waitQueueList, answersChan) // Запуск потока который будет оповещать пользователей когда выполнение из запросов задерживается.
go processResponseSync(syncRequestChan, answersChan) // Запуск потока который обрабатывает запросы пользователей последовательно, то есть по очереди
go processNotificationLog(notificationLogStop, answersChan)
// processResponseAsync(requestChan chan<- tgRequest, Request tgRequest) будет запускаться для каждого асинхронного запроса отдельно
go processLoadFiles(loadFilesChan, answersChan) // Процесс для загрузки файлов
go processNotificationOrders(stopNotificationOp, answersChan)
Схема:
Основная функция отслеживает ответы горутин приходящие через канал answersChan
и направляет их пользователям.
Сначала через API телеграмм приходит сообщение или нажатие на кнопку. Оно конвертируется в структуру типа tgRequest и распределяется между горутиной для последовательной обработки запросов, например для финансовых операций. И параллельным выполнением в отдельной горутине. Распределение происходит по списку команд.
var SyncСommandPrefixes = map[string]bool{ // Перечисленные в этом массиве команды выполняются синхронно, остальные асинхронно
"/зачисление": true,
"/depositing": true, // Административная (бухгалтерская) команда для зачисление средств на счёт пользователя. Синтаксис /depositing UserName +1000
"/резервирование": true, // Резервирование товара. Если введено число выше доступных остатков или доступных средств, то происходит меньшее резервирование.
"/reservation": true, // Пользовательская команда для резервирования определённого числа товара. Синтаксис /reservation артикул +10
// Если в параметре отрицательное число, то происходит снятие с резерва.
"/order": true,
"/заказ": true, // Команда для совершения заказа из резерва
}
Так же сообщение дублируется в горутину уведомителя о задержках.
Все горутины обрабатывающие запросы отправляют ответы в единый канал answersChan. Всё что в него приходит отправляется пользователям как ответы и дублируется в уведомитель о задержках.
Уведомитель о задержках, посылает снижающие суету сообщения, если в него пришёл запрос и в течении нескольких секунд не пришёл ответ.
Для упрощения в схеме не обозначена горутина принимающая файлы, отсылающая уведомления операторам о ошибках. А так же перенаправление всей обработки в "диалоги", то есть горутины отключающие обычную обработку команд для отдельных пользователей. И многие другие функции.
Используется библиотека tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"