Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Роутер и Контекст (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):

  1. Предотвращение Deadlock: Семафор, управляющий пулом воркеров, обернут в select с ctx.Done(). Это защищает роутер от зависания (deadlock) при выключении, если пул воркеров был переполнен.
  2. Завершение обработки: Вычитанные из канала, но еще не взятые в работу апдейты, обязательно дорабатываются даже при отмене основного контекста. Для этого внутри воркера используется изолированный от отмены контекст context.WithoutCancel(ctx).
  3. Ограничение времени: Каждому дорабатывающему воркеру выдается тайм-аут в 30 секунд (context.WithTimeout), чтобы предотвратить вечное ожидание ответа от серверов Яндекса.
  4. Гарантия завершения: Роутер блокируется до тех пор, пока все запущенные горутины-воркеры не закончат свою работу (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("Получен сигнал выключения. Ожидание завершения обработки обновлений...")

// Роутер автоматически дождется завершения всех запущенных горутин
// и предотвратит потерю апдейтов.