Роутер и Контекст (Router & Context)
Модуль router предоставляет удобный механизм для обработки входящих обновлений (Updates) от Яндекс Мессенджера. Он построен по принципу цепочки обязанностей с поддержкой middleware и строго типизированного контекста Context.
Архитектура Роутера
Роутер отвечает за распределение входящих обновлений по соответствующим обработчикам (хендлерам). Он обрабатывает обновления конкурентно, запуская каждый хендлер в отдельной горутине. Для предотвращения исчерпания ресурсов (например, при огромном наплыве событий) роутер использует семафор, который жестко ограничивает максимальное количество одновременных горутин (по умолчанию 50).
Регистрация обработчиков
Вы можете привязать обработчики к различным типам событий:
- Команды (
HandleCommand): Обработка текстовых сообщений, начинающихся со слеша (например,/start). - Кнопки (
HandleButton): Обработка нажатий на инлайн-кнопки (Action buttons). Привязывается к имени серверного действияServerAction.Name. - Опросы (
HandlePoll): Обработка событий появления новых опросов в чате (u.Poll). - Текст (
HandleText): Резервный обработчик для любых текстовых сообщений, которые не являются командами. - Состояния FSM (
HandleState,HandleStateRegexp): Обработчики для пользователей, находящихся в определенном состоянии конечного автомата (FSM). - Middlewares (
Use): Промежуточное ПО, которое выполняется до и/или после основного обработчика. Идеально подходит для логирования, сбора метрик или проверки прав.
Пример инициализации и маршрутизации
package main
import (
"log"
yabotapi "github.com/go-yandex-bot-api/yandex-bot-api"
"github.com/go-yandex-bot-api/yandex-bot-api/pkg/router"
)
func main() {
bot, err := yabotapi.NewBot("YOUR_TOKEN")
if err != nil {
log.Fatalf("Ошибка инициализации: %v", err)
}
r := router.NewRouter(bot)
// Добавление встроенных middleware (для логирования и перехвата паник)
r.Use(router.LoggerMiddleware())
r.Use(router.RecoverMiddleware())
// Кастомный middleware (например, проверка прав)
r.Use(func(next router.HandlerFunc) router.HandlerFunc {
return func(c *router.Context) error {
log.Printf("Получено обновление ID: %d", c.Update.UpdateID)
return next(c) // Передача управления следующему хендлеру
}
})
// Обработка команды /start
r.HandleCommand("start", func(c *router.Context) error {
return c.Reply("Привет! Я бот.")
})
// Обработка нажатия на кнопку с ServerAction.Name == "click_btn"
r.HandleButton("click_btn", func(c *router.Context) error {
return c.Reply("Вы нажали на кнопку!")
})
// Обработка любого другого текста (эхо-режим)
r.HandleText(func(c *router.Context) error {
return c.Reply("Вы сказали: " + c.Update.Text)
})
// Резервный обработчик, если ничего не подошло
r.HandleDefault(func(c *router.Context) error {
return c.Reply("Я не понимаю вас.")
})
// ... запуск получения обновлений (Short-Polling/Webhooks) и r.Start(ctx, ch)
}
Контекст (Context)
Объект Context передается в каждый хендлер и содержит всю необходимую информацию для работы с текущим обновлением. Он оборачивает само обновление (types.Update), экземпляр клиента API (*yabotapi.Bot) и предоставляет удобные вспомогательные методы.
Хелперы отправки сообщений
Вместо ручного формирования сложных объектов запроса, вы можете использовать встроенные методы контекста, которые автоматически подставляют нужные ID чата и треда:
c.Reply(text string): Отвечает текстовым сообщением в тот же чат с цитированием (reply_message_id) и поддержкой тредов.c.Replyf(format string, args ...any): Отвечает форматированным сообщением с цитированием.c.ReplyWithKeyboard(text string, keyboard *types.SuggestButtons): Отвечает сообщением с клавиатурой и цитированием.c.Send(text string): Отправляет новое самостоятельное сообщение в текущий чат без цитирования.c.Sendf(format string, args ...any): Отправляет форматированное новое сообщение без цитирования.c.SendWithKeyboard(text string, keyboard *types.SuggestButtons): Отправляет новое сообщение с клавиатурой без цитирования.c.SendTo(recipient string, text string): Отправляет сообщение в произвольный чат или логин пользователя.c.BindPayload(dest interface{}): Удобно распаковывает JSON-полезную нагрузку из нажатой кнопки прямо в структуру.c.FirstImage(): Возвращает указатель на первое изображение-вложение иtrue, если оно есть в сообщении.c.FirstFile(): Возвращает указатель на файл-документ иtrue, если он прикреплен к сообщению.r.StartPolling(ctx, offset): Запускает Long-Polling канал обновлений и обработку событий одной строкой кода.router.GetFSMData[T](c, key): Строго типизированное извлечение сохраненных FSM-данных с помощью Generics без ручного type assertion.
Глобальный перехватчик ошибок (OnError)
Вы можете зарегистрировать глобальный обработчик ошибок на уровне роутера. Если любой хендлер вернет ошибку, она автоматически попадет в функцию OnError:
r.OnError(func(c *router.Context, err error) {
log.Printf("[ОШИБКА] Обработка запроса от %s завершилась с ошибкой: %v", c.Update.GetFromLogin(), err)
_ = c.Reply("⚠️ Произошла внутренняя ошибка. Попробуйте позже.")
})
⚠️ Безопасное извлечение ID (Критично!)
В Яндекс Мессенджере структура входящего обновления (Update) имеет специфичные особенности. В частности, при получении личных сообщений поле Chat может быть nil, а для определения получателя используется поле From.Login.
Из-за этого прямое обращение u.Chat.ID часто вызывает критическую панику: nil pointer dereference.
Строго запрещено использовать c.Update.Chat.ID напрямую!
Всегда используйте встроенные безопасные хелперы контекста:
c.IsPrivate()— возвращаетtrue, если сообщение пришло в Личных Сообщениях (ЛС), иfalse, если из группы.c.Update.IsGroup()— возвращаетtrue, если сообщение отправлено в групповом чате или канале.c.Update.Command()— возвращает имя поступившей команды без/(например,"start").c.Update.CommandArguments()— возвращает строку аргументов, переданых с командой (например,"arg1 arg2"для/start arg1 arg2).c.ChatID()— универсально возвращает ID чата (для групп) или Логин (для ЛС).c.SenderLogin()— всегда безопасно возвращает логин отправителя.
if c.IsPrivate() {
// Пользователь написал в ЛС
return c.Send("Ответ в Личных Сообщениях")
} else {
// Пользователь написал в группе/чате
return c.Reply("Ответ в группу")
}
Для безопасного и быстрой работы с идентификаторами в контексте предусмотрены короткие методы-псевдонимы:
c.SenderLogin(): Безопасно возвращаетtypes.UserLoginотправителя.c.ChatID(): Безопасно возвращаетtypes.ChatIDтекущего чата.c.SenderLoginIs("admin"): Быстрая проверка логина отправителя без ручного приведения типаstring(...).
func customHandler(c *router.Context) error {
// Проверка прав администратора без приведения типов:
if c.SenderLoginIs("admin_login") {
return c.Reply("Доступ разрешен!")
}
login := c.SenderLogin()
chatID := c.ChatID()
// ...
return nil
}
Специализированные обработчики контента и фильтры
Для быстрой подписки на события с определенным типом содержимого в Router предусмотрены отдельные вызовы:
r.HandleFile(fn)— перехватывает прикрепленные файлы/документы (c.IsFile()).r.HandleImage(fn)— перехватывает изображения и альбомы (c.IsImage()).r.HandleSticker(fn)— перехватывает стикеры (c.IsSticker()).r.HandleVote(fn)— перехватывает события результатов голосований в опросах (c.IsVote()).
r.HandleFile(func(c *router.Context) error {
return c.Replyf("Файл %s получен!", c.Update.File.Name)
})
r.HandleVote(func(c *router.Context) error {
return c.Replyf("Спасибо за голос в опросе %s!", c.Update.Vote.PollID)
})
Graceful Shutdown (Изящная остановка)
При выключении приложения (по Ctrl+C или команде от Docker/Kubernetes) очень важно корректно завершить работу и доработать уже полученные обновления. В противном случае события от пользователей могут быть потеряны безвозвратно.
В метод Router.Start встроен мощный механизм изящной остановки (Graceful Shutdown):
- Предотвращение Deadlock: Семафор, управляющий пулом воркеров, обернут в
selectсctx.Done(). Это защищает роутер от зависания (deadlock) при выключении, если пул воркеров был переполнен. - Завершение обработки: Вычитанные из канала, но еще не взятые в работу апдейты, обязательно дорабатываются даже при отмене основного контекста. Для этого внутри воркера используется изолированный от отмены контекст
context.WithoutCancel(ctx). - Ограничение времени: Каждому дорабатывающему воркеру выдается тайм-аут в 30 секунд (
context.WithTimeout), чтобы предотвратить вечное ожидание ответа от серверов Яндекса. - Гарантия завершения: Роутер блокируется до тех пор, пока все запущенные горутины-воркеры не закончат свою работу (
sync.WaitGroup.Wait()).
Пример корректного запуска:
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer cancel()
updatesCh := make(chan types.Update, 100)
// 1. Запуск роутера в отдельной горутине
go r.Start(ctx, updatesCh)
// 2. Запуск сервиса получения обновлений (Short-Polling / Webhooks)...
// ...
// 3. Ожидание сигнала выключения
<-ctx.Done()
log.Println("Получен сигнал выключения. Ожидание завершения обработки обновлений...")
// Роутер автоматически дождется завершения всех запущенных горутин
// и предотвратит потерю апдейтов.