README_RU.md
24 KiB
Laniakea
Легковесная, простая в использовании и производительная обёртка для Telegram Bot API на Go. Она упрощает разработку ботов благодаря чистой системе плагинов, поддержке Middleware, автоматической генерации команд и встроенному рейтлимитеру.
✨ Возможности
- Простой и интуитивный API: Разработан для лёгкости использования, основан на практических примерах.
- Система плагинов: Организуйте функциональность бота в независимые, переиспользуемые плагины.
- Обработка команд: Легко регистрируйте команды и извлекайте аргументы.
- Поддержка промежуточных слоёв (Middleware): Выполняйте код до или после команд (например, логирование, проверка доступа).
- Автоматическая генерация команд: Генерируйте справку и списки команд автоматически.
- Встроенный ограничитель запросов (Rate Limiter): Защитите бота от превышения лимитов Telegram API (с обработкой
retry_after). - Контекст данных: Передавайте общие данные приложения или state в обработчики.
- Настраиваемый API: Комбинируйте
Set...иAdd...helper-методы для понятной конфигурации, напримерbot.SetErrorTemplate(...).AddPlugins(...). - Polling и Webhook Runtime: Запускайте бота через long polling с
Run()/RunWithContext(...)или через webhook server, которым владеет сам бот, сRunWebhookWithContext(...).
📦 Установка
go get git.scuroneko.dev/scuroneko/laniakea
или
go get github.com/scuroneko/laniakea
🚀 Быстрый старт (с пошаговыми комментариями)
Вот минимальный пример бота "echo/ping" с подробными комментариями.
package main
import (
"log"
"git.scuroneko.dev/scuroneko/laniakea" // Импортируем библиотеку Laniakea
)
// echo — это функция-обработчик команды.
// Она получает два параметра:
// - ctx: контекст сообщения (содержит информацию о сообщении, отправителе, чате и т.д.)
// - data: ваши общие данные приложения (здесь мы используем NoData — заглушку без общих зависимостей)
func echo(ctx *laniakea.MessageContext, data laniakea.NoData) error {
// Отвечаем пользователю текстом, который он прислал, без префикса команды.
// ctx.Text содержит сообщение пользователя, из которого удалена часть с командой.
ctx.Answer(ctx.Text) // Ввод пользователя БЕЗ команды
return nil
}
func main() {
// 1. Создаём опции бота. Замените "TOKEN" на реальный токен от @BotFather.
opts := &laniakea.BotOpts{Token: "TOKEN"}
// 2. Инициализируем новый экземпляр бота.
// Используем laniakea.NoData как тип данных приложения (общие зависимости не нужны для примера).
bot, err := laniakea.NewBot[laniakea.NoData](opts)
if err != nil {
log.Fatal(err)
}
// Гарантируем освобождение ресурсов бота при выходе.
defer bot.Close()
// 3. Создаём новый плагин с именем "ping".
// Плагины помогают группировать связанные команды и промежуточные обработчики.
p := laniakea.NewPlugin[laniakea.NoData]("ping")
// 4. Добавляем команду в плагин.
// p.Command("echo", echo) создаёт команду, которая вызывает функцию 'echo' по команде "/echo".
p.Command("echo", echo)
// 5. Добавляем ещё одну команду, используя анонимную функцию (замыкание).
// Эта команда просто отвечает "Pong", когда пользователь отправляет "/ping".
p.Command("ping", func(ctx *laniakea.MessageContext, data laniakea.NoData) error {
ctx.Answer("Pong")
return nil
})
// 6. Настраиваем бота: задаём шаблон ошибки и добавляем плагин.
// SetErrorTemplate устанавливает формат для сообщений об ошибках (где %s будет заменён на текст ошибки).
// AddPlugins(p) регистрирует наш плагин "ping" в боте.
bot = bot.SetErrorTemplate("Ошибка\n\n%s").AddPlugins(p)
// 7. Автоматически генерируем команды, такие как /start, /help и список всех зарегистрированных команд.
// Это необязательно, но очень полезно для большинства ботов.
if err := bot.AutoGenerateCommands(); err != nil {
log.Println(err)
}
// 8. Запускаем бота, начиная прослушивание обновлений (long polling).
if err := bot.Run(); err != nil {
log.Fatal(err)
}
}
Как это работает
BotOpts: Содержит конфигурацию, например, токен API.NewBot[T]: Создаёт экземпляр бота. Параметр типа T позволяет передать общие данные приложения (например, *sql.DB или контейнер сервисов), которые будут доступны во всех обработчиках. Используйте laniakea.NoData, если они не нужны.NewPlugin: Создаёт логическую группу для команд и Middleware.Command: Создаёт и регистрирует команду. Первый аргумент — имя команды без слеша, второй — функция-обработчик (func(*MessageContext, T) error).- Функции-обработчики: Получают *MessageContext (детали сообщения, методы типа Answer) и ваши данные приложения типа T, а ошибку возвращают для централизованной обработки.
SetErrorTemplate: Устанавливает шаблон для сообщений об ошибках. Плейсхолдер %s заменяется на текст ошибки.AutoGenerateCommands: Регистрирует команды из плагинов в Telegram для поддерживаемых scope.Run(): Запускает цикл опроса обновлений бота и возвращает ошибку, если старт или polling завершился неуспешно.RunWebhookWithContext(...): Запускает bot-owned webhook runtime, когда Telegram должен доставлять update по HTTP вместо long polling.- Экземпляр
Botодноразовый. После завершенияRun(),RunWithContext()илиRunWebhookWithContext()для следующего запуска создавайте новый бот.
Конфиг из файла
BotOpts можно не только собирать вручную или из environment, но и загружать и сохранять через file codec API.
Из коробки доступно:
BotOptsFileJSONCodecдля JSON-файлов.
Пример:
codec := laniakea.BotOptsFileJSONCodec{}
opts, err := laniakea.LoadBotOptsFile(codec, "config.json")
if err != nil {
log.Fatal(err)
}
bot, err := laniakea.NewBot[laniakea.NoData](opts)
if err != nil {
log.Fatal(err)
}
Плейсхолдеры вида {{ TG_TOKEN }} внутри файла перед декодированием разворачиваются из переменных окружения.
Для других форматов можно реализовать собственный codec через интерфейс BotOptsFileCodec.
Из коробки сейчас поддерживается только JSON. Если нужен другой формат, например TOML, используй BotOptsFileJSONCodec как эталонную реализацию собственного codec.
Подробности есть в wiki: Bot Options and Configuration RU
Webhook Runtime
Laniakea также поддерживает bot-owned webhook runtime через RunWebhookWithContext(...) и RunWebhook(...).
Используй его, когда:
- Telegram должен сам отправлять update на твой HTTP endpoint вместо polling.
- Ты хочешь, чтобы webhook-update проходили через ту же внутреннюю очередь, тот же worker pool, тех же runners и тот же single-use lifecycle, что и polling.
- Ты хочешь, чтобы Laniakea сама регистрировала webhook и владела локальным HTTP server.
Практические замечания:
- Задавай
BotWebhookOpts.SecretTokenдля аутентификации запросов. - Непустой
BotWebhookOpts.SecretTokenобязателен, если включёнBotWebhookOpts.UseStatusPath. - Используй явный
BotWebhookOpts.Path, а не/. - Если ты переводишь уже существующий deployment с webhook-режима на long polling, сначала удали webhook через
CloseWebhook()илиtgapi.DeleteWebhook(...). Пока webhook не удалён, Telegram продолжает доставку через него. - Запускай
RunWebhookWithContext(...)с cancelable context и после остановки runtime всё равно вызывайClose().
Полное руководство есть в wiki: Webhook Runtime
📖 Основные концепции
Плагины (Plugins)
Плагины — основной способ организации кода. Плагин может содержать несколько команд и Middleware.
plugin := laniakea.NewPlugin[*MyDB]("admin")
plugin.Command("ban", banUser)
bot.AddPlugins(plugin)
Команды (Commands)
Команда — это функция, которая обрабатывает конкретную команду бота (например, /start).
func myHandler(ctx *laniakea.MessageContext, db *MyDB) error {
// Доступ к аргументам команды через ctx.Args ([]string)
// Ответ пользователю: ctx.Answer("какой-то текст")
return nil
}
Контекст сообщения (MessageContext)
Предоставляет доступ к входящему сообщению и полезные методы для ответа:
Answer(text string): Отправляет сообщение с parse_mode none.AnswerLong(text string) []*AnswerMessage: Разбивает длинный plain text на несколько сообщений.AnswerMarkdown(text string): Отправляет сообщение, отформатированное MarkdownV2 (экранирование на вашей стороне).Keyboard(text string, keyboard *InlineKeyboard) *AnswerMessage: Отправляет сообщение с parse_mode none и Inline клавиатурой.KeyboardLong(text string, keyboard *InlineKeyboard) []*AnswerMessage: Разбивает длинный plain text на несколько сообщений и вешает клавиатуру на последний chunk.KeyboardMarkdown(text string, keyboard *InlineKeyboard) *AnswerMessage: Отправляет сообщение, отформатированное MarkdownV2 (экранирование на вашей стороне), и Inline клавиатурой.AnswerPhoto(photoID, text string) *AnswerMessage: Отправляет фотографию с подписью и parse_mode none.AnswerPhotoMarkdown(photoID, text string) *AnswerMessage: Отправляет фотографию с подписью, отформатированной MarkdownV2 (экранирование на вашей стороне).EditCallback(text string, keyboard *InlineKeyboard) *AnswerMessage: Редактирует сообщение сparse_modenone после нажатия inline-кнопки.EditCallbackMarkdown(text string, keyboard *InlineKeyboard) *AnswerMessage: Редактирует сообщение в формате MarkdownV2 (экранирование на вашей стороне) после нажатия inline-кнопки.SendAction(action tgapi.ChatActionType): Отправляет действие "печатает", "загружает фото" и т.д.- Поля:
Text,Args,From,FromID,Msg,InlineMsgID,CallbackQueryIDи другие. - И много других методов и полей!
App Data
Параметр типа T в NewBot[T] — мощная возможность. Вы можете передать любой тип, но для разделяемых зависимостей вроде пула соединений с БД, контейнера сервисов или API-клиента обычно стоит использовать pointer type.
type MyDB struct { /* ... */ }
db := &MyDB{...}
bot, err := laniakea.NewBot[*MyDB](opts)
if err != nil {
log.Fatal(err)
}
bot.SetAppData(db)
Сцены и сессии (Scenes and Sessions)
Сцены описывают многошаговые диалоги внутри плагина. Активная сцена хранится в session state, ключ которого зависит от scope, поэтому поток можно изолировать на пользователя, на чат или на пару пользователь-чат.
plugin := laniakea.NewPlugin[MyDB]("signup")
plugin.Scene("signup").
SetScope(laniakea.SceneScopeUserChat).
SetEntry("ask_name").
OnStep("ask_name", func(ctx *laniakea.SceneContext, db MyDB) (laniakea.SceneResult, error) {
if ctx.Text == "" {
ctx.Answer("Как тебя зовут?")
return ctx.Stay(), nil
}
if err := ctx.SaveData(struct {
Name string `json:"name"`
}{Name: ctx.Text}); err != nil {
return laniakea.SceneResult{}, err
}
ctx.Answer("Приятно познакомиться.")
return ctx.Next("done"), nil
}).
OnStep("done", func(ctx *laniakea.SceneContext, db MyDB) (laniakea.SceneResult, error) {
return ctx.Exit(), nil
})
- Используйте
ctx.EnterScene("signup"), чтобы войти в entry step, настроенный у сцены. - Используйте
ctx.EnterSceneStep("signup", "done"), если нужен явный стартовый step. - Из scene handler возвращайте
ctx.Stay(),ctx.Next(step),ctx.Exit()илиctx.Pass()для управления потоком. SceneActionPassне меняет текущую session state и продолжает обычный routing бота.- Для JSON-состояния сцены используйте
SceneContext.SaveData(...)иSceneContext.BindData(...). - Выбирайте
SceneScopeUser,SceneScopeChatилиSceneScopeUserChatв зависимости от того, насколько широко должен разделяться диалог.
⏱️ Раннеры (Runners)
Раннеры — фоновые задачи, которые выполняются вместе с bot runtime. Они регистрируются до запуска бота и автоматически запускаются при старте.
import "time"
// Одноразовый раннер — запускается один раз в горутине при старте (по умолчанию).
bot.AddRunner(
laniakea.NewRunner("seed-cache", func(b *laniakea.Bot[*MyDB]) error {
return b.GetAppData().SeedCache()
}),
)
// Периодический раннер — запускается каждые 10 минут в горутине.
bot.AddRunner(
laniakea.NewRunner("refresh-stats", func(b *laniakea.Bot[*MyDB]) error {
return b.GetAppData().RefreshStats()
}).Every(10 * time.Minute),
)
// Синхронный одноразовый — блокирует запуск runtime до завершения.
bot.AddRunner(
laniakea.NewRunner("migrate", func(b *laniakea.Bot[*MyDB]) error {
return b.GetAppData().Migrate()
}).Async(false),
)
Методы builder:
Async(bool) *Runner[T]— еслиtrue(по умолчанию), запускается в горутине; еслиfalse, блокирует запуск runtime.Every(time.Duration) *Runner[T]— задаёт интервал повторного запуска. Ноль (по умолчанию) означает одноразовый запуск; положительное значение — периодический. Периодические раннеры требуютAsync(true).
tgapi: API и Uploader
В tgapi есть два клиента:
APIдля JSON-запросов (SendMessage,EditMessageText, методы сfile_id/URL).Uploaderдля multipart-загрузок (SendPhoto,SendDocument,SendVideoс бинарными файлами).
Для продвинутых сценариев tgapi.NewRequest(...) и tgapi.NewUploaderRequest(...) остаются публичными low-level escape hatch API. Они менее безопасны, чем типизированные helper-методы: вызывающая сторона сама отвечает за корректное имя Telegram-метода и совместимые типы параметров/ответа.
🧩 Промежуточные слои (Middleware)
Middleware — это функции, которые выполняются перед обработчиком команды. Они идеально подходят для сквозных задач, таких как логирование, контроль доступа, ограничение скорости запросов или модификация контекста.
Сигнатура
Функция middleware имеет ту же сигнатуру, что и обработчик команды, но должна возвращать bool:
func(ctx *MessageContext, db T) bool
- Если возвращается true, выполняется следующий middleware (или сама команда).
- Если возвращается false, цепочка выполнения немедленно прерывается (команда не запускается).
Добавление middleware
Используйте метод AddMiddleware плагина для добавления одной или нескольких функций middleware. Они выполняются в порядке добавления.
plugin := laniakea.NewPlugin[*MyDB]("admin")
plugin.AddMiddleware(laniakea.NewMiddleware("logging", loggingMiddleware))
plugin.AddMiddleware(laniakea.NewMiddleware("admin-only", adminOnlyMiddleware))
plugin.Command("ban", banUser)
Примеры middleware
- Логирующий middleware – логирует каждое выполнение команды.
func loggingMiddleware(ctx *laniakea.MessageContext, db *MyDB) bool {
log.Printf("Пользователь %d выполнил команду: %s", ctx.FromID, ctx.Msg.Text)
return true // продолжаем к следующему middleware/команде
}
- Middleware только для администраторов – ограничивает доступ пользователям с определённой ролью.
func adminOnlyMiddleware(ctx *laniakea.MessageContext, db *MyDB) bool {
if !db.IsAdmin(ctx.FromID) { // предполагается, что db имеет метод IsAdmin
ctx.Answer("⛔ Доступ запрещён. Только для администраторов.")
return false // останавливаем выполнение
}
return true
}
Важные замечания
- Middleware может изменять MessageContext (например, добавлять пользовательские поля) перед запуском команды.
⚙️ Расширенная настройка
- Инлайн-клавиатуры: Создавайте клавиатуры с помощью
laniakea.NewInlineKeyboardJSON,laniakea.NewInlineKeyboardBase64илиlaniakea.NewInlineKeyboard.Bot.SetPayloadType(...)задаёт payload format по умолчанию, аInlineKeyboard.SetPayloadType(...)переопределяет его для конкретной клавиатуры. - Ограничение запросов: Передайте настроенный
utils.RateLimiterчерезBotOptsдля корректной обработки лимитов Telegram. - Локализация:
L10nбезопасен для конкурентного использования после подключения к боту. - Пользовательские update handlers: Используйте
plugin.AddUpdateHandler(...)для Telegram update types вне command/payload flow. - Жизненный цикл:
RunWithContext(...)иRunWebhookWithContext(...)не вызываютClose()автоматически. Завершайте бот явно и создавайте новыйBotдля следующего запуска.
Обработка Telegram Updates
- Команды и payload-ы обрабатываются через плагины.
- Для некомандных update-ов можно зарегистрировать обработчик через
plugin.AddUpdateHandler(updateType, handler). message,channel_postиcallback_queryостаются в command/payload flow.- После JSON-декодирования
tgapi.Updateзаполняет полеType, чтобы обработчики могли явно видеть итоговый вид update.
📝 Лицензия
Этот проект лицензирован под GNU General Public License v3.0 - подробности см. в файле LICENSE.
📚 Дополнительная информация
✅ Создано с ❤️ scuroneko
