REPOSITORY / ScuroNeko/Laniakea

Wiki

KNOWLEDGE REPOSITORY

update for v1.0.0

2026-05-20 13:22:27 +03:00
parent c8d9eae25c
commit 2d6734663f
32 changed files with 334 additions and 289 deletions
+1 -1
@@ -116,7 +116,7 @@ There are two skip levels:
### Skip a single command ### Skip a single command
```go ```go
plugin.NewCommand(exec, "internal"). plugin.Command("internal", exec).
SetDescription("Internal only"). SetDescription("Internal only").
SkipCommandAutoGen() SkipCommandAutoGen()
``` ```
+6 -6
@@ -9,7 +9,7 @@ English version: [[Bot-Lifecycle]]
1. Собрать `BotOpts`. 1. Собрать `BotOpts`.
2. Создать `Bot` через `NewBot[T](opts)`. 2. Создать `Bot` через `NewBot[T](opts)`.
3. Полностью настроить бот: плагины, middleware, фоновые задачи, политику данных callback, l10n и app data. 3. Полностью настроить бот: плагины, middleware, фоновые задачи, политику данных callback, l10n и app data.
4. Запустить через `Run()`, `RunWithContext(...)` или `RunWebHookWithContext(...)`. 4. Запустить через `Run()`, `RunWithContext(...)` или `RunWebhookWithContext(...)`.
5. Остановить выполнение через завершение runtime или отмену context. 5. Остановить выполнение через завершение runtime или отмену context.
6. Освободить локальные ресурсы через `Close()`. 6. Освободить локальные ресурсы через `Close()`.
7. Для следующего запуска создать новый `Bot`. 7. Для следующего запуска создать новый `Bot`.
@@ -66,7 +66,7 @@ English version: [[Bot-Lifecycle]]
1. Построение и настройка `Bot` после `NewBot[T](opts)`. 1. Построение и настройка `Bot` после `NewBot[T](opts)`.
2. Снимок конфигурации плагина в `AddPlugins(...)`. 2. Снимок конфигурации плагина в `AddPlugins(...)`.
3. Фиксация bot-level конфигурации после первого `Run()`, `RunWithContext(...)` или `RunWebHookWithContext(...)`. 3. Фиксация bot-level конфигурации после первого `Run()`, `RunWithContext(...)` или `RunWebhookWithContext(...)`.
Практически это значит: Практически это значит:
- структуру плагина нужно закончить до `AddPlugins(...)`; - структуру плагина нужно закончить до `AddPlugins(...)`;
@@ -85,7 +85,7 @@ Laniakea предпочитает предсказуемый no-op вместо
- путаницу в том, влияет ли изменение только на будущие update или ещё и на уже принятую работу; - путаницу в том, влияет ли изменение только на будущие update или ещё и на уже принятую работу;
- разные ментальные модели для snapshot-поведения плагинов и bot-level состояния. - разные ментальные модели для snapshot-поведения плагинов и bot-level состояния.
## `Run()`, `RunWithContext(...)` и `RunWebHookWithContext(...)` ## `Run()`, `RunWithContext(...)` и `RunWebhookWithContext(...)`
`Run()` — это короткая форма для простых случаев. `Run()` — это короткая форма для простых случаев.
@@ -94,11 +94,11 @@ Laniakea предпочитает предсказуемый no-op вместо
- ждет завершения queued updates; - ждет завершения queued updates;
- корректно дожидается фоновых задач. - корректно дожидается фоновых задач.
`RunWebHookWithContext(...)` — webhook-вариант runtime. Он использует тот же single-use контракт, тот же запуск runners, ту же очередь обновлений и ту же worker-pool обработку. `RunWebhookWithContext(...)` — webhook-вариант runtime. Он использует тот же single-use контракт, тот же запуск runners, ту же очередь обновлений и ту же worker-pool обработку.
Если bot уже был запущен раньше, повторный запуск вернет `ErrBotAlreadyRun`. Если bot уже был запущен раньше, повторный запуск вернет `ErrBotAlreadyRun`.
Если ты переводишь уже существующий deployment с webhook-доставки на polling, сначала удали текущий webhook через `CloseWebHook()` или низкоуровневый `tgapi.DeleteWebhook(...)`. Пока webhook не удалён, Telegram продолжает доставлять update через него. Если ты переводишь уже существующий deployment с webhook-доставки на polling, сначала удали текущий webhook через `CloseWebhook()` или низкоуровневый `tgapi.DeleteWebhook(...)`. Пока webhook не удалён, Telegram продолжает доставлять update через него.
Для webhook-специфичных опций, транспортного поведения и практических советов смотри [[Webhook-Runtime-RU]]. Для webhook-специфичных опций, транспортного поведения и практических советов смотри [[Webhook-Runtime-RU]].
@@ -131,7 +131,7 @@ Laniakea предпочитает предсказуемый no-op вместо
Обычно боту нужен именно `Close()`. Обычно боту нужен именно `Close()`.
`RunWithContext(...)` и `RunWebHookWithContext(...)` не заменяют `Close()`: локальные ресурсы всё равно нужно закрывать отдельно. `RunWithContext(...)` и `RunWebhookWithContext(...)` не заменяют `Close()`: локальные ресурсы всё равно нужно закрывать отдельно.
## Частые ошибки ## Частые ошибки
+10 -10
@@ -7,7 +7,7 @@ This page explains how a `Bot` is created, configured, started, stopped, and ret
## Lifecycle at a glance ## Lifecycle at a glance
1. Build `BotOpts` and call `NewBot`. 1. Build `BotOpts` and call `NewBot`.
2. Configure the bot instance: prefixes, plugins, middleware, runners, localization, payload defaults, and optional app data. 2. Configure the bot instance: prefixes, plugins, middleware, runners, localization, payload defaults, and optional app data.
3. Start it with `RunWithContext(ctx)`, `Run()`, or `RunWebHookWithContext(...)`. 3. Start it with `RunWithContext(ctx)`, `Run()`, or `RunWebhookWithContext(...)`.
4. Stop runtime by canceling the context or letting the run method return. 4. Stop runtime by canceling the context or letting the run method return.
5. Call `Close()` to release local resources. 5. Call `Close()` to release local resources.
6. Create a new `Bot` if you need another run. 6. Create a new `Bot` if you need another run.
@@ -66,7 +66,7 @@ Laniakea has three practical configuration phases:
1. Construction and bot setup after `NewBot[T](opts)`. 1. Construction and bot setup after `NewBot[T](opts)`.
2. Plugin snapshotting at `AddPlugins(...)`. 2. Plugin snapshotting at `AddPlugins(...)`.
3. Runtime freeze after the first `Run()`, `RunWithContext(...)`, or `RunWebHookWithContext(...)`. 3. Runtime freeze after the first `Run()`, `RunWithContext(...)`, or `RunWebhookWithContext(...)`.
That means: That means:
- finish plugin structure before `AddPlugins(...)`; - finish plugin structure before `AddPlugins(...)`;
@@ -97,10 +97,10 @@ if err != nil {
defer bot.Close() defer bot.Close()
plugin := laniakea.NewPlugin[laniakea.NoData]("main") plugin := laniakea.NewPlugin[laniakea.NoData]("main")
plugin.NewCommand(func(ctx *laniakea.MsgContext, db laniakea.NoData) error { plugin.Command("ping", func(ctx *laniakea.MsgContext, db laniakea.NoData) error {
ctx.Answer("pong") ctx.Answer("pong")
return nil return nil
}, "ping") })
bot.AddPlugins(plugin) bot.AddPlugins(plugin)
@@ -109,7 +109,7 @@ if err := bot.Run(); err != nil {
} }
``` ```
## `RunWithContext(...)`, `Run()`, and `RunWebHookWithContext(...)` ## `RunWithContext(...)`, `Run()`, and `RunWebhookWithContext(...)`
`RunWithContext(ctx)` is the main polling runtime entry point. `RunWithContext(ctx)` is the main polling runtime entry point.
@@ -124,16 +124,16 @@ It:
`Run()` is only a shorthand for `RunWithContext(context.Background())`. `Run()` is only a shorthand for `RunWithContext(context.Background())`.
`RunWebHookWithContext(...)` is the webhook runtime entry point. It shares the same: `RunWebhookWithContext(...)` is the webhook runtime entry point. It shares the same:
- single-use rule; - single-use rule;
- runner startup behavior; - runner startup behavior;
- internal update queue; - internal update queue;
- worker-pool dispatch model; - worker-pool dispatch model;
- graceful shutdown semantics. - graceful shutdown semantics.
Use `RunWithContext(...)` for production services that poll Telegram directly. Use `RunWebHookWithContext(...)` when Telegram should deliver updates through your HTTP endpoint. Use `RunWithContext(...)` for production services that poll Telegram directly. Use `RunWebhookWithContext(...)` when Telegram should deliver updates through your HTTP endpoint.
If you switch an existing deployment from webhook delivery to polling, remove the current webhook first with `CloseWebHook()` or low-level `tgapi.DeleteWebhook(...)`. Telegram keeps webhook delivery active until the webhook is deleted. If you switch an existing deployment from webhook delivery to polling, remove the current webhook first with `CloseWebhook()` or low-level `tgapi.DeleteWebhook(...)`. Telegram keeps webhook delivery active until the webhook is deleted.
For the webhook-specific option model, transport behavior, and operational guidance, see [[Webhook-Runtime]]. For the webhook-specific option model, transport behavior, and operational guidance, see [[Webhook-Runtime]].
@@ -172,7 +172,7 @@ Canceling the runtime context tells the bot to stop accepting new work and finis
- one-time async runners to finish; - one-time async runners to finish;
- background runners to exit after noticing `ctx.Done()`. - background runners to exit after noticing `ctx.Done()`.
`RunWithContext(...)` and `RunWebHookWithContext(...)` do not automatically release API, uploader, or logger resources. You still need to call `Close()`. `RunWithContext(...)` and `RunWebhookWithContext(...)` do not automatically release API, uploader, or logger resources. You still need to call `Close()`.
## `Close()` versus `CloseRemote()` ## `Close()` versus `CloseRemote()`
@@ -192,7 +192,7 @@ Use `CloseRemote(ctx)` only when you specifically need Telegram-side session shu
## Single-use rule ## Single-use rule
A `Bot` cannot be started twice. After `Run()`, `RunWithContext(...)`, or `RunWebHookWithContext(...)` returns, later start attempts fail with `ErrBotAlreadyRun`. A `Bot` cannot be started twice. After `Run()`, `RunWithContext(...)`, or `RunWebhookWithContext(...)` returns, later start attempts fail with `ErrBotAlreadyRun`.
That means: That means:
- do not call a runtime entry point again after a graceful stop; - do not call a runtime entry point again after a graceful stop;
+4 -4
@@ -46,10 +46,10 @@ opts := laniakea.LoadOptsFromEnv()
`LoadBotOptsFile(...)` подходит, когда конфиг бота удобнее хранить в отдельном файле. `LoadBotOptsFile(...)` подходит, когда конфиг бота удобнее хранить в отдельном файле.
Из коробки доступно: Из коробки доступно:
- `BotOptsFileJsonCodec` для JSON. - `BotOptsFileJSONCodec` для JSON.
```go ```go
codec := laniakea.BotOptsFileJsonCodec{} codec := laniakea.BotOptsFileJSONCodec{}
opts, err := laniakea.LoadBotOptsFile(codec, "config.json") opts, err := laniakea.LoadBotOptsFile(codec, "config.json")
if err != nil { if err != nil {
return err return err
@@ -67,7 +67,7 @@ if err := laniakea.SaveBotOptsFile(codec, "config.json", opts); err != nil {
Перед декодированием loader разворачивает плейсхолдеры вроде `{{ TG_TOKEN }}` из environment variables. Перед декодированием loader разворачивает плейсхолдеры вроде `{{ TG_TOKEN }}` из environment variables.
Из коробки библиотека пока поддерживает только JSON. Для других форматов можно реализовать свой codec через `BotOptsFileCodec`. Из коробки библиотека пока поддерживает только JSON. Для других форматов можно реализовать свой codec через `BotOptsFileCodec`.
Если нужен другой формат, например TOML, используй `BotOptsFileJsonCodec` как референсную реализацию собственного codec. Если нужен другой формат, например TOML, используй `BotOptsFileJSONCodec` как референсную реализацию собственного codec.
## Что обязательно ## Что обязательно
@@ -128,7 +128,7 @@ if err := laniakea.SaveBotOptsFile(codec, "config.json", opts); err != nil {
Управляют записью логов в файлы. Управляют записью логов в файлы.
### `UseTestServer` и `APIUrl` ### `UseTestServer` и `APIURL`
Полезны для тестового окружения, proxy или собственного Telegram gateway. Полезны для тестового окружения, proxy или собственного Telegram gateway.
+5 -5
@@ -58,10 +58,10 @@ This is often the easiest approach for containers, CI, and production services.
Use `LoadBotOptsFile(...)` when you want to keep bot configuration in a checked-in or deployment-managed config file. Use `LoadBotOptsFile(...)` when you want to keep bot configuration in a checked-in or deployment-managed config file.
Built in: Built in:
- `BotOptsFileJsonCodec` for JSON files. - `BotOptsFileJSONCodec` for JSON files.
```go ```go
codec := laniakea.BotOptsFileJsonCodec{} codec := laniakea.BotOptsFileJSONCodec{}
opts, err := laniakea.LoadBotOptsFile(codec, "config.json") opts, err := laniakea.LoadBotOptsFile(codec, "config.json")
if err != nil { if err != nil {
return err return err
@@ -84,7 +84,7 @@ if err := laniakea.SaveBotOptsFile(codec, "config.json", opts); err != nil {
Before decoding, the loader expands placeholders like `{{ TG_TOKEN }}` from environment variables. Before decoding, the loader expands placeholders like `{{ TG_TOKEN }}` from environment variables.
Only JSON support is built into the library right now. If you want another format, implement `BotOptsFileCodec` yourself. Only JSON support is built into the library right now. If you want another format, implement `BotOptsFileCodec` yourself.
Use `BotOptsFileJsonCodec` as the reference implementation for custom codecs such as TOML. Use `BotOptsFileJSONCodec` as the reference implementation for custom codecs such as TOML.
## Required setting ## Required setting
@@ -222,12 +222,12 @@ Env:
Use this only for development and testing scenarios that explicitly target Telegram's test environment. Use this only for development and testing scenarios that explicitly target Telegram's test environment.
### `APIUrl` ### `APIURL`
Overrides the default Telegram API base URL. Overrides the default Telegram API base URL.
Setter: Setter:
- `SetAPIUrl(url)` - `SetAPIURL(url)`
Env: Env:
- `API_URL` - `API_URL`
+11 -11
@@ -73,7 +73,7 @@ func start(ctx *laniakea.MsgContext, db *App) error {
```go ```go
plugin := laniakea.NewPlugin[*App]("main") plugin := laniakea.NewPlugin[*App]("main")
plugin.AddCommand(plugin.NewCommand(start, "start")) plugin.Command("start", start)
``` ```
Важно: Важно:
@@ -102,12 +102,12 @@ plugin.AddCommand(plugin.NewCommand(start, "start"))
Пример: Пример:
```go ```go
plugin.AddCommand( plugin.Command(
plugin.NewCommand(banUser, "ban", "ban",
laniakea.NewCommandArg("user_id"). banUser,
SetValueType(laniakea.CommandValueIntType). laniakea.NewCommandArg("user_id").
SetRequired(), SetValueType(laniakea.CommandValueInt).
), SetRequired(),
) )
``` ```
@@ -130,7 +130,7 @@ func confirmDelete(ctx *laniakea.MsgContext, db *App) error {
return nil return nil
} }
plugin.AddPayload(plugin.NewPayload(confirmDelete, "delete.confirm")) plugin.Payload("delete.confirm", confirmDelete)
``` ```
Важно помнить: Важно помнить:
@@ -204,7 +204,7 @@ plugin.AddMiddleware(...)
Добавляется через: Добавляется через:
```go ```go
plugin.NewCommand(handler, "name").Use(middleware) plugin.Command("name", handler).Use(middleware)
``` ```
Подходит, когда проверка нужна только одной команде или одному обработчику данных callback. Подходит, когда проверка нужна только одной команде или одному обработчику данных callback.
@@ -218,13 +218,13 @@ plugin.NewCommand(handler, "name").Use(middleware)
Неправильно: Неправильно:
```go ```go
plugin.NewCommand(start, "/start") plugin.Command("/start", start)
``` ```
Правильно: Правильно:
```go ```go
plugin.NewCommand(start, "start") plugin.Command("start", start)
``` ```
### Считать данные callback обычной командой ### Считать данные callback обычной командой
+19 -19
@@ -71,11 +71,11 @@ func start(ctx *laniakea.MsgContext, db *App) error {
## Registering commands ## Registering commands
Create a command with `NewCommand(...)` and add it to a plugin: Create and register a command with `Plugin.Command(...)`:
```go ```go
plugin := laniakea.NewPlugin[*App]("main") plugin := laniakea.NewPlugin[*App]("main")
plugin.AddCommand(plugin.NewCommand(start, "start")) plugin.Command("start", start)
``` ```
The command name: The command name:
@@ -99,7 +99,7 @@ func echo(ctx *laniakea.MsgContext, db laniakea.NoData) error {
return nil return nil
} }
plugin.AddCommand(plugin.NewCommand(echo, "echo")) plugin.Command("echo", echo)
``` ```
For `/echo hello world`: For `/echo hello world`:
@@ -113,12 +113,12 @@ You can declare command arguments using `CommandArg`.
Example: Example:
```go ```go
plugin.AddCommand( plugin.Command(
plugin.NewCommand(banUser, "ban", "ban",
laniakea.NewCommandArg("user_id"). banUser,
SetValueType(laniakea.CommandValueIntType). laniakea.NewCommandArg("user_id").
SetRequired(), SetValueType(laniakea.CommandValueInt).
), SetRequired(),
) )
``` ```
@@ -133,7 +133,7 @@ If validation fails, the command does not run and the bot error path is used.
Payload handlers are for callback data coming from inline keyboard buttons. Payload handlers are for callback data coming from inline keyboard buttons.
Register them with `NewPayload(...)` or `AddPayload(...)`: Register them with `Plugin.Payload(...)` or `AddPayload(...)`:
```go ```go
func confirmDelete(ctx *laniakea.MsgContext, db *App) error { func confirmDelete(ctx *laniakea.MsgContext, db *App) error {
@@ -141,7 +141,7 @@ func confirmDelete(ctx *laniakea.MsgContext, db *App) error {
return nil return nil
} }
plugin.AddPayload(plugin.NewPayload(confirmDelete, "delete.confirm")) plugin.Payload("delete.confirm", confirmDelete)
``` ```
Payload handlers: Payload handlers:
@@ -213,7 +213,7 @@ Use this for logic shared by most handlers in the plugin.
Added with: Added with:
```go ```go
plugin.NewCommand(handler, "name").Use(middleware) plugin.Command("name", handler).Use(middleware)
``` ```
Use this when only one command or payload needs the check. Use this when only one command or payload needs the check.
@@ -243,20 +243,20 @@ admin.AddMiddleware(laniakea.NewMiddleware("admin-only", func(ctx *laniakea.MsgC
return true return true
})) }))
admin.AddCommand(admin.NewCommand(func(ctx *laniakea.MsgContext, app *App) error { admin.Command("ban", func(ctx *laniakea.MsgContext, app *App) error {
ctx.Answer("Banned") ctx.Answer("Banned")
return nil return nil
}, "ban")) })
``` ```
### Example: payload handler for inline keyboard callback ### Example: payload handler for inline keyboard callback
```go ```go
plugin.AddPayload(plugin.NewPayload(func(ctx *laniakea.MsgContext, app *App) error { plugin.Payload("approve", func(ctx *laniakea.MsgContext, app *App) error {
ctx.AnswerCbQueryText("Accepted") ctx.AnswerCallbackText("Accepted")
ctx.EditCallback("Done", nil) ctx.EditCallback("Done", nil)
return nil return nil
}, "approve")) })
``` ```
## Common mistakes ## Common mistakes
@@ -266,13 +266,13 @@ plugin.AddPayload(plugin.NewPayload(func(ctx *laniakea.MsgContext, app *App) err
Wrong: Wrong:
```go ```go
plugin.NewCommand(start, "/start") plugin.Command("/start", start)
``` ```
Right: Right:
```go ```go
plugin.NewCommand(start, "start") plugin.Command("start", start)
``` ```
### Treating payloads like commands ### Treating payloads like commands
+2 -2
@@ -118,8 +118,8 @@ That last detail matters: if a `Push(...)` call makes the draft too large, you s
Each draft gets a provider-generated `ID`. Each draft gets a provider-generated `ID`.
ID generation modes: ID generation modes:
- random IDs from `RandomDraftIdGenerator`; - random IDs from `RandomDraftIDGenerator`;
- monotonic IDs from `LinearDraftIdGenerator`. - monotonic IDs from `LinearDraftIDGenerator`.
The ID is mainly useful when: The ID is mainly useful when:
- correlating draft activity in logs; - correlating draft activity in logs;
+1 -1
@@ -88,7 +88,7 @@ if err := db.WarmCache(); err != nil {
Для callback-потока возвращённая ошибка превращается не в обычное сообщение в чат, а в ответ на callback query. Для callback-потока возвращённая ошибка превращается не в обычное сообщение в чат, а в ответ на callback query.
Если нужен другой UX, лучше: Если нужен другой UX, лучше:
- вызвать `AnswerCbQueryText(...)` или `AnswerCbQueryAlert(...)`; - вызвать `AnswerCallbackText(...)` или `AnswerCallbackAlert(...)`;
- вернуть `nil`. - вернуть `nil`.
## `ErrorTemplate(...)` ## `ErrorTemplate(...)`
+11 -11
@@ -133,10 +133,10 @@ This matters because callback error UX is different:
- no new chat message is posted for the error path. - no new chat message is posted for the error path.
If you want a different callback UX, answer manually with: If you want a different callback UX, answer manually with:
- `AnswerCbQuery()` - `AnswerCallback()`
- `AnswerCbQueryText(...)` - `AnswerCallbackText(...)`
- `AnswerCbQueryAlert(...)` - `AnswerCallbackAlert(...)`
- `AnswerCbQueryUrl(...)` - `AnswerCallbackURL(...)`
and return `nil`. and return `nil`.
@@ -183,38 +183,38 @@ Related page:
### Centralized command failure ### Centralized command failure
```go ```go
plugin.NewCommand(func(ctx *laniakea.MsgContext, db *App) error { plugin.Command("report", func(ctx *laniakea.MsgContext, db *App) error {
result, err := db.DoWork() result, err := db.DoWork()
if err != nil { if err != nil {
return fmt.Errorf("failed to build report: %w", err) return fmt.Errorf("failed to build report: %w", err)
} }
ctx.Answer(result) ctx.Answer(result)
return nil return nil
}, "report") })
``` ```
### Manual denial response ### Manual denial response
```go ```go
plugin.NewCommand(func(ctx *laniakea.MsgContext, db *App) error { plugin.Command("admin", func(ctx *laniakea.MsgContext, db *App) error {
if ctx.From == nil || !db.Allowed(ctx.From.ID) { if ctx.From == nil || !db.Allowed(ctx.From.ID) {
ctx.Answer("Access denied") ctx.Answer("Access denied")
return nil return nil
} }
return doProtectedWork(ctx, db) return doProtectedWork(ctx, db)
}, "admin") })
``` ```
### Callback-specific manual alert ### Callback-specific manual alert
```go ```go
plugin.NewPayload(func(ctx *laniakea.MsgContext, db *App) error { plugin.Payload("start", func(ctx *laniakea.MsgContext, db *App) error {
if !ready { if !ready {
ctx.AnswerCbQueryAlert("This action is not available yet") ctx.AnswerCallbackAlert("This action is not available yet")
return nil return nil
} }
return nil return nil
}, "start") })
``` ```
## Recommendations ## Recommendations
+7 -7
@@ -9,7 +9,7 @@
Текущее состояние: Текущее состояние:
- В репозитории уже есть низкоуровневые API для настройки webhook на уровне `tgapi`: `SetWebhook(...)`, `DeleteWebhook(...)`, `GetWebhookInfo(...)`, а также поддержка загрузки сертификата через uploader. - В репозитории уже есть низкоуровневые API для настройки webhook на уровне `tgapi`: `SetWebhook(...)`, `DeleteWebhook(...)`, `GetWebhookInfo(...)`, а также поддержка загрузки сертификата через uploader.
- Во фреймворке теперь есть полноценные bot-level точки входа webhook runtime: `RunWebHookWithContext(...)` и `RunWebHook(...)`. - Во фреймворке теперь есть полноценные bot-level точки входа webhook runtime: `RunWebhookWithContext(...)` и `RunWebhook(...)`.
- Webhook-доставка теперь использует ту же внутреннюю очередь update-ов, тот же worker pool, тот же запуск runners и тот же single-use runtime contract, что и polling. - Webhook-доставка теперь использует ту же внутреннюю очередь update-ов, тот же worker pool, тот же запуск runners и тот же single-use runtime contract, что и polling.
- Поведение webhook runtime, security-модель и правила перехода обратно на polling теперь описаны в основной документации и wiki. - Поведение webhook runtime, security-модель и правила перехода обратно на polling теперь описаны в основной документации и wiki.
@@ -21,12 +21,12 @@
Что теперь есть: Что теперь есть:
- `BotWebHookOpts`, `NewBotWebHookOpts()` и fluent helper-методы для webhook-конфигурации. - `BotWebhookOpts`, `NewBotWebhookOpts()` и fluent helper-методы для webhook-конфигурации.
- `RunWebHookWithContext(...)` и `RunWebHook(...)` как bot-owned точки входа runtime. - `RunWebhookWithContext(...)` и `RunWebhook(...)` как bot-owned точки входа runtime.
- Общая queued dispatch-модель, worker-pool обработка, запуск runners и single-use semantics для polling и webhook mode. - Общая queued dispatch-модель, worker-pool обработка, запуск runners и single-use semantics для polling и webhook mode.
- Fallback webhook `AllowedUpdates` к bot-level конфигурации типов update. - Fallback webhook `AllowedUpdates` к bot-level конфигурации типов update.
- Валидация webhook path и количества TLS-файлов до remote webhook setup. - Валидация webhook path и количества TLS-файлов до remote webhook setup.
- Явное удаление remote webhook через `CloseWebHook()` или низкоуровневый `tgapi.DeleteWebhook(...)` при переходе deployment с webhook-доставки обратно на polling. - Явное удаление remote webhook через `CloseWebhook()` или низкоуровневый `tgapi.DeleteWebhook(...)` при переходе deployment с webhook-доставки обратно на polling.
- Регрессионные тесты на queue delivery, запуск runners, single-use behavior, rejection слишком большого body, path/TLS validation и auth-поведение status endpoint. - Регрессионные тесты на queue delivery, запуск runners, single-use behavior, rejection слишком большого body, path/TLS validation и auth-поведение status endpoint.
Практическая цель: Практическая цель:
@@ -117,7 +117,7 @@
Текущее состояние: Текущее состояние:
- Фреймворк теперь считает конфигурацию бота структурно завершённой после начала первого runtime entry point: `Run()`, `RunWithContext(...)` или `RunWebHookWithContext(...)`. - Фреймворк теперь считает конфигурацию бота структурно завершённой после начала первого runtime entry point: `Run()`, `RunWithContext(...)` или `RunWebhookWithContext(...)`.
- Поздние bot-level попытки мутации больше не применяются частично после старта runtime. - Поздние bot-level попытки мутации больше не применяются частично после старта runtime.
- Границы между регистрацией плагинов, стартом runtime и фиксацией конфигурации теперь оформлены как явное поведение фреймворка и закреплены тестами. - Границы между регистрацией плагинов, стартом runtime и фиксацией конфигурации теперь оформлены как явное поведение фреймворка и закреплены тестами.
@@ -189,7 +189,7 @@
Текущее направление API: Текущее направление API:
- `Scene`, `SceneContext`, `SceneSession` и `SessionStore`. - `Scene`, `SceneContext`, `SceneSession` и `SessionStore`.
- `Plugin.NewScene(...)` и `Plugin.AddScene(...)`. - `Plugin.Scene(...)` и `Plugin.AddScene(...)`.
- `MsgContext.EnterScene(...)`, `EnterSceneStep(...)` и `ExitScene(...)`. - `MsgContext.EnterScene(...)`, `EnterSceneStep(...)` и `ExitScene(...)`.
- `SceneContext.Stay()`, `Next(...)`, `Exit()`, `Pass()`, `BindData(...)` и `SaveData(...)`. - `SceneContext.Stay()`, `Next(...)`, `Exit()`, `Pass()`, `BindData(...)` и `SaveData(...)`.
- Состояние на пользователя или чат с хранением в `SessionStore` и чистым интерфейсом для собственного постоянного хранения. - Состояние на пользователя или чат с хранением в `SessionStore` и чистым интерфейсом для собственного постоянного хранения.
@@ -263,7 +263,7 @@ func ban(ctx *laniakea.MsgContext, db *App) error {
Текущее состояние: Текущее состояние:
- `RunWithContext(...)` и `RunWebHookWithContext(...)` управляют жизненным циклом выполнения бота и корректным завершением. - `RunWithContext(...)` и `RunWebhookWithContext(...)` управляют жизненным циклом выполнения бота и корректным завершением.
- `tgapi` уже поддерживает методы, принимающие `context.Context`. - `tgapi` уже поддерживает методы, принимающие `context.Context`.
- Обычные обработчики не получают полноценный `context.Context`, привязанный к обработке конкретного запроса. - Обычные обработчики не получают полноценный `context.Context`, привязанный к обработке конкретного запроса.
+52 -7
@@ -4,12 +4,57 @@ This page tracks framework-level backlog items that are about missing concepts i
## Done ## Done
### [1.0.0]
### Major — close before 1.0.0 tag
- [X] **M1. `BotPayloadType*` are `var`, must be `const`**`bot.go:50-59`. Public sentinels are user-mutable globals. `KeyboardButtonStyle*` in `keyboard.go:10-17` already uses `const`; match the pattern.
- [X] **M2. `Observer` method naming asymmetry**`observer.go:147-157`. `OnReceiveUpdate``OnUpdateReceived`; `OnHandledUpdate``OnUpdateHandled` to match `UpdateReceivedEvent` / `UpdateHandledEvent` and the rest of the `OnX` pattern. Breaking after 1.0.
- [X] **M3. Uploader returns ad-hoc error string instead of `*ResponseError`**`tgapi/uploader_api.go:183`. `tgapi/api.go:258-292` returns `*ResponseError`; uploader must do the same so `errors.As(err, &tgapi.ResponseError{})` works for upload paths too.
- [X] **M4. `BotOptsFileJSON` is missing `PollTimeout`**`bot_opts_loader.go:35-46`, plus `FromBytes`/`ToBytes` mapping. File round-trip silently drops `PollTimeout`.
- [X] **M5. Stale `Bot.Updates` godoc**`methods.go:11-44`. Claims "30-second timeout" and "empty slice if none"; in reality timeout is `bot.pollTimeout` and the function returns `nil` on error.
- [X] **M6. Self-contradicting `NewRandomDraftProvider` godoc**`drafts.go:50-59`. Says "cryptographically secure random numbers" but uses `math/rand/v2` (the underlying generator type correctly notes it is not crypto-secure).
- [X] **M7. `Draft.Delete` godoc says "internal method"**`drafts.go:190-201`. Method is exported; either rewrite the godoc with a public-intent description or unexport.
- [X] **M8. Russian comments in production code**
- `msg_handler.go:28` — "Ищем команду по точному совпадению"
- `tgapi/uploader_api.go:181` — "Повторяем запрос"
- [X] **M9. `MessageContext.Error` godoc references unexported helper**`msg_context.go:540`. "Error is an alias for error()" — rewrite to describe the centralized handler error path and `IsUserError` gating.
- [X] **M10. `Scene` and `SceneSession` mix exported fields with setters**
- `Scene` exports `Name/Scope/Entry/PluginName` and also has `SetScope/SetEntry`; `PluginName` is framework-assigned but publicly mutable.
- `SceneSession` exports `Data []byte` and also has `Set/Get/HasData/ClearData/BindData/SaveData`.
- Pick one model per type before 1.0.0.
- [X] **M11. Constant-time compare for webhook secret**`bot_webhook.go:296` (update handler) and `bot_webhook.go:341` (`/status`). Use `subtle.ConstantTimeCompare`.
### Minor — can slip to 1.0.x
- [X] Strip `// Internal helper …` godoc from unexported funcs (~23 occurrences in repo); `AGENTS.md` explicitly forbids godoc-style comments on unexported declarations without a strong reason.
- [X] `Plugin.AddCommand` godoc references unexported field `.command``plugins.go:48-49`.
- [X] `Runner` builder naming: `runner.Once(true)`, `runner.Async(true)` read awkwardly; consider `SetOnce`/`SetAsync` to match `Set*` on other types, or zero-arg `Once()` + paired `Repeat(every)`.
- [X] Typo in webhook error string: `bot_webhook.go:143` — "MaxConnections must between 1 and 100" (missing `be`).
- [X] `RunWebhookWithContext` uses inline `errors.New(...)` instead of `Err*` sentinels (`bot_webhook.go:131-156`); rest of the package uses sentinels from `errors.go`.
- [X] `tgapi.UpdateTypeManagedBot` (`tgapi/types.go:61`) has no godoc.
- [X] `Bot.GetAPI`, `Bot.GetUploader`, `InlineKeyboard.GetMaxRow` have no godoc.
- [X] `Bot.L10n` godoc says "Returns empty string if translation not found"; actually returns the key (`l10n.go:48-59`).
- [X] `Bot.handle` panic recovery only logs — emit `ErrorEvent` so observers see panics (`handler.go:18-23`).
- [X] `handleCallback` vs `handleMessage` differ in plugin-logger assignment: callback assigns unconditionally then falls back to bot logger (`msg_handler.go:209-212`); message only assigns if non-nil (`msg_handler.go:35-37`). Align.
- [X] `SetCallbackData` godoc says "default payload type is JSON" — actually the zero `BotPayloadType` falls through to the `default` branch (which happens to be JSON). Either document the zero-value behavior explicitly or initialize the builder with the bot's default (`keyboard.go:106-122`).
- [X] `commands.go:62-66` — empty `case CommandValueAny:` next to `default: regex = nil` looks like an incomplete switch. Merge or add a one-line comment.
- [X] `Bot.SetDebug` does not call `configMutable` unlike sibling setters; if intentional, note it in godoc.
### Tests to add after the fixes
- [X] `BotOptsFileJSON` round-trip for `PollTimeout` (after M4).
- [X] Uploader 4xx/429 surfaces `*tgapi.ResponseError` (after M3).
- [X] `Bot.handle` panic → observer receives `ErrorEvent` (after panic-recovery fix).
- [X] Webhook `/status` with wrong `SecretToken` returns 403 / `403`-equivalent (after M11), incl. a constant-time-compare smoke.
- [X] Table-driven `parseCommand` cases for `/cmd@botname` and stripping behavior.
### [1.0.0-rc.14] Webhook runtime model ### [1.0.0-rc.14] Webhook runtime model
Current state: Current state:
- The repository already exposes low-level Telegram webhook setup APIs through `tgapi`, including `SetWebhook(...)`, `DeleteWebhook(...)`, `GetWebhookInfo(...)`, and uploader-based certificate upload support. - The repository already exposes low-level Telegram webhook setup APIs through `tgapi`, including `SetWebhook(...)`, `DeleteWebhook(...)`, `GetWebhookInfo(...)`, and uploader-based certificate upload support.
- The framework now exposes first-class bot-level webhook runtime entry points through `RunWebHookWithContext(...)` and `RunWebHook(...)`. - The framework now exposes first-class bot-level webhook runtime entry points through `RunWebhookWithContext(...)` and `RunWebhook(...)`.
- Webhook delivery now uses the same internal update queue, worker pool, runner startup model, and single-use runtime contract as polling. - Webhook delivery now uses the same internal update queue, worker pool, runner startup model, and single-use runtime contract as polling.
- The webhook runtime behavior, security model, and polling-transition requirements are now documented in the main docs and wiki. - The webhook runtime behavior, security model, and polling-transition requirements are now documented in the main docs and wiki.
@@ -21,12 +66,12 @@ Why this matters:
What is now present: What is now present:
- `BotWebHookOpts`, `NewBotWebHookOpts()`, and fluent helpers for webhook-specific configuration. - `BotWebhookOpts`, `NewBotWebhookOpts()`, and fluent helpers for webhook-specific configuration.
- `RunWebHookWithContext(...)` and `RunWebHook(...)` as bot-owned runtime entry points. - `RunWebhookWithContext(...)` and `RunWebhook(...)` as bot-owned runtime entry points.
- Shared queued update dispatch, worker-pool delivery, runner startup, and single-use run semantics between polling and webhook modes. - Shared queued update dispatch, worker-pool delivery, runner startup, and single-use run semantics between polling and webhook modes.
- Default fallback from webhook `AllowedUpdates` to the bot-level update type configuration. - Default fallback from webhook `AllowedUpdates` to the bot-level update type configuration.
- Request validation for webhook path shape and TLS file count before remote webhook setup. - Request validation for webhook path shape and TLS file count before remote webhook setup.
- Explicit remote webhook teardown through `CloseWebHook()` or low-level `tgapi.DeleteWebhook(...)` when switching a deployment from webhook delivery back to polling. - Explicit remote webhook teardown through `CloseWebhook()` or low-level `tgapi.DeleteWebhook(...)` when switching a deployment from webhook delivery back to polling.
- Regression coverage for queue delivery, runner startup, single-use behavior, body-size rejection, path and TLS validation, and status-endpoint auth behavior. - Regression coverage for queue delivery, runner startup, single-use behavior, body-size rejection, path and TLS validation, and status-endpoint auth behavior.
Practical target: Practical target:
@@ -117,7 +162,7 @@ Practical target:
Current state: Current state:
- The framework now treats bot configuration as structurally complete once the first runtime entry point begins: `Run()`, `RunWithContext(...)`, or `RunWebHookWithContext(...)`. - The framework now treats bot configuration as structurally complete once the first runtime entry point begins: `Run()`, `RunWithContext(...)`, or `RunWebhookWithContext(...)`.
- Late bot-level mutation attempts no longer partially apply after runtime startup. - Late bot-level mutation attempts no longer partially apply after runtime startup.
- Plugin registration and runtime configuration boundaries are now documented and tested as explicit framework behavior. - Plugin registration and runtime configuration boundaries are now documented and tested as explicit framework behavior.
@@ -189,7 +234,7 @@ What is still missing or not yet settled:
Current API direction: Current API direction:
- `Scene`, `SceneContext`, `SceneSession`, and `SessionStore`. - `Scene`, `SceneContext`, `SceneSession`, and `SessionStore`.
- `Plugin.NewScene(...)` and `Plugin.AddScene(...)`. - `Plugin.Scene(...)` and `Plugin.AddScene(...)`.
- `MsgContext.EnterScene(...)`, `EnterSceneStep(...)`, and `ExitScene(...)`. - `MsgContext.EnterScene(...)`, `EnterSceneStep(...)`, and `ExitScene(...)`.
- `SceneContext.Stay()`, `Next(...)`, `Exit()`, `Pass()`, `BindData(...)`, and `SaveData(...)`. - `SceneContext.Stay()`, `Next(...)`, `Exit()`, `Pass()`, `BindData(...)`, and `SaveData(...)`.
- Storage-backed per-user or per-chat state with a clean interface for custom persistence. - Storage-backed per-user or per-chat state with a clean interface for custom persistence.
@@ -263,7 +308,7 @@ Practical target:
Current state: Current state:
- `RunWithContext(...)` and `RunWebHookWithContext(...)` control bot runtime lifecycle and graceful shutdown. - `RunWithContext(...)` and `RunWebhookWithContext(...)` control bot runtime lifecycle and graceful shutdown.
- `tgapi` already supports context-aware methods. - `tgapi` already supports context-aware methods.
- Regular handlers do not receive a first-class request-scoped `context.Context`. - Regular handlers do not receive a first-class request-scoped `context.Context`.
+5 -5
@@ -48,7 +48,7 @@ func main() {
defer bot.Close() defer bot.Close()
plugin := laniakea.NewPlugin[laniakea.NoData]("main") plugin := laniakea.NewPlugin[laniakea.NoData]("main")
plugin.AddCommand(plugin.NewCommand(ping, "ping")) plugin.Command("ping", ping)
bot.AddPlugins(plugin) bot.AddPlugins(plugin)
@@ -100,7 +100,7 @@ bot.SetAppData(app)
```go ```go
plugin := laniakea.NewPlugin[laniakea.NoData]("admin") plugin := laniakea.NewPlugin[laniakea.NoData]("admin")
plugin.AddCommand(plugin.NewCommand(ping, "ping")) plugin.Command("ping", ping)
bot.AddPlugins(plugin) bot.AddPlugins(plugin)
``` ```
@@ -136,7 +136,7 @@ func profile(ctx *laniakea.MsgContext, db *App) error {
### 4. `Bot` single-use ### 4. `Bot` single-use
После `Run()`, `RunWithContext(...)` или `RunWebHookWithContext(...)` нельзя снова запускать тот же экземпляр `Bot`. После `Run()`, `RunWithContext(...)` или `RunWebhookWithContext(...)` нельзя снова запускать тот же экземпляр `Bot`.
Правильная модель: Правильная модель:
- создать bot - создать bot
@@ -151,7 +151,7 @@ func profile(ctx *laniakea.MsgContext, db *App) error {
### 5. `Close()` все равно нужен ### 5. `Close()` все равно нужен
Даже если ты используешь `Run()`, `RunWithContext(...)` или `RunWebHookWithContext(...)`, ресурсы нужно закрывать явно: Даже если ты используешь `Run()`, `RunWithContext(...)` или `RunWebhookWithContext(...)`, ресурсы нужно закрывать явно:
```go ```go
defer bot.Close() defer bot.Close()
@@ -168,7 +168,7 @@ defer bot.Close()
5. Добавить команды, payloads и middleware в плагины 5. Добавить команды, payloads и middleware в плагины
6. Зарегистрировать плагины через `AddPlugins(...)` 6. Зарегистрировать плагины через `AddPlugins(...)`
7. При необходимости вызвать `AutoGenerateCommands()` 7. При необходимости вызвать `AutoGenerateCommands()`
8. Вызвать `Run()`, `RunWithContext(...)` или `RunWebHookWithContext(...)` 8. Вызвать `Run()`, `RunWithContext(...)` или `RunWebhookWithContext(...)`
9. Закрыть bot через `Close()` 9. Закрыть bot через `Close()`
## Частые ошибки на старте ## Частые ошибки на старте
+6 -6
@@ -49,7 +49,7 @@ func main() {
defer bot.Close() defer bot.Close()
plugin := laniakea.NewPlugin[laniakea.NoData]("main") plugin := laniakea.NewPlugin[laniakea.NoData]("main")
plugin.AddCommand(plugin.NewCommand(ping, "ping")) plugin.Command("ping", ping)
bot.AddPlugins(plugin) bot.AddPlugins(plugin)
@@ -102,7 +102,7 @@ Example:
```go ```go
plugin := laniakea.NewPlugin[laniakea.NoData]("admin") plugin := laniakea.NewPlugin[laniakea.NoData]("admin")
plugin.AddCommand(plugin.NewCommand(ping, "ping")) plugin.Command("ping", ping)
bot.AddPlugins(plugin) bot.AddPlugins(plugin)
``` ```
@@ -139,7 +139,7 @@ func profile(ctx *laniakea.MsgContext, db *App) error {
A `Bot` instance is single-use. A `Bot` instance is single-use.
After `Run()`, `RunWithContext(...)`, or `RunWebHookWithContext(...)` returns: After `Run()`, `RunWithContext(...)`, or `RunWebhookWithContext(...)` returns:
- do not call a runtime entry point again on the same bot - do not call a runtime entry point again on the same bot
- create a new bot instance for the next run - create a new bot instance for the next run
@@ -149,7 +149,7 @@ See [[Bot-Lifecycle]] for details.
### 5. Always close the bot ### 5. Always close the bot
`Run()`, `RunWithContext(...)`, and `RunWebHookWithContext(...)` do not replace `Close()`. `Run()`, `RunWithContext(...)`, and `RunWebhookWithContext(...)` do not replace `Close()`.
You should still release bot-owned resources explicitly: You should still release bot-owned resources explicitly:
@@ -168,7 +168,7 @@ For most bots, this order is the least surprising:
5. Add commands, payloads, and middleware to plugins 5. Add commands, payloads, and middleware to plugins
6. Register plugins with `AddPlugins(...)` 6. Register plugins with `AddPlugins(...)`
7. Optionally call `AutoGenerateCommands()` 7. Optionally call `AutoGenerateCommands()`
8. Call `Run()`, `RunWithContext(...)`, or `RunWebHookWithContext(...)` 8. Call `Run()`, `RunWithContext(...)`, or `RunWebhookWithContext(...)`
9. Call `Close()` when done 9. Call `Close()` when done
## A slightly more realistic example ## A slightly more realistic example
@@ -207,7 +207,7 @@ func main() {
bot.SetAppData(&App{}) bot.SetAppData(&App{})
plugin := laniakea.NewPlugin[*App]("main") plugin := laniakea.NewPlugin[*App]("main")
plugin.AddCommand(plugin.NewCommand(echo, "echo")) plugin.Command("echo", echo)
bot.AddPlugins(plugin) bot.AddPlugins(plugin)
+5 -5
@@ -15,17 +15,17 @@ English version: [[Inline-Keyboards-and-Payloads]]
## Как строить клавиатуру ## Как строить клавиатуру
Основные конструкторы: Основные конструкторы:
- `NewInlineKeyboardJson(maxRow)` - `NewInlineKeyboardJSON(maxRow)`
- `NewInlineKeyboardBase64(maxRow)` - `NewInlineKeyboardBase64(maxRow)`
- `NewInlineKeyboard(payloadType, maxRow)` - `NewInlineKeyboard(payloadType, maxRow)`
Пример: Пример:
```go ```go
kb := laniakea.NewInlineKeyboardJson(2). kb := laniakea.NewInlineKeyboardJSON(2).
AddCallbackButton("Open", "open", 42). AddCallbackButton("Open", "open", 42).
AddCallbackButton("Delete", "delete", 42). AddCallbackButton("Delete", "delete", 42).
AddUrlButton("Docs", "https://example.com/docs") AddURLButton("Docs", "https://example.com/docs")
``` ```
`maxRow` определяет, сколько кнопок автоматически помещается в один ряд. `maxRow` определяет, сколько кнопок автоматически помещается в один ряд.
@@ -54,7 +54,7 @@ kb := laniakea.NewInlineKeyboardJson(2).
## JSON vs Base64 ## JSON vs Base64
`BotPayloadJson`: `BotPayloadJSON`:
- удобнее читать в логах и тестах; - удобнее читать в логах и тестах;
- проще отлаживать. - проще отлаживать.
@@ -85,7 +85,7 @@ bot.SetStrictPayloadType(true)
```go ```go
kb := ctx.NewInlineKeyboard(2). kb := ctx.NewInlineKeyboard(2).
SetPayloadType(laniakea.BotPayloadJson) SetPayloadType(laniakea.BotPayloadJSON)
``` ```
Это удобно для сценариев миграции или отладки. Это удобно для сценариев миграции или отладки.
+19 -19
@@ -16,7 +16,7 @@ Inline keyboards in Laniakea are built explicitly: you choose a row width, add U
## Building a keyboard ## Building a keyboard
The most direct constructors are: The most direct constructors are:
- `laniakea.NewInlineKeyboardJson(maxRow)` - `laniakea.NewInlineKeyboardJSON(maxRow)`
- `laniakea.NewInlineKeyboardBase64(maxRow)` - `laniakea.NewInlineKeyboardBase64(maxRow)`
- `laniakea.NewInlineKeyboard(payloadType, maxRow)` - `laniakea.NewInlineKeyboard(payloadType, maxRow)`
@@ -25,10 +25,10 @@ The most direct constructors are:
Example: Example:
```go ```go
kb := laniakea.NewInlineKeyboardJson(2). kb := laniakea.NewInlineKeyboardJSON(2).
AddCallbackButton("Open", "open", 42). AddCallbackButton("Open", "open", 42).
AddCallbackButton("Delete", "delete", 42). AddCallbackButton("Delete", "delete", 42).
AddUrlButton("Docs", "https://example.com/docs") AddURLButton("Docs", "https://example.com/docs")
``` ```
In that example: In that example:
@@ -77,7 +77,7 @@ All payload arguments are converted with `fmt.Sprint`, so handlers receive strin
Example: Example:
```go ```go
kb := laniakea.NewInlineKeyboardJson(1). kb := laniakea.NewInlineKeyboardJSON(1).
AddCallbackButton("Ban", "ban_user", 12345, "spam") AddCallbackButton("Ban", "ban_user", 12345, "spam")
``` ```
@@ -97,7 +97,7 @@ There is no separate `ctx.Payload` object in the current API. Payload handlers r
Laniakea supports two payload encodings: Laniakea supports two payload encodings:
- `BotPayloadJson` - `BotPayloadJSON`
- `BotPayloadBase64` - `BotPayloadBase64`
JSON is easier to inspect in logs and tests. JSON is easier to inspect in logs and tests.
@@ -132,7 +132,7 @@ For a single keyboard, you can override it locally:
```go ```go
kb := ctx.NewInlineKeyboard(2). kb := ctx.NewInlineKeyboard(2).
SetPayloadType(laniakea.BotPayloadJson). SetPayloadType(laniakea.BotPayloadJSON).
AddCallbackButton("Inspect", "inspect", 7) AddCallbackButton("Inspect", "inspect", 7)
``` ```
@@ -194,23 +194,23 @@ In both cases, keep payloads short and intentional. Telegram callback data is li
## Button builder for advanced cases ## Button builder for advanced cases
`InlineKbButtonBuilder` is the flexible path when you want button-specific styling or custom emoji icons. `InlineKeyboardButtonBuilder` is the flexible path when you want button-specific styling or custom emoji icons.
Example: Example:
```go ```go
button := laniakea.NewInlineKbButton("Confirm"). button := laniakea.NewInlineKeyboardButton("Confirm").
SetStyle(laniakea.ButtonStyleSuccess). SetStyle(laniakea.ButtonStyleSuccess).
SetCallbackDataJson("confirm_order", 99) SetCallbackDataJSON("confirm_order", 99)
kb := laniakea.NewInlineKeyboardJson(2).AddButton(button) kb := laniakea.NewInlineKeyboardJSON(2).AddButton(button)
``` ```
The builder supports: The builder supports:
- `SetStyle(...)` - `SetStyle(...)`
- `SetUrl(...)` - `SetURL(...)`
- `SetIconCustomEmojiId(...)` - `SetIconCustomEmojiID(...)`
- `SetCallbackDataJson(...)` - `SetCallbackDataJSON(...)`
- `SetCallbackDataBase64(...)` - `SetCallbackDataBase64(...)`
Use it when `AddCallbackButton(...)` is not expressive enough. Use it when `AddCallbackButton(...)` is not expressive enough.
@@ -223,9 +223,9 @@ Laniakea exposes three convenience style constants:
- `ButtonStyleDanger` - `ButtonStyleDanger`
You can use them with: You can use them with:
- `AddUrlButtonStyle(...)` - `AddURLButtonStyle(...)`
- `AddCallbackButtonStyle(...)` - `AddCallbackButtonStyle(...)`
- `InlineKbButtonBuilder.SetStyle(...)` - `InlineKeyboardButtonBuilder.SetStyle(...)`
URL buttons and callback buttons can live in the same keyboard. Use URL buttons for external navigation and callback buttons for bot-side actions. URL buttons and callback buttons can live in the same keyboard. Use URL buttons for external navigation and callback buttons for bot-side actions.
@@ -241,17 +241,17 @@ If your plain-text reply may exceed Telegrams message length limit, use:
## Routing payloads to handlers ## Routing payloads to handlers
Payloads are registered on plugins with: Payloads are registered on plugins with:
- `Plugin.NewPayload(...)` - `Plugin.Payload(...)`
- `Plugin.AddPayload(...)` - `Plugin.AddPayload(...)`
Example: Example:
```go ```go
plugin.NewPayload(func(ctx *laniakea.MsgContext, db *App) error { plugin.Payload("inspect", func(ctx *laniakea.MsgContext, db *App) error {
id := ctx.Args[0] id := ctx.Args[0]
ctx.AnswerCbQueryText("Handled " + id) ctx.AnswerCallbackText("Handled " + id)
return nil return nil
}, "inspect") })
``` ```
When a callback arrives: When a callback arrives:
+1 -1
@@ -200,7 +200,7 @@ admin.AddMiddleware(
### Command-specific validation ### Command-specific validation
```go ```go
ban := admin.NewCommand(banUser, "ban") ban := admin.Command("ban", banUser)
ban.Use(laniakea.NewMiddleware("require-reply", func(ctx *laniakea.MsgContext, db *App) bool { ban.Use(laniakea.NewMiddleware("require-reply", func(ctx *laniakea.MsgContext, db *App) bool {
if ctx.Msg == nil || ctx.Msg.ReplyToMessage == nil { if ctx.Msg == nil || ctx.Msg.ReplyToMessage == nil {
ctx.Answer("Reply to a user message first") ctx.Answer("Reply to a user message first")
+1 -1
@@ -34,7 +34,7 @@ The largest migration points in the current history are `rc.4`, `rc.7`, `rc.10`,
### What changed ### What changed
- `CommandExecutor[T]` changed from `func(ctx *MsgContext, db T)` to `func(ctx *MsgContext, db T) error`. - `CommandExecutor[T]` changed from `func(ctx *MsgContext, db T)` to `func(ctx *MsgContext, db T) error`.
- `Plugin.NewCommand(...)`, `Plugin.NewPayload(...)`, and `Plugin.AddUpdateHandler(...)` now expect error-returning handlers. - `Plugin.Command(...)`, `Plugin.Payload(...)`, and `Plugin.AddUpdateHandler(...)` now expect error-returning handlers.
- Long plain-text reply helpers were added: `AnswerLong(...)`, `AnswerLongf(...)`, `KeyboardLong(...)`, and `SplitMessageText(...)`. - Long plain-text reply helpers were added: `AnswerLong(...)`, `AnswerLongf(...)`, `KeyboardLong(...)`, and `SplitMessageText(...)`.
- Message and caption validation now happens before sending Telegram API requests. - Message and caption validation now happens before sending Telegram API requests.
- Optional strict callback payload decoding was added through `StrictPayloadType`. - Optional strict callback payload decoding was added through `StrictPayloadType`.
+4 -4
@@ -97,10 +97,10 @@ English version: [[MsgContext]]
В потоке callback особенно полезны: В потоке callback особенно полезны:
- `EditCallback(...)` - `EditCallback(...)`
- `AnswerCbQuery()` - `AnswerCallback()`
- `AnswerCbQueryText(...)` - `AnswerCallbackText(...)`
- `AnswerCbQueryAlert(...)` - `AnswerCallbackAlert(...)`
- `AnswerCbQueryUrl(...)` - `AnswerCallbackURL(...)`
- `CallbackDelete()` - `CallbackDelete()`
Они покрывают самые частые callback-сценарии без ручного хождения в `tgapi`. Они покрывают самые частые callback-сценарии без ручного хождения в `tgapi`.
+7 -7
@@ -179,21 +179,21 @@ func approve(ctx *laniakea.MsgContext, db laniakea.NoData) error {
} }
``` ```
### `AnswerCbQuery` ### `AnswerCallback`
Acknowledges the callback query itself. Acknowledges the callback query itself.
Use: Use:
- `AnswerCbQuery()` for empty acknowledgement - `AnswerCallback()` for empty acknowledgement
- `AnswerCbQueryText(...)` for a short notice - `AnswerCallbackText(...)` for a short notice
- `AnswerCbQueryAlert(...)` for a visible alert - `AnswerCallbackAlert(...)` for a visible alert
- `AnswerCbQueryUrl(...)` for redirect behavior - `AnswerCallbackURL(...)` for redirect behavior
Example: Example:
```go ```go
func approve(ctx *laniakea.MsgContext, db laniakea.NoData) error { func approve(ctx *laniakea.MsgContext, db laniakea.NoData) error {
ctx.AnswerCbQueryText("Saved") ctx.AnswerCallbackText("Saved")
ctx.EditCallback("Saved", nil) ctx.EditCallback("Saved", nil)
return nil return nil
} }
@@ -333,7 +333,7 @@ Do not pass raw user input to MarkdownV2 methods without escaping.
### Callback helpers only make sense in callback flow ### Callback helpers only make sense in callback flow
Methods like `EditCallback(...)` and `AnswerCbQueryText(...)` depend on callback-specific context. Methods like `EditCallback(...)` and `AnswerCallbackText(...)` depend on callback-specific context.
## A practical example ## A practical example
+1 -1
@@ -19,7 +19,7 @@ English version: [[Recipes]]
Комбинируй: Комбинируй:
- `NewInlineKeyboard(...)` - `NewInlineKeyboard(...)`
- обработчик данных callback; - обработчик данных callback;
- `AnswerCbQuery...`; - `AnswerCallback...`;
- `EditCallback(...)` - `EditCallback(...)`
### Long reply ### Long reply
+15 -15
@@ -18,10 +18,10 @@ admin.AddMiddleware(laniakea.NewMiddleware("admin-only", func(ctx *laniakea.MsgC
return ctx.From != nil && app.IsAdmin(ctx.From.ID) return ctx.From != nil && app.IsAdmin(ctx.From.ID)
})) }))
admin.NewCommand(func(ctx *laniakea.MsgContext, app *App) error { admin.Command("reload", func(ctx *laniakea.MsgContext, app *App) error {
ctx.Answer("Admin command executed") ctx.Answer("Admin command executed")
return nil return nil
}, "reload") })
``` ```
## Callback button flow ## Callback button flow
@@ -31,17 +31,17 @@ Use a payload handler for inline keyboard callbacks.
```go ```go
menu := laniakea.NewPlugin[laniakea.NoData]("menu") menu := laniakea.NewPlugin[laniakea.NoData]("menu")
menu.NewCommand(func(ctx *laniakea.MsgContext, db laniakea.NoData) error { menu.Command("menu", func(ctx *laniakea.MsgContext, db laniakea.NoData) error {
kb := ctx.NewInlineKeyboard(1) kb := ctx.NewInlineKeyboard(1)
kb.AddCallbackButton("Open settings", "settings") kb.AddCallbackButton("Open settings", "settings")
ctx.Keyboard("Choose an action", kb) ctx.Keyboard("Choose an action", kb)
return nil return nil
}, "menu") })
menu.NewPayload(func(ctx *laniakea.MsgContext, db laniakea.NoData) error { menu.Payload("settings", func(ctx *laniakea.MsgContext, db laniakea.NoData) error {
ctx.EditCallback("Settings screen", nil) ctx.EditCallback("Settings screen", nil)
return nil return nil
}, "settings") })
``` ```
## Long plain-text reply ## Long plain-text reply
@@ -49,11 +49,11 @@ menu.NewPayload(func(ctx *laniakea.MsgContext, db laniakea.NoData) error {
Use `AnswerLong(...)` when you want explicit splitting into multiple safe Telegram messages. Use `AnswerLong(...)` when you want explicit splitting into multiple safe Telegram messages.
```go ```go
plugin.NewCommand(func(ctx *laniakea.MsgContext, db *App) error { plugin.Command("report", func(ctx *laniakea.MsgContext, db *App) error {
report := buildLargePlainTextReport() report := buildLargePlainTextReport()
ctx.AnswerLong(report) ctx.AnswerLong(report)
return nil return nil
}, "report") })
``` ```
If you need an inline keyboard on the final chunk, use `KeyboardLong(...)`. If you need an inline keyboard on the final chunk, use `KeyboardLong(...)`.
@@ -71,10 +71,10 @@ l10n := laniakea.NewL10n("en").
bot.SetL10n(l10n) bot.SetL10n(l10n)
plugin.NewCommand(func(ctx *laniakea.MsgContext, db *App) error { plugin.Command("start", func(ctx *laniakea.MsgContext, db *App) error {
ctx.Answer(ctx.Translate("greeting")) ctx.Answer(ctx.Translate("greeting"))
return nil return nil
}, "start") })
``` ```
## Non-command update handler ## Non-command update handler
@@ -97,7 +97,7 @@ This is usually cleaner than forcing non-command traffic through a command parse
Use drafts when you want to build a reply progressively and publish it once at the end. Use drafts when you want to build a reply progressively and publish it once at the end.
```go ```go
plugin.NewCommand(func(ctx *laniakea.MsgContext, db *App) error { plugin.Command("build", func(ctx *laniakea.MsgContext, db *App) error {
draft := ctx.NewDraft() draft := ctx.NewDraft()
if draft == nil { if draft == nil {
return nil return nil
@@ -111,7 +111,7 @@ plugin.NewCommand(func(ctx *laniakea.MsgContext, db *App) error {
} }
return draft.Flush() return draft.Flush()
}, "build") })
``` ```
## File upload with `tgapi` ## File upload with `tgapi`
@@ -119,15 +119,15 @@ plugin.NewCommand(func(ctx *laniakea.MsgContext, db *App) error {
Use the higher-level handler flow for routing, but drop down to `tgapi` uploader methods when you need multipart upload behavior. Use the higher-level handler flow for routing, but drop down to `tgapi` uploader methods when you need multipart upload behavior.
```go ```go
plugin.NewCommand(func(ctx *laniakea.MsgContext, db *App) error { plugin.Command("upload", func(ctx *laniakea.MsgContext, db *App) error {
uploader := tgapi.NewUploader(ctx.Api) uploader := tgapi.NewUploader(ctx.API)
defer uploader.Close() defer uploader.Close()
_, err := uploader.SendPhoto(tgapi.UploadPhoto{ _, err := uploader.SendPhoto(tgapi.UploadPhoto{
ChatID: ctx.Msg.Chat.ID, ChatID: ctx.Msg.Chat.ID,
}, tgapi.NewUploaderFile("report.jpg", []byte("hello"))) }, tgapi.NewUploaderFile("report.jpg", []byte("hello")))
return err return err
}, "upload") })
``` ```
If your exact uploader call differs, keep the general rule in mind: handler routing can stay high-level even when the send path needs `tgapi`. If your exact uploader call differs, keep the general rule in mind: handler routing can stay high-level even when the send path needs `tgapi`.
+4 -4
@@ -23,9 +23,9 @@ runner := laniakea.NewRunner("cleanup", fn)
``` ```
Потом конфигурируются методы builder: Потом конфигурируются методы builder:
- `Onetime(bool)` - `Once(bool)`
- `Async(bool)` - `Async(bool)`
- `Timeout(duration)` - `Every(duration)`
## Основные режимы ## Основные режимы
@@ -51,11 +51,11 @@ runner := laniakea.NewRunner("cleanup", fn)
Повторяющийся synchronous runner считается невалидным и пропускается с предупреждением. Повторяющийся synchronous runner считается невалидным и пропускается с предупреждением.
Также повторяющийся async runner без `Timeout(...)` пропускается. Также повторяющийся async runner без `Every(...)` пропускается.
## Когда стартуют фоновые задачи ## Когда стартуют фоновые задачи
Фоновые задачи стартуют из `RunWithContext(...)` или `RunWebHookWithContext(...)`, а не из `NewBot(...)`. Фоновые задачи стартуют из `RunWithContext(...)` или `RunWebhookWithContext(...)`, а не из `NewBot(...)`.
Это часть фазы выполнения, а не фазы сборки конфигурации. Это часть фазы выполнения, а не фазы сборки конфигурации.
+13 -13
@@ -14,9 +14,9 @@ Each runner is built from:
- execution flags configured through builder methods. - execution flags configured through builder methods.
Main builder methods: Main builder methods:
- `Onetime(bool)` - `Once(bool)`
- `Async(bool)` - `Async(bool)`
- `Timeout(duration)` - `Every(duration)`
## Creating a runner ## Creating a runner
@@ -43,7 +43,7 @@ There are three meaningful configurations.
```go ```go
runner := laniakea.NewRunner("warmup", fn). runner := laniakea.NewRunner("warmup", fn).
Onetime(true). Once(true).
Async(false) Async(false)
``` ```
@@ -58,7 +58,7 @@ Use this for startup work that must complete before the bot is considered ready.
```go ```go
runner := laniakea.NewRunner("prefetch", fn). runner := laniakea.NewRunner("prefetch", fn).
Onetime(true) Once(true)
``` ```
Behavior: Behavior:
@@ -72,7 +72,7 @@ Use this for fire-and-forget startup work that is useful but not required before
```go ```go
runner := laniakea.NewRunner("cleanup", fn). runner := laniakea.NewRunner("cleanup", fn).
Timeout(time.Minute) Every(time.Minute)
``` ```
Behavior: Behavior:
@@ -86,13 +86,13 @@ Use this for recurring background jobs.
One configuration is intentionally treated as invalid: One configuration is intentionally treated as invalid:
- `Onetime(false).Async(false)` - `Once(false).Async(false)`
That means: That means:
- synchronous repeating runners are skipped; - synchronous repeating runners are skipped;
- the bot logs a warning instead of trying to run them inline forever. - the bot logs a warning instead of trying to run them inline forever.
Also, repeating async runners with `Timeout(0)` are skipped with a warning. Also, repeating async runners with `Every(0)` are skipped with a warning.
## Registration ## Registration
@@ -106,7 +106,7 @@ Runners with an empty name are skipped with a warning, so always give them a sta
## Lifecycle ## Lifecycle
Runners are not started by `NewBot(...)`. They start from `RunWithContext(...)` or `RunWebHookWithContext(...)`, right before the bot begins polling or webhook ingestion. Runners are not started by `NewBot(...)`. They start from `RunWithContext(...)` or `RunWebhookWithContext(...)`, right before the bot begins polling or webhook ingestion.
That means runner execution belongs to the bot's runtime lifecycle, not to its configuration phase. That means runner execution belongs to the bot's runtime lifecycle, not to its configuration phase.
@@ -134,7 +134,7 @@ If a runner must be fatal for startup, make it one-time synchronous and return a
## Shutdown behavior ## Shutdown behavior
`RunWithContext(...)` and `RunWebHookWithContext(...)` wait for runner completion in two groups: `RunWithContext(...)` and `RunWebhookWithContext(...)` wait for runner completion in two groups:
- one-time async runners; - one-time async runners;
- background repeating runners. - background repeating runners.
@@ -149,7 +149,7 @@ This means graceful shutdown includes runner shutdown, but only if your runner f
```go ```go
cleanup := laniakea.NewRunner("cleanup", func(bot *laniakea.Bot[*App]) error { cleanup := laniakea.NewRunner("cleanup", func(bot *laniakea.Bot[*App]) error {
return bot.GetAppData().CleanupExpired() return bot.GetAppData().CleanupExpired()
}).Timeout(5 * time.Minute) }).Every(5 * time.Minute)
``` ```
### Startup warmup ### Startup warmup
@@ -157,7 +157,7 @@ cleanup := laniakea.NewRunner("cleanup", func(bot *laniakea.Bot[*App]) error {
```go ```go
warmup := laniakea.NewRunner("warmup", func(bot *laniakea.Bot[*App]) error { warmup := laniakea.NewRunner("warmup", func(bot *laniakea.Bot[*App]) error {
return bot.GetAppData().WarmCaches() return bot.GetAppData().WarmCaches()
}).Onetime(true).Async(false) }).Once(true).Async(false)
``` ```
### Background metrics push ### Background metrics push
@@ -165,14 +165,14 @@ warmup := laniakea.NewRunner("warmup", func(bot *laniakea.Bot[*App]) error {
```go ```go
metrics := laniakea.NewRunner("metrics", func(bot *laniakea.Bot[*App]) error { metrics := laniakea.NewRunner("metrics", func(bot *laniakea.Bot[*App]) error {
return pushMetrics(bot.GetAppData()) return pushMetrics(bot.GetAppData())
}).Timeout(30 * time.Second) }).Every(30 * time.Second)
``` ```
## Recommendations ## Recommendations
- Use one-time sync runners only for short startup-critical work. - Use one-time sync runners only for short startup-critical work.
- Use repeating async runners for periodic jobs. - Use repeating async runners for periodic jobs.
- Always set `Timeout(...)` on repeating runners. - Always set `Every(...)` on repeating runners.
- Keep runner bodies small and delegate complex work to regular application services. - Keep runner bodies small and delegate complex work to regular application services.
- Treat runner names as operational identifiers that should make sense in logs. - Treat runner names as operational identifiers that should make sense in logs.
+2 -2
@@ -4,7 +4,7 @@ Scenes — это слой маршрутизации Laniakea с сохране
## Что дают сцены ## Что дают сцены
- Регистрацию через `Plugin.NewScene(...)` и `Plugin.AddScene(...)`. - Регистрацию через `Plugin.Scene(...)` и `Plugin.AddScene(...)`.
- Явный вход и выход через `MsgContext.EnterScene(...)`, `EnterSceneStep(...)` и `ExitScene()`. - Явный вход и выход через `MsgContext.EnterScene(...)`, `EnterSceneStep(...)` и `ExitScene()`.
- Области действия сессии на пользователя, чат или пару пользователь-чат. - Области действия сессии на пользователя, чат или пару пользователь-чат.
- Обработчики шагов, локальные команды сцены и резервный обработчик сообщений на уровне сцены. - Обработчики шагов, локальные команды сцены и резервный обработчик сообщений на уровне сцены.
@@ -54,7 +54,7 @@ type SessionStore interface {
Сцены регистрируются внутри плагина в том же стиле, что и команды с данными callback. Сцены регистрируются внутри плагина в том же стиле, что и команды с данными callback.
```go ```go
plugin.NewScene("signup"). plugin.Scene("signup").
SetScope(laniakea.SceneScopeUserChat). SetScope(laniakea.SceneScopeUserChat).
SetEntry("ask_name"). SetEntry("ask_name").
OnStep("ask_name", askName). OnStep("ask_name", askName).
+2 -2
@@ -4,7 +4,7 @@ Scenes are Laniakea's stateful routing layer for multi-step and modal bot flows.
## What scenes give you ## What scenes give you
- Scene registration through `Plugin.NewScene(...)` and `Plugin.AddScene(...)`. - Scene registration through `Plugin.Scene(...)` and `Plugin.AddScene(...)`.
- Explicit entry and exit through `MsgContext.EnterScene(...)`, `EnterSceneStep(...)`, and `ExitScene()`. - Explicit entry and exit through `MsgContext.EnterScene(...)`, `EnterSceneStep(...)`, and `ExitScene()`.
- Per-user, per-chat, or per-user-chat session scopes. - Per-user, per-chat, or per-user-chat session scopes.
- Step handlers, scene-local commands, and a scene-level message fallback. - Step handlers, scene-local commands, and a scene-level message fallback.
@@ -54,7 +54,7 @@ Recommended default:
Scenes are registered inside plugins in the same style as commands and payloads. Scenes are registered inside plugins in the same style as commands and payloads.
```go ```go
plugin.NewScene("signup"). plugin.Scene("signup").
SetScope(laniakea.SceneScopeUserChat). SetScope(laniakea.SceneScopeUserChat).
SetEntry("ask_name"). SetEntry("ask_name").
OnStep("ask_name", askName). OnStep("ask_name", askName).
+1 -1
@@ -53,7 +53,7 @@ In broad terms:
- patch version: backward-compatible fixes and clarifications; - patch version: backward-compatible fixes and clarifications;
- `-rc.N`: prerelease iteration before the stable release line. - `-rc.N`: prerelease iteration before the stable release line.
The current code declares its version in [utils/version.go](/home/scuro/projects/Laniakea/utils/version.go): The current code declares its version in `utils/version.go`:
- `VersionString` - `VersionString`
- `VersionMajor` - `VersionMajor`
- `VersionMinor` - `VersionMinor`
+44 -44
@@ -10,12 +10,12 @@ English version: [[Update-Routing-Model]]
Сейчас Laniakea маршрутизирует обновления так: Сейчас Laniakea маршрутизирует обновления так:
| Update type | Routing path | | Update type | Routing path |
|---|---| |------------------|-------------------------|
| `message` | command flow | | `message` | command flow |
| `channel_post` | command flow | | `channel_post` | command flow |
| `callback_query` | payload flow | | `callback_query` | payload flow |
| всё остальное | `AddUpdateHandler(...)` | | всё остальное | `AddUpdateHandler(...)` |
Важно: Важно:
- `message`, `channel_post` и `callback_query` зарезервированы и не должны регистрироваться через `AddUpdateHandler(...)`. - `message`, `channel_post` и `callback_query` зарезервированы и не должны регистрироваться через `AddUpdateHandler(...)`.
@@ -65,7 +65,7 @@ Payload flow применяется только к:
Базовые гарантии: Базовые гарантии:
- `ctx.Update` всегда заполнен - `ctx.Update` всегда заполнен
- `ctx.CallbackQueryId` заполнен - `ctx.CallbackQueryID` заполнен
- `ctx.From` и `ctx.FromID` заполняются, если Telegram прислал user - `ctx.From` и `ctx.FromID` заполняются, если Telegram прислал user
- `ctx.Args` заполняется из декодированных данных callback - `ctx.Args` заполняется из декодированных данных callback
- `ctx.Logger` переключается на logger совпавшего плагина, если он задан - `ctx.Logger` переключается на logger совпавшего плагина, если он задан
@@ -80,15 +80,15 @@ Payload flow применяется только к:
Гарантии: Гарантии:
- `ctx.Msg` заполнен - `ctx.Msg` заполнен
- `ctx.CallbackMsgId` заполнен - `ctx.CallbackMsgID` заполнен
- `ctx.InlineMsgId == ""` - `ctx.InlineMsgID == ""`
### Callback query, привязанный к inline message ### Callback query, привязанный к inline message
Гарантии: Гарантии:
- `ctx.Msg == nil` - `ctx.Msg == nil`
- `ctx.CallbackMsgId == 0` - `ctx.CallbackMsgID == 0`
- `ctx.InlineMsgId` заполнен - `ctx.InlineMsgID` заполнен
## Контракт generic update handlers ## Контракт generic update handlers
@@ -110,14 +110,14 @@ Generic update handlers регистрируются через `Plugin.AddUpdat
### Update types, которые несут message ### Update types, которые несут message
| Update type | `ctx.Msg` | `ctx.From` / `ctx.FromID` | | Update type | `ctx.Msg` | `ctx.From` / `ctx.FromID` |
|---|---|---| |---------------------------|-----------|-----------------------------|
| `message` | да | да, если есть `Msg.From` | | `message` | да | да, если есть `Msg.From` |
| `edited_message` | да | да, если есть `Msg.From` | | `edited_message` | да | да, если есть `Msg.From` |
| `channel_post` | да | только если есть `Msg.From` | | `channel_post` | да | только если есть `Msg.From` |
| `edited_channel_post` | да | только если есть `Msg.From` | | `edited_channel_post` | да | только если есть `Msg.From` |
| `business_message` | да | да, если есть `Msg.From` | | `business_message` | да | да, если есть `Msg.From` |
| `edited_business_message` | да | да, если есть `Msg.From` | | `edited_business_message` | да | да, если есть `Msg.From` |
Важно: Важно:
- message-backed не означает command-routed - message-backed не означает command-routed
@@ -125,37 +125,37 @@ Generic update handlers регистрируются через `Plugin.AddUpdat
### Update types с user, но без message ### Update types с user, но без message
| Update type | `ctx.Msg` | `ctx.From` / `ctx.FromID` | | Update type | `ctx.Msg` | `ctx.From` / `ctx.FromID` |
|---|---|---| |------------------------|-----------|----------------------------------|
| `inline_query` | нет | да | | `inline_query` | нет | да |
| `chosen_inline_result` | нет | да | | `chosen_inline_result` | нет | да |
| `shipping_query` | нет | да | | `shipping_query` | нет | да |
| `pre_checkout_query` | нет | да | | `pre_checkout_query` | нет | да |
| `purchased_paid_media` | нет | да | | `purchased_paid_media` | нет | да |
| `my_chat_member` | нет | да | | `my_chat_member` | нет | да |
| `chat_member` | нет | да | | `chat_member` | нет | да |
| `chat_join_request` | нет | да | | `chat_join_request` | нет | да |
| `business_connection` | нет | да | | `business_connection` | нет | да |
| `poll_answer` | нет | да | | `poll_answer` | нет | да |
| `message_reaction` | нет | да, если Telegram прислал `User` | | `message_reaction` | нет | да, если Telegram прислал `User` |
| `chat_boost` | нет | да | | `chat_boost` | нет | да |
| `removed_chat_boost` | нет | да | | `removed_chat_boost` | нет | да |
### Callback-specific update type ### Callback-specific update type
| Update type | `ctx.Msg` | `ctx.From` / `ctx.FromID` | Спецполя | | Update type | `ctx.Msg` | `ctx.From` / `ctx.FromID` | Спецполя |
|---|---|---|---| |--------------------------------------|-----------|---------------------------|------------------------------------|
| `callback_query` с `Message` | да | да | `CallbackQueryId`, `CallbackMsgId` | | `callback_query` с `Message` | да | да | `CallbackQueryID`, `CallbackMsgID` |
| `callback_query` с `InlineMessageID` | нет | да | `CallbackQueryId`, `InlineMsgId` | | `callback_query` с `InlineMessageID` | нет | да | `CallbackQueryID`, `InlineMsgID` |
### Update types без нормализованных гарантий по user/message ### Update types без нормализованных гарантий по user/message
| Update type | `ctx.Msg` | `ctx.From` / `ctx.FromID` | | Update type | `ctx.Msg` | `ctx.From` / `ctx.FromID` |
|---|---|---| |-----------------------------|-----------|---------------------------|
| `poll` | нет | нет | | `poll` | нет | нет |
| `message_reaction_count` | нет | нет | | `message_reaction_count` | нет | нет |
| `deleted_business_messages` | нет | нет | | `deleted_business_messages` | нет | нет |
| `unknown` | нет | нет | | `unknown` | нет | нет |
## Что уже достаточно стабильно ## Что уже достаточно стабильно
+44 -44
@@ -10,12 +10,12 @@ This is a model of current framework behavior, not a second API surface. The goa
Laniakea currently routes updates like this: Laniakea currently routes updates like this:
| Update type | Routing path | | Update type | Routing path |
|---|---| |------------------|-------------------------|
| `message` | command flow | | `message` | command flow |
| `channel_post` | command flow | | `channel_post` | command flow |
| `callback_query` | payload flow | | `callback_query` | payload flow |
| everything else | `AddUpdateHandler(...)` | | everything else | `AddUpdateHandler(...)` |
Important: Important:
- `message`, `channel_post`, and `callback_query` are reserved from `AddUpdateHandler(...)`. - `message`, `channel_post`, and `callback_query` are reserved from `AddUpdateHandler(...)`.
@@ -65,7 +65,7 @@ Payload flow applies only to:
Base guarantees: Base guarantees:
- `ctx.Update` is always present - `ctx.Update` is always present
- `ctx.CallbackQueryId` is populated - `ctx.CallbackQueryID` is populated
- `ctx.From` and `ctx.FromID` are populated when Telegram includes a user - `ctx.From` and `ctx.FromID` are populated when Telegram includes a user
- `ctx.Args` is populated from decoded payload args - `ctx.Args` is populated from decoded payload args
- `ctx.Logger` switches to the matched plugin logger when one exists - `ctx.Logger` switches to the matched plugin logger when one exists
@@ -80,15 +80,15 @@ There are two payload target shapes.
Guarantees: Guarantees:
- `ctx.Msg` is present - `ctx.Msg` is present
- `ctx.CallbackMsgId` is populated - `ctx.CallbackMsgID` is populated
- `ctx.InlineMsgId == ""` - `ctx.InlineMsgID == ""`
### Callback query targeting an inline message ### Callback query targeting an inline message
Guarantees: Guarantees:
- `ctx.Msg == nil` - `ctx.Msg == nil`
- `ctx.CallbackMsgId == 0` - `ctx.CallbackMsgID == 0`
- `ctx.InlineMsgId` is populated - `ctx.InlineMsgID` is populated
## Generic update handler contract ## Generic update handler contract
@@ -110,14 +110,14 @@ Important limitation:
### Message-backed update kinds ### Message-backed update kinds
| Update type | `ctx.Msg` | `ctx.From` / `ctx.FromID` | | Update type | `ctx.Msg` | `ctx.From` / `ctx.FromID` |
|---|---|---| |---------------------------|-----------|-----------------------------|
| `message` | yes | yes, when `Msg.From` exists | | `message` | yes | yes, when `Msg.From` exists |
| `edited_message` | yes | yes, when `Msg.From` exists | | `edited_message` | yes | yes, when `Msg.From` exists |
| `channel_post` | yes | only when `Msg.From` exists | | `channel_post` | yes | only when `Msg.From` exists |
| `edited_channel_post` | yes | only when `Msg.From` exists | | `edited_channel_post` | yes | only when `Msg.From` exists |
| `business_message` | yes | yes, when `Msg.From` exists | | `business_message` | yes | yes, when `Msg.From` exists |
| `edited_business_message` | yes | yes, when `Msg.From` exists | | `edited_business_message` | yes | yes, when `Msg.From` exists |
Important: Important:
- message-backed does **not** mean command-routed - message-backed does **not** mean command-routed
@@ -125,37 +125,37 @@ Important:
### User-backed but not message-backed update kinds ### User-backed but not message-backed update kinds
| Update type | `ctx.Msg` | `ctx.From` / `ctx.FromID` | | Update type | `ctx.Msg` | `ctx.From` / `ctx.FromID` |
|---|---|---| |------------------------|-----------|------------------------------------|
| `inline_query` | no | yes | | `inline_query` | no | yes |
| `chosen_inline_result` | no | yes | | `chosen_inline_result` | no | yes |
| `shipping_query` | no | yes | | `shipping_query` | no | yes |
| `pre_checkout_query` | no | yes | | `pre_checkout_query` | no | yes |
| `purchased_paid_media` | no | yes | | `purchased_paid_media` | no | yes |
| `my_chat_member` | no | yes | | `my_chat_member` | no | yes |
| `chat_member` | no | yes | | `chat_member` | no | yes |
| `chat_join_request` | no | yes | | `chat_join_request` | no | yes |
| `business_connection` | no | yes | | `business_connection` | no | yes |
| `poll_answer` | no | yes | | `poll_answer` | no | yes |
| `message_reaction` | no | yes, when Telegram includes `User` | | `message_reaction` | no | yes, when Telegram includes `User` |
| `chat_boost` | no | yes | | `chat_boost` | no | yes |
| `removed_chat_boost` | no | yes | | `removed_chat_boost` | no | yes |
### Callback-specific update kind ### Callback-specific update kind
| Update type | `ctx.Msg` | `ctx.From` / `ctx.FromID` | Special fields | | Update type | `ctx.Msg` | `ctx.From` / `ctx.FromID` | Special fields |
|---|---|---|---| |-----------------------------------------|-----------|---------------------------|------------------------------------|
| `callback_query` with `Message` | yes | yes | `CallbackQueryId`, `CallbackMsgId` | | `callback_query` with `Message` | yes | yes | `CallbackQueryID`, `CallbackMsgID` |
| `callback_query` with `InlineMessageID` | no | yes | `CallbackQueryId`, `InlineMsgId` | | `callback_query` with `InlineMessageID` | no | yes | `CallbackQueryID`, `InlineMsgID` |
### Update kinds without normalized user/message guarantees ### Update kinds without normalized user/message guarantees
| Update type | `ctx.Msg` | `ctx.From` / `ctx.FromID` | | Update type | `ctx.Msg` | `ctx.From` / `ctx.FromID` |
|---|---|---| |-----------------------------|-----------|---------------------------|
| `poll` | no | no | | `poll` | no | no |
| `message_reaction_count` | no | no | | `message_reaction_count` | no | no |
| `deleted_business_messages` | no | no | | `deleted_business_messages` | no | no |
| `unknown` | no | no | | `unknown` | no | no |
## What is already stable enough to rely on ## What is already stable enough to rely on
+15 -15
@@ -2,11 +2,11 @@
English version: [[Webhook-Runtime]] English version: [[Webhook-Runtime]]
Эта страница объясняет bot-level webhook runtime в Laniakea: как работает `RunWebHookWithContext(...)`, чем он отличается от низкоуровневых webhook-вызовов в `tgapi` и какие runtime-гарантии он делит с polling-режимом. Эта страница объясняет bot-level webhook runtime в Laniakea: как работает `RunWebhookWithContext(...)`, чем он отличается от низкоуровневых webhook-вызовов в `tgapi` и какие runtime-гарантии он делит с polling-режимом.
Важно про naming: Важно про naming:
- текущий публичный API использует историческое написание `WebHook` в идентификаторах вроде `RunWebHookWithContext(...)`, `RunWebHook(...)` и `BotWebHookOpts`; - публичный API использует идиоматичное написание `Webhook` в идентификаторах вроде `RunWebhookWithContext(...)`, `RunWebhook(...)` и `BotWebhookOpts`;
- в тексте страницы используется обычное слово "webhook", но в примерах остаются реальные имена Go API. - в примерах остаются реальные имена Go API.
## Когда использовать webhook runtime ## Когда использовать webhook runtime
@@ -25,11 +25,11 @@ English version: [[Webhook-Runtime]]
## Точки входа ## Точки входа
Главные bot-level точки входа такие: Главные bot-level точки входа такие:
- `RunWebHookWithContext(ctx, opts, tlsFiles...)` - `RunWebhookWithContext(ctx, opts, tlsFiles...)`
- `RunWebHook(opts, tlsFiles...)` - `RunWebhook(opts, tlsFiles...)`
- `NewBotWebHookOpts()` - `NewBotWebhookOpts()`
`RunWebHook(...)` — это просто короткая форма для `RunWebHookWithContext(context.Background(), ...)`. `RunWebhook(...)` — это просто короткая форма для `RunWebhookWithContext(context.Background(), ...)`.
Обычный шаблон выглядит так: Обычный шаблон выглядит так:
@@ -46,20 +46,20 @@ defer bot.Close()
bot.SetAppData(app) bot.SetAppData(app)
bot.AddPlugins(plugin) bot.AddPlugins(plugin)
webhookOpts := laniakea.NewBotWebHookOpts(). webhookOpts := laniakea.NewBotWebhookOpts().
SetURL("https://bot.example.com"). SetURL("https://bot.example.com").
SetPath("/telegram"). SetPath("/telegram").
SetLocalPort(8080). SetLocalPort(8080).
SetSecretToken("shared-secret") SetSecretToken("shared-secret")
if err := bot.RunWebHookWithContext(ctx, webhookOpts); err != nil { if err := bot.RunWebhookWithContext(ctx, webhookOpts); err != nil {
return err return err
} }
``` ```
## Что именно берет на себя bot-level runtime ## Что именно берет на себя bot-level runtime
`RunWebHookWithContext(...)` — это не просто обертка над Telegram `setWebhook`. `RunWebhookWithContext(...)` — это не просто обертка над Telegram `setWebhook`.
Он: Он:
- валидирует bot-level условия старта, например prefixes и наличие зарегистрированных plugins; - валидирует bot-level условия старта, например prefixes и наличие зарегистрированных plugins;
@@ -86,7 +86,7 @@ Webhook runtime использует те же основные гарантии
## Основные webhook options ## Основные webhook options
`BotWebHookOpts` управляет и регистрацией webhook у Telegram, и локальным HTTP server. `BotWebhookOpts` управляет и регистрацией webhook у Telegram, и локальным HTTP server.
Поля, которые важны в первую очередь: Поля, которые важны в первую очередь:
@@ -167,12 +167,12 @@ Webhook runtime использует те же основные гарантии
## Поведение HTTP и TLS ## Поведение HTTP и TLS
По умолчанию `RunWebHookWithContext(...)` поднимает обычный HTTP server на `LocalPort`. По умолчанию `RunWebhookWithContext(...)` поднимает обычный HTTP server на `LocalPort`.
Если передать два TLS-файла, локально стартует HTTPS. Если передать два TLS-файла, локально стартует HTTPS.
Важно: Важно:
- текущий публичный API ожидает существующий порядок аргументов `key, cert` при вызове `RunWebHookWithContext(...)`; - текущий публичный API ожидает существующий порядок аргументов `key, cert` при вызове `RunWebhookWithContext(...)`;
- это отличается от более привычной ментальной модели `cert, key`, которую многие Go-разработчики ожидают от `ListenAndServeTLS`. - это отличается от более привычной ментальной модели `cert, key`, которую многие Go-разработчики ожидают от `ListenAndServeTLS`.
Поэтому в реальном setup лучше писать этот вызов максимально явно. Поэтому в реальном setup лучше писать этот вызов максимально явно.
@@ -202,7 +202,7 @@ Webhook runtime использует те же основные гарантии
- `URL` — это то, что видит Telegram; - `URL` — это то, что видит Telegram;
- `Path` и `LocalPort` — это то, что реально обслуживает твой бот; - `Path` и `LocalPort` — это то, что реально обслуживает твой бот;
- в production эти значения часто относятся к разным слоям инфраструктуры. - в production эти значения часто относятся к разным слоям инфраструктуры.
- если ты переводишь работающий deployment с webhook-режима на polling, сначала удали webhook через `CloseWebHook()` или `tgapi.DeleteWebhook(...)`; Telegram не прекращает webhook-доставку автоматически. - если ты переводишь работающий deployment с webhook-режима на polling, сначала удали webhook через `CloseWebhook()` или `tgapi.DeleteWebhook(...)`; Telegram не прекращает webhook-доставку автоматически.
## Связь с webhook methods в `tgapi` ## Связь с webhook methods в `tgapi`
@@ -221,7 +221,7 @@ Webhook runtime использует те же основные гарантии
когда тебе нужна собственная инфраструктура вокруг webhook path и ты не хочешь, чтобы сам бот владел HTTP server. когда тебе нужна собственная инфраструктура вокруг webhook path и ты не хочешь, чтобы сам бот владел HTTP server.
То есть: То есть:
- `RunWebHookWithContext(...)` — это framework runtime API; - `RunWebhookWithContext(...)` — это framework runtime API;
- webhook methods из `tgapi` — это низкоуровневые transport primitives. - webhook methods из `tgapi` — это низкоуровневые transport primitives.
## Частые ошибки ## Частые ошибки
+15 -15
@@ -2,11 +2,11 @@
Russian version: [[Webhook-Runtime-RU]] Russian version: [[Webhook-Runtime-RU]]
This page explains the bot-level webhook runtime in Laniakea: how `RunWebHookWithContext(...)` works, what it owns, how it differs from low-level `tgapi` webhook calls, and what runtime guarantees it shares with polling mode. This page explains the bot-level webhook runtime in Laniakea: how `RunWebhookWithContext(...)` works, what it owns, how it differs from low-level `tgapi` webhook calls, and what runtime guarantees it shares with polling mode.
Important naming note: Important naming note:
- the current public API uses the historical `WebHook` spelling in identifiers such as `RunWebHookWithContext(...)`, `RunWebHook(...)`, and `BotWebHookOpts`; - the public API uses idiomatic `Webhook` spelling in identifiers such as `RunWebhookWithContext(...)`, `RunWebhook(...)`, and `BotWebhookOpts`;
- this page uses the more common English term "webhook" for readability, but examples keep the actual Go API names. - examples keep the actual Go API names.
## When to use webhook runtime ## When to use webhook runtime
@@ -25,11 +25,11 @@ Use polling when:
## Entry points ## Entry points
The main bot-level entry points are: The main bot-level entry points are:
- `RunWebHookWithContext(ctx, opts, tlsFiles...)` - `RunWebhookWithContext(ctx, opts, tlsFiles...)`
- `RunWebHook(opts, tlsFiles...)` - `RunWebhook(opts, tlsFiles...)`
- `NewBotWebHookOpts()` - `NewBotWebhookOpts()`
`RunWebHook(...)` is only a shorthand for `RunWebHookWithContext(context.Background(), ...)`. `RunWebhook(...)` is only a shorthand for `RunWebhookWithContext(context.Background(), ...)`.
The usual pattern looks like: The usual pattern looks like:
@@ -46,20 +46,20 @@ defer bot.Close()
bot.SetAppData(app) bot.SetAppData(app)
bot.AddPlugins(plugin) bot.AddPlugins(plugin)
webhookOpts := laniakea.NewBotWebHookOpts(). webhookOpts := laniakea.NewBotWebhookOpts().
SetURL("https://bot.example.com"). SetURL("https://bot.example.com").
SetPath("/telegram"). SetPath("/telegram").
SetLocalPort(8080). SetLocalPort(8080).
SetSecretToken("shared-secret") SetSecretToken("shared-secret")
if err := bot.RunWebHookWithContext(ctx, webhookOpts); err != nil { if err := bot.RunWebhookWithContext(ctx, webhookOpts); err != nil {
return err return err
} }
``` ```
## What the bot-level runtime owns ## What the bot-level runtime owns
`RunWebHookWithContext(...)` is more than a wrapper around Telegram's `setWebhook`. `RunWebhookWithContext(...)` is more than a wrapper around Telegram's `setWebhook`.
It: It:
- validates bot startup preconditions such as prefixes and registered plugins; - validates bot startup preconditions such as prefixes and registered plugins;
@@ -86,7 +86,7 @@ If you already understand [[Bot-Lifecycle]], the webhook mode should feel like a
## Main webhook options ## Main webhook options
`BotWebHookOpts` controls both Telegram webhook registration and the local server behavior. `BotWebhookOpts` controls both Telegram webhook registration and the local server behavior.
Fields you will care about first: Fields you will care about first:
@@ -167,12 +167,12 @@ Use this only when you specifically need Telegram's `ip_address` webhook option.
## HTTP and TLS behavior ## HTTP and TLS behavior
By default, `RunWebHookWithContext(...)` starts a plain HTTP server on `LocalPort`. By default, `RunWebhookWithContext(...)` starts a plain HTTP server on `LocalPort`.
If you pass two TLS files, it starts HTTPS locally instead. If you pass two TLS files, it starts HTTPS locally instead.
Important: Important:
- the current public API expects the existing key-then-cert argument order when calling `RunWebHookWithContext(...)`; - the current public API expects the existing key-then-cert argument order when calling `RunWebhookWithContext(...)`;
- that differs from the more common `cert, key` mental model many Go developers expect from `ListenAndServeTLS`. - that differs from the more common `cert, key` mental model many Go developers expect from `ListenAndServeTLS`.
Be explicit in your own setup code so this does not become a deployment footgun. Be explicit in your own setup code so this does not become a deployment footgun.
@@ -202,7 +202,7 @@ Also keep in mind:
- `URL` is what Telegram sees; - `URL` is what Telegram sees;
- `Path` and `LocalPort` are what your bot actually serves; - `Path` and `LocalPort` are what your bot actually serves;
- these are often not the same thing in production. - these are often not the same thing in production.
- if you switch a running deployment from webhook mode to polling, delete the webhook first with `CloseWebHook()` or `tgapi.DeleteWebhook(...)`; Telegram does not stop webhook delivery automatically. - if you switch a running deployment from webhook mode to polling, delete the webhook first with `CloseWebhook()` or `tgapi.DeleteWebhook(...)`; Telegram does not stop webhook delivery automatically.
## Relation to `tgapi` webhook methods ## Relation to `tgapi` webhook methods
@@ -221,7 +221,7 @@ Use lower-level `tgapi` calls such as:
when you need custom infrastructure around the webhook path and do not want the bot to own the HTTP server itself. when you need custom infrastructure around the webhook path and do not want the bot to own the HTTP server itself.
In other words: In other words:
- `RunWebHookWithContext(...)` is the framework runtime API; - `RunWebhookWithContext(...)` is the framework runtime API;
- `tgapi` webhook methods are the lower-level transport primitives. - `tgapi` webhook methods are the lower-level transport primitives.
## Common mistakes ## Common mistakes
+1 -1
@@ -82,7 +82,7 @@ Use the context-aware variants when cancellation, deadlines, or graceful shutdow
Useful options include: Useful options include:
- `SetHTTPClient(...)` - `SetHTTPClient(...)`
- `UseTestServer(...)` - `UseTestServer(...)`
- `SetAPIUrl(...)` - `SetAPIURL(...)`
- `SetLimiter(...)` - `SetLimiter(...)`
- `SetLimiterDrop(...)` - `SetLimiterDrop(...)`