REPOSITORY / ScuroNeko/Laniakea
Wiki
update for v1.0.0
@@ -116,7 +116,7 @@ There are two skip levels:
|
||||
### Skip a single command
|
||||
|
||||
```go
|
||||
plugin.NewCommand(exec, "internal").
|
||||
plugin.Command("internal", exec).
|
||||
SetDescription("Internal only").
|
||||
SkipCommandAutoGen()
|
||||
```
|
||||
|
||||
+6
-6
@@ -9,7 +9,7 @@ English version: [[Bot-Lifecycle]]
|
||||
1. Собрать `BotOpts`.
|
||||
2. Создать `Bot` через `NewBot[T](opts)`.
|
||||
3. Полностью настроить бот: плагины, middleware, фоновые задачи, политику данных callback, l10n и app data.
|
||||
4. Запустить через `Run()`, `RunWithContext(...)` или `RunWebHookWithContext(...)`.
|
||||
4. Запустить через `Run()`, `RunWithContext(...)` или `RunWebhookWithContext(...)`.
|
||||
5. Остановить выполнение через завершение runtime или отмену context.
|
||||
6. Освободить локальные ресурсы через `Close()`.
|
||||
7. Для следующего запуска создать новый `Bot`.
|
||||
@@ -66,7 +66,7 @@ English version: [[Bot-Lifecycle]]
|
||||
|
||||
1. Построение и настройка `Bot` после `NewBot[T](opts)`.
|
||||
2. Снимок конфигурации плагина в `AddPlugins(...)`.
|
||||
3. Фиксация bot-level конфигурации после первого `Run()`, `RunWithContext(...)` или `RunWebHookWithContext(...)`.
|
||||
3. Фиксация bot-level конфигурации после первого `Run()`, `RunWithContext(...)` или `RunWebhookWithContext(...)`.
|
||||
|
||||
Практически это значит:
|
||||
- структуру плагина нужно закончить до `AddPlugins(...)`;
|
||||
@@ -85,7 +85,7 @@ Laniakea предпочитает предсказуемый no-op вместо
|
||||
- путаницу в том, влияет ли изменение только на будущие update или ещё и на уже принятую работу;
|
||||
- разные ментальные модели для snapshot-поведения плагинов и bot-level состояния.
|
||||
|
||||
## `Run()`, `RunWithContext(...)` и `RunWebHookWithContext(...)`
|
||||
## `Run()`, `RunWithContext(...)` и `RunWebhookWithContext(...)`
|
||||
|
||||
`Run()` — это короткая форма для простых случаев.
|
||||
|
||||
@@ -94,11 +94,11 @@ Laniakea предпочитает предсказуемый no-op вместо
|
||||
- ждет завершения queued updates;
|
||||
- корректно дожидается фоновых задач.
|
||||
|
||||
`RunWebHookWithContext(...)` — webhook-вариант runtime. Он использует тот же single-use контракт, тот же запуск runners, ту же очередь обновлений и ту же worker-pool обработку.
|
||||
`RunWebhookWithContext(...)` — webhook-вариант runtime. Он использует тот же single-use контракт, тот же запуск runners, ту же очередь обновлений и ту же worker-pool обработку.
|
||||
|
||||
Если 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]].
|
||||
|
||||
@@ -131,7 +131,7 @@ Laniakea предпочитает предсказуемый no-op вместо
|
||||
|
||||
Обычно боту нужен именно `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
|
||||
1. Build `BotOpts` and call `NewBot`.
|
||||
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.
|
||||
5. Call `Close()` to release local resources.
|
||||
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)`.
|
||||
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:
|
||||
- finish plugin structure before `AddPlugins(...)`;
|
||||
@@ -97,10 +97,10 @@ if err != nil {
|
||||
defer bot.Close()
|
||||
|
||||
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")
|
||||
return nil
|
||||
}, "ping")
|
||||
})
|
||||
|
||||
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.
|
||||
|
||||
@@ -124,16 +124,16 @@ It:
|
||||
|
||||
`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;
|
||||
- runner startup behavior;
|
||||
- internal update queue;
|
||||
- worker-pool dispatch model;
|
||||
- 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]].
|
||||
|
||||
@@ -172,7 +172,7 @@ Canceling the runtime context tells the bot to stop accepting new work and finis
|
||||
- one-time async runners to finish;
|
||||
- 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()`
|
||||
|
||||
@@ -192,7 +192,7 @@ Use `CloseRemote(ctx)` only when you specifically need Telegram-side session shu
|
||||
|
||||
## 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:
|
||||
- do not call a runtime entry point again after a graceful stop;
|
||||
|
||||
@@ -46,10 +46,10 @@ opts := laniakea.LoadOptsFromEnv()
|
||||
`LoadBotOptsFile(...)` подходит, когда конфиг бота удобнее хранить в отдельном файле.
|
||||
|
||||
Из коробки доступно:
|
||||
- `BotOptsFileJsonCodec` для JSON.
|
||||
- `BotOptsFileJSONCodec` для JSON.
|
||||
|
||||
```go
|
||||
codec := laniakea.BotOptsFileJsonCodec{}
|
||||
codec := laniakea.BotOptsFileJSONCodec{}
|
||||
opts, err := laniakea.LoadBotOptsFile(codec, "config.json")
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -67,7 +67,7 @@ if err := laniakea.SaveBotOptsFile(codec, "config.json", opts); err != nil {
|
||||
Перед декодированием loader разворачивает плейсхолдеры вроде `{{ TG_TOKEN }}` из environment variables.
|
||||
|
||||
Из коробки библиотека пока поддерживает только 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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
Built in:
|
||||
- `BotOptsFileJsonCodec` for JSON files.
|
||||
- `BotOptsFileJSONCodec` for JSON files.
|
||||
|
||||
```go
|
||||
codec := laniakea.BotOptsFileJsonCodec{}
|
||||
codec := laniakea.BotOptsFileJSONCodec{}
|
||||
opts, err := laniakea.LoadBotOptsFile(codec, "config.json")
|
||||
if err != nil {
|
||||
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.
|
||||
|
||||
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
|
||||
|
||||
@@ -222,12 +222,12 @@ Env:
|
||||
|
||||
Use this only for development and testing scenarios that explicitly target Telegram's test environment.
|
||||
|
||||
### `APIUrl`
|
||||
### `APIURL`
|
||||
|
||||
Overrides the default Telegram API base URL.
|
||||
|
||||
Setter:
|
||||
- `SetAPIUrl(url)`
|
||||
- `SetAPIURL(url)`
|
||||
|
||||
Env:
|
||||
- `API_URL`
|
||||
|
||||
+11
-11
@@ -73,7 +73,7 @@ func start(ctx *laniakea.MsgContext, db *App) error {
|
||||
|
||||
```go
|
||||
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
|
||||
plugin.AddCommand(
|
||||
plugin.NewCommand(banUser, "ban",
|
||||
laniakea.NewCommandArg("user_id").
|
||||
SetValueType(laniakea.CommandValueIntType).
|
||||
SetRequired(),
|
||||
),
|
||||
plugin.Command(
|
||||
"ban",
|
||||
banUser,
|
||||
laniakea.NewCommandArg("user_id").
|
||||
SetValueType(laniakea.CommandValueInt).
|
||||
SetRequired(),
|
||||
)
|
||||
```
|
||||
|
||||
@@ -130,7 +130,7 @@ func confirmDelete(ctx *laniakea.MsgContext, db *App) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
plugin.AddPayload(plugin.NewPayload(confirmDelete, "delete.confirm"))
|
||||
plugin.Payload("delete.confirm", confirmDelete)
|
||||
```
|
||||
|
||||
Важно помнить:
|
||||
@@ -204,7 +204,7 @@ plugin.AddMiddleware(...)
|
||||
Добавляется через:
|
||||
|
||||
```go
|
||||
plugin.NewCommand(handler, "name").Use(middleware)
|
||||
plugin.Command("name", handler).Use(middleware)
|
||||
```
|
||||
|
||||
Подходит, когда проверка нужна только одной команде или одному обработчику данных callback.
|
||||
@@ -218,13 +218,13 @@ plugin.NewCommand(handler, "name").Use(middleware)
|
||||
Неправильно:
|
||||
|
||||
```go
|
||||
plugin.NewCommand(start, "/start")
|
||||
plugin.Command("/start", start)
|
||||
```
|
||||
|
||||
Правильно:
|
||||
|
||||
```go
|
||||
plugin.NewCommand(start, "start")
|
||||
plugin.Command("start", start)
|
||||
```
|
||||
|
||||
### Считать данные callback обычной командой
|
||||
|
||||
+19
-19
@@ -71,11 +71,11 @@ func start(ctx *laniakea.MsgContext, db *App) error {
|
||||
|
||||
## Registering commands
|
||||
|
||||
Create a command with `NewCommand(...)` and add it to a plugin:
|
||||
Create and register a command with `Plugin.Command(...)`:
|
||||
|
||||
```go
|
||||
plugin := laniakea.NewPlugin[*App]("main")
|
||||
plugin.AddCommand(plugin.NewCommand(start, "start"))
|
||||
plugin.Command("start", start)
|
||||
```
|
||||
|
||||
The command name:
|
||||
@@ -99,7 +99,7 @@ func echo(ctx *laniakea.MsgContext, db laniakea.NoData) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
plugin.AddCommand(plugin.NewCommand(echo, "echo"))
|
||||
plugin.Command("echo", echo)
|
||||
```
|
||||
|
||||
For `/echo hello world`:
|
||||
@@ -113,12 +113,12 @@ You can declare command arguments using `CommandArg`.
|
||||
Example:
|
||||
|
||||
```go
|
||||
plugin.AddCommand(
|
||||
plugin.NewCommand(banUser, "ban",
|
||||
laniakea.NewCommandArg("user_id").
|
||||
SetValueType(laniakea.CommandValueIntType).
|
||||
SetRequired(),
|
||||
),
|
||||
plugin.Command(
|
||||
"ban",
|
||||
banUser,
|
||||
laniakea.NewCommandArg("user_id").
|
||||
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.
|
||||
|
||||
Register them with `NewPayload(...)` or `AddPayload(...)`:
|
||||
Register them with `Plugin.Payload(...)` or `AddPayload(...)`:
|
||||
|
||||
```go
|
||||
func confirmDelete(ctx *laniakea.MsgContext, db *App) error {
|
||||
@@ -141,7 +141,7 @@ func confirmDelete(ctx *laniakea.MsgContext, db *App) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
plugin.AddPayload(plugin.NewPayload(confirmDelete, "delete.confirm"))
|
||||
plugin.Payload("delete.confirm", confirmDelete)
|
||||
```
|
||||
|
||||
Payload handlers:
|
||||
@@ -213,7 +213,7 @@ Use this for logic shared by most handlers in the plugin.
|
||||
Added with:
|
||||
|
||||
```go
|
||||
plugin.NewCommand(handler, "name").Use(middleware)
|
||||
plugin.Command("name", handler).Use(middleware)
|
||||
```
|
||||
|
||||
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
|
||||
}))
|
||||
|
||||
admin.AddCommand(admin.NewCommand(func(ctx *laniakea.MsgContext, app *App) error {
|
||||
admin.Command("ban", func(ctx *laniakea.MsgContext, app *App) error {
|
||||
ctx.Answer("Banned")
|
||||
return nil
|
||||
}, "ban"))
|
||||
})
|
||||
```
|
||||
|
||||
### Example: payload handler for inline keyboard callback
|
||||
|
||||
```go
|
||||
plugin.AddPayload(plugin.NewPayload(func(ctx *laniakea.MsgContext, app *App) error {
|
||||
ctx.AnswerCbQueryText("Accepted")
|
||||
plugin.Payload("approve", func(ctx *laniakea.MsgContext, app *App) error {
|
||||
ctx.AnswerCallbackText("Accepted")
|
||||
ctx.EditCallback("Done", nil)
|
||||
return nil
|
||||
}, "approve"))
|
||||
})
|
||||
```
|
||||
|
||||
## Common mistakes
|
||||
@@ -266,13 +266,13 @@ plugin.AddPayload(plugin.NewPayload(func(ctx *laniakea.MsgContext, app *App) err
|
||||
Wrong:
|
||||
|
||||
```go
|
||||
plugin.NewCommand(start, "/start")
|
||||
plugin.Command("/start", start)
|
||||
```
|
||||
|
||||
Right:
|
||||
|
||||
```go
|
||||
plugin.NewCommand(start, "start")
|
||||
plugin.Command("start", start)
|
||||
```
|
||||
|
||||
### 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`.
|
||||
|
||||
ID generation modes:
|
||||
- random IDs from `RandomDraftIdGenerator`;
|
||||
- monotonic IDs from `LinearDraftIdGenerator`.
|
||||
- random IDs from `RandomDraftIDGenerator`;
|
||||
- monotonic IDs from `LinearDraftIDGenerator`.
|
||||
|
||||
The ID is mainly useful when:
|
||||
- correlating draft activity in logs;
|
||||
|
||||
+1
-1
@@ -88,7 +88,7 @@ if err := db.WarmCache(); err != nil {
|
||||
Для callback-потока возвращённая ошибка превращается не в обычное сообщение в чат, а в ответ на callback query.
|
||||
|
||||
Если нужен другой UX, лучше:
|
||||
- вызвать `AnswerCbQueryText(...)` или `AnswerCbQueryAlert(...)`;
|
||||
- вызвать `AnswerCallbackText(...)` или `AnswerCallbackAlert(...)`;
|
||||
- вернуть `nil`.
|
||||
|
||||
## `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.
|
||||
|
||||
If you want a different callback UX, answer manually with:
|
||||
- `AnswerCbQuery()`
|
||||
- `AnswerCbQueryText(...)`
|
||||
- `AnswerCbQueryAlert(...)`
|
||||
- `AnswerCbQueryUrl(...)`
|
||||
- `AnswerCallback()`
|
||||
- `AnswerCallbackText(...)`
|
||||
- `AnswerCallbackAlert(...)`
|
||||
- `AnswerCallbackURL(...)`
|
||||
|
||||
and return `nil`.
|
||||
|
||||
@@ -183,38 +183,38 @@ Related page:
|
||||
### Centralized command failure
|
||||
|
||||
```go
|
||||
plugin.NewCommand(func(ctx *laniakea.MsgContext, db *App) error {
|
||||
plugin.Command("report", func(ctx *laniakea.MsgContext, db *App) error {
|
||||
result, err := db.DoWork()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to build report: %w", err)
|
||||
}
|
||||
ctx.Answer(result)
|
||||
return nil
|
||||
}, "report")
|
||||
})
|
||||
```
|
||||
|
||||
### Manual denial response
|
||||
|
||||
```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) {
|
||||
ctx.Answer("Access denied")
|
||||
return nil
|
||||
}
|
||||
return doProtectedWork(ctx, db)
|
||||
}, "admin")
|
||||
})
|
||||
```
|
||||
|
||||
### Callback-specific manual alert
|
||||
|
||||
```go
|
||||
plugin.NewPayload(func(ctx *laniakea.MsgContext, db *App) error {
|
||||
plugin.Payload("start", func(ctx *laniakea.MsgContext, db *App) error {
|
||||
if !ready {
|
||||
ctx.AnswerCbQueryAlert("This action is not available yet")
|
||||
ctx.AnswerCallbackAlert("This action is not available yet")
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}, "start")
|
||||
})
|
||||
```
|
||||
|
||||
## Recommendations
|
||||
|
||||
+7
-7
@@ -9,7 +9,7 @@
|
||||
Текущее состояние:
|
||||
|
||||
- В репозитории уже есть низкоуровневые 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 runtime, security-модель и правила перехода обратно на polling теперь описаны в основной документации и wiki.
|
||||
|
||||
@@ -21,12 +21,12 @@
|
||||
|
||||
Что теперь есть:
|
||||
|
||||
- `BotWebHookOpts`, `NewBotWebHookOpts()` и fluent helper-методы для webhook-конфигурации.
|
||||
- `RunWebHookWithContext(...)` и `RunWebHook(...)` как bot-owned точки входа runtime.
|
||||
- `BotWebhookOpts`, `NewBotWebhookOpts()` и fluent helper-методы для webhook-конфигурации.
|
||||
- `RunWebhookWithContext(...)` и `RunWebhook(...)` как bot-owned точки входа runtime.
|
||||
- Общая queued dispatch-модель, worker-pool обработка, запуск runners и single-use semantics для polling и webhook mode.
|
||||
- Fallback webhook `AllowedUpdates` к bot-level конфигурации типов update.
|
||||
- Валидация 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.
|
||||
|
||||
Практическая цель:
|
||||
@@ -117,7 +117,7 @@
|
||||
|
||||
Текущее состояние:
|
||||
|
||||
- Фреймворк теперь считает конфигурацию бота структурно завершённой после начала первого runtime entry point: `Run()`, `RunWithContext(...)` или `RunWebHookWithContext(...)`.
|
||||
- Фреймворк теперь считает конфигурацию бота структурно завершённой после начала первого runtime entry point: `Run()`, `RunWithContext(...)` или `RunWebhookWithContext(...)`.
|
||||
- Поздние bot-level попытки мутации больше не применяются частично после старта runtime.
|
||||
- Границы между регистрацией плагинов, стартом runtime и фиксацией конфигурации теперь оформлены как явное поведение фреймворка и закреплены тестами.
|
||||
|
||||
@@ -189,7 +189,7 @@
|
||||
Текущее направление API:
|
||||
|
||||
- `Scene`, `SceneContext`, `SceneSession` и `SessionStore`.
|
||||
- `Plugin.NewScene(...)` и `Plugin.AddScene(...)`.
|
||||
- `Plugin.Scene(...)` и `Plugin.AddScene(...)`.
|
||||
- `MsgContext.EnterScene(...)`, `EnterSceneStep(...)` и `ExitScene(...)`.
|
||||
- `SceneContext.Stay()`, `Next(...)`, `Exit()`, `Pass()`, `BindData(...)` и `SaveData(...)`.
|
||||
- Состояние на пользователя или чат с хранением в `SessionStore` и чистым интерфейсом для собственного постоянного хранения.
|
||||
@@ -263,7 +263,7 @@ func ban(ctx *laniakea.MsgContext, db *App) error {
|
||||
|
||||
Текущее состояние:
|
||||
|
||||
- `RunWithContext(...)` и `RunWebHookWithContext(...)` управляют жизненным циклом выполнения бота и корректным завершением.
|
||||
- `RunWithContext(...)` и `RunWebhookWithContext(...)` управляют жизненным циклом выполнения бота и корректным завершением.
|
||||
- `tgapi` уже поддерживает методы, принимающие `context.Context`.
|
||||
- Обычные обработчики не получают полноценный `context.Context`, привязанный к обработке конкретного запроса.
|
||||
|
||||
|
||||
+52
-7
@@ -4,12 +4,57 @@ This page tracks framework-level backlog items that are about missing concepts i
|
||||
|
||||
## 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
|
||||
|
||||
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 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.
|
||||
- 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:
|
||||
|
||||
- `BotWebHookOpts`, `NewBotWebHookOpts()`, and fluent helpers for webhook-specific configuration.
|
||||
- `RunWebHookWithContext(...)` and `RunWebHook(...)` as bot-owned runtime entry points.
|
||||
- `BotWebhookOpts`, `NewBotWebhookOpts()`, and fluent helpers for webhook-specific configuration.
|
||||
- `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.
|
||||
- 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.
|
||||
- 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.
|
||||
|
||||
Practical target:
|
||||
@@ -117,7 +162,7 @@ Practical target:
|
||||
|
||||
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.
|
||||
- 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:
|
||||
|
||||
- `Scene`, `SceneContext`, `SceneSession`, and `SessionStore`.
|
||||
- `Plugin.NewScene(...)` and `Plugin.AddScene(...)`.
|
||||
- `Plugin.Scene(...)` and `Plugin.AddScene(...)`.
|
||||
- `MsgContext.EnterScene(...)`, `EnterSceneStep(...)`, and `ExitScene(...)`.
|
||||
- `SceneContext.Stay()`, `Next(...)`, `Exit()`, `Pass()`, `BindData(...)`, and `SaveData(...)`.
|
||||
- Storage-backed per-user or per-chat state with a clean interface for custom persistence.
|
||||
@@ -263,7 +308,7 @@ Practical target:
|
||||
|
||||
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.
|
||||
- Regular handlers do not receive a first-class request-scoped `context.Context`.
|
||||
|
||||
|
||||
+5
-5
@@ -48,7 +48,7 @@ func main() {
|
||||
defer bot.Close()
|
||||
|
||||
plugin := laniakea.NewPlugin[laniakea.NoData]("main")
|
||||
plugin.AddCommand(plugin.NewCommand(ping, "ping"))
|
||||
plugin.Command("ping", ping)
|
||||
|
||||
bot.AddPlugins(plugin)
|
||||
|
||||
@@ -100,7 +100,7 @@ bot.SetAppData(app)
|
||||
|
||||
```go
|
||||
plugin := laniakea.NewPlugin[laniakea.NoData]("admin")
|
||||
plugin.AddCommand(plugin.NewCommand(ping, "ping"))
|
||||
plugin.Command("ping", ping)
|
||||
bot.AddPlugins(plugin)
|
||||
```
|
||||
|
||||
@@ -136,7 +136,7 @@ func profile(ctx *laniakea.MsgContext, db *App) error {
|
||||
|
||||
### 4. `Bot` single-use
|
||||
|
||||
После `Run()`, `RunWithContext(...)` или `RunWebHookWithContext(...)` нельзя снова запускать тот же экземпляр `Bot`.
|
||||
После `Run()`, `RunWithContext(...)` или `RunWebhookWithContext(...)` нельзя снова запускать тот же экземпляр `Bot`.
|
||||
|
||||
Правильная модель:
|
||||
- создать bot
|
||||
@@ -151,7 +151,7 @@ func profile(ctx *laniakea.MsgContext, db *App) error {
|
||||
|
||||
### 5. `Close()` все равно нужен
|
||||
|
||||
Даже если ты используешь `Run()`, `RunWithContext(...)` или `RunWebHookWithContext(...)`, ресурсы нужно закрывать явно:
|
||||
Даже если ты используешь `Run()`, `RunWithContext(...)` или `RunWebhookWithContext(...)`, ресурсы нужно закрывать явно:
|
||||
|
||||
```go
|
||||
defer bot.Close()
|
||||
@@ -168,7 +168,7 @@ defer bot.Close()
|
||||
5. Добавить команды, payloads и middleware в плагины
|
||||
6. Зарегистрировать плагины через `AddPlugins(...)`
|
||||
7. При необходимости вызвать `AutoGenerateCommands()`
|
||||
8. Вызвать `Run()`, `RunWithContext(...)` или `RunWebHookWithContext(...)`
|
||||
8. Вызвать `Run()`, `RunWithContext(...)` или `RunWebhookWithContext(...)`
|
||||
9. Закрыть bot через `Close()`
|
||||
|
||||
## Частые ошибки на старте
|
||||
|
||||
+6
-6
@@ -49,7 +49,7 @@ func main() {
|
||||
defer bot.Close()
|
||||
|
||||
plugin := laniakea.NewPlugin[laniakea.NoData]("main")
|
||||
plugin.AddCommand(plugin.NewCommand(ping, "ping"))
|
||||
plugin.Command("ping", ping)
|
||||
|
||||
bot.AddPlugins(plugin)
|
||||
|
||||
@@ -102,7 +102,7 @@ Example:
|
||||
|
||||
```go
|
||||
plugin := laniakea.NewPlugin[laniakea.NoData]("admin")
|
||||
plugin.AddCommand(plugin.NewCommand(ping, "ping"))
|
||||
plugin.Command("ping", ping)
|
||||
bot.AddPlugins(plugin)
|
||||
```
|
||||
|
||||
@@ -139,7 +139,7 @@ func profile(ctx *laniakea.MsgContext, db *App) error {
|
||||
|
||||
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
|
||||
- create a new bot instance for the next run
|
||||
|
||||
@@ -149,7 +149,7 @@ See [[Bot-Lifecycle]] for details.
|
||||
|
||||
### 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:
|
||||
|
||||
@@ -168,7 +168,7 @@ For most bots, this order is the least surprising:
|
||||
5. Add commands, payloads, and middleware to plugins
|
||||
6. Register plugins with `AddPlugins(...)`
|
||||
7. Optionally call `AutoGenerateCommands()`
|
||||
8. Call `Run()`, `RunWithContext(...)`, or `RunWebHookWithContext(...)`
|
||||
8. Call `Run()`, `RunWithContext(...)`, or `RunWebhookWithContext(...)`
|
||||
9. Call `Close()` when done
|
||||
|
||||
## A slightly more realistic example
|
||||
@@ -207,7 +207,7 @@ func main() {
|
||||
bot.SetAppData(&App{})
|
||||
|
||||
plugin := laniakea.NewPlugin[*App]("main")
|
||||
plugin.AddCommand(plugin.NewCommand(echo, "echo"))
|
||||
plugin.Command("echo", echo)
|
||||
|
||||
bot.AddPlugins(plugin)
|
||||
|
||||
|
||||
@@ -15,17 +15,17 @@ English version: [[Inline-Keyboards-and-Payloads]]
|
||||
## Как строить клавиатуру
|
||||
|
||||
Основные конструкторы:
|
||||
- `NewInlineKeyboardJson(maxRow)`
|
||||
- `NewInlineKeyboardJSON(maxRow)`
|
||||
- `NewInlineKeyboardBase64(maxRow)`
|
||||
- `NewInlineKeyboard(payloadType, maxRow)`
|
||||
|
||||
Пример:
|
||||
|
||||
```go
|
||||
kb := laniakea.NewInlineKeyboardJson(2).
|
||||
kb := laniakea.NewInlineKeyboardJSON(2).
|
||||
AddCallbackButton("Open", "open", 42).
|
||||
AddCallbackButton("Delete", "delete", 42).
|
||||
AddUrlButton("Docs", "https://example.com/docs")
|
||||
AddURLButton("Docs", "https://example.com/docs")
|
||||
```
|
||||
|
||||
`maxRow` определяет, сколько кнопок автоматически помещается в один ряд.
|
||||
@@ -54,7 +54,7 @@ kb := laniakea.NewInlineKeyboardJson(2).
|
||||
|
||||
## JSON vs Base64
|
||||
|
||||
`BotPayloadJson`:
|
||||
`BotPayloadJSON`:
|
||||
- удобнее читать в логах и тестах;
|
||||
- проще отлаживать.
|
||||
|
||||
@@ -85,7 +85,7 @@ bot.SetStrictPayloadType(true)
|
||||
|
||||
```go
|
||||
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
|
||||
|
||||
The most direct constructors are:
|
||||
- `laniakea.NewInlineKeyboardJson(maxRow)`
|
||||
- `laniakea.NewInlineKeyboardJSON(maxRow)`
|
||||
- `laniakea.NewInlineKeyboardBase64(maxRow)`
|
||||
- `laniakea.NewInlineKeyboard(payloadType, maxRow)`
|
||||
|
||||
@@ -25,10 +25,10 @@ The most direct constructors are:
|
||||
Example:
|
||||
|
||||
```go
|
||||
kb := laniakea.NewInlineKeyboardJson(2).
|
||||
kb := laniakea.NewInlineKeyboardJSON(2).
|
||||
AddCallbackButton("Open", "open", 42).
|
||||
AddCallbackButton("Delete", "delete", 42).
|
||||
AddUrlButton("Docs", "https://example.com/docs")
|
||||
AddURLButton("Docs", "https://example.com/docs")
|
||||
```
|
||||
|
||||
In that example:
|
||||
@@ -77,7 +77,7 @@ All payload arguments are converted with `fmt.Sprint`, so handlers receive strin
|
||||
Example:
|
||||
|
||||
```go
|
||||
kb := laniakea.NewInlineKeyboardJson(1).
|
||||
kb := laniakea.NewInlineKeyboardJSON(1).
|
||||
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:
|
||||
|
||||
- `BotPayloadJson`
|
||||
- `BotPayloadJSON`
|
||||
- `BotPayloadBase64`
|
||||
|
||||
JSON is easier to inspect in logs and tests.
|
||||
@@ -132,7 +132,7 @@ For a single keyboard, you can override it locally:
|
||||
|
||||
```go
|
||||
kb := ctx.NewInlineKeyboard(2).
|
||||
SetPayloadType(laniakea.BotPayloadJson).
|
||||
SetPayloadType(laniakea.BotPayloadJSON).
|
||||
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
|
||||
|
||||
`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:
|
||||
|
||||
```go
|
||||
button := laniakea.NewInlineKbButton("Confirm").
|
||||
button := laniakea.NewInlineKeyboardButton("Confirm").
|
||||
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:
|
||||
- `SetStyle(...)`
|
||||
- `SetUrl(...)`
|
||||
- `SetIconCustomEmojiId(...)`
|
||||
- `SetCallbackDataJson(...)`
|
||||
- `SetURL(...)`
|
||||
- `SetIconCustomEmojiID(...)`
|
||||
- `SetCallbackDataJSON(...)`
|
||||
- `SetCallbackDataBase64(...)`
|
||||
|
||||
Use it when `AddCallbackButton(...)` is not expressive enough.
|
||||
@@ -223,9 +223,9 @@ Laniakea exposes three convenience style constants:
|
||||
- `ButtonStyleDanger`
|
||||
|
||||
You can use them with:
|
||||
- `AddUrlButtonStyle(...)`
|
||||
- `AddURLButtonStyle(...)`
|
||||
- `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.
|
||||
|
||||
@@ -241,17 +241,17 @@ If your plain-text reply may exceed Telegram’s message length limit, use:
|
||||
## Routing payloads to handlers
|
||||
|
||||
Payloads are registered on plugins with:
|
||||
- `Plugin.NewPayload(...)`
|
||||
- `Plugin.Payload(...)`
|
||||
- `Plugin.AddPayload(...)`
|
||||
|
||||
Example:
|
||||
|
||||
```go
|
||||
plugin.NewPayload(func(ctx *laniakea.MsgContext, db *App) error {
|
||||
plugin.Payload("inspect", func(ctx *laniakea.MsgContext, db *App) error {
|
||||
id := ctx.Args[0]
|
||||
ctx.AnswerCbQueryText("Handled " + id)
|
||||
ctx.AnswerCallbackText("Handled " + id)
|
||||
return nil
|
||||
}, "inspect")
|
||||
})
|
||||
```
|
||||
|
||||
When a callback arrives:
|
||||
|
||||
+1
-1
@@ -200,7 +200,7 @@ admin.AddMiddleware(
|
||||
### Command-specific validation
|
||||
|
||||
```go
|
||||
ban := admin.NewCommand(banUser, "ban")
|
||||
ban := admin.Command("ban", banUser)
|
||||
ban.Use(laniakea.NewMiddleware("require-reply", func(ctx *laniakea.MsgContext, db *App) bool {
|
||||
if ctx.Msg == nil || ctx.Msg.ReplyToMessage == nil {
|
||||
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
|
||||
|
||||
- `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(...)`.
|
||||
- Message and caption validation now happens before sending Telegram API requests.
|
||||
- Optional strict callback payload decoding was added through `StrictPayloadType`.
|
||||
|
||||
+4
-4
@@ -97,10 +97,10 @@ English version: [[MsgContext]]
|
||||
|
||||
В потоке callback особенно полезны:
|
||||
- `EditCallback(...)`
|
||||
- `AnswerCbQuery()`
|
||||
- `AnswerCbQueryText(...)`
|
||||
- `AnswerCbQueryAlert(...)`
|
||||
- `AnswerCbQueryUrl(...)`
|
||||
- `AnswerCallback()`
|
||||
- `AnswerCallbackText(...)`
|
||||
- `AnswerCallbackAlert(...)`
|
||||
- `AnswerCallbackURL(...)`
|
||||
- `CallbackDelete()`
|
||||
|
||||
Они покрывают самые частые callback-сценарии без ручного хождения в `tgapi`.
|
||||
|
||||
+7
-7
@@ -179,21 +179,21 @@ func approve(ctx *laniakea.MsgContext, db laniakea.NoData) error {
|
||||
}
|
||||
```
|
||||
|
||||
### `AnswerCbQuery`
|
||||
### `AnswerCallback`
|
||||
|
||||
Acknowledges the callback query itself.
|
||||
|
||||
Use:
|
||||
- `AnswerCbQuery()` for empty acknowledgement
|
||||
- `AnswerCbQueryText(...)` for a short notice
|
||||
- `AnswerCbQueryAlert(...)` for a visible alert
|
||||
- `AnswerCbQueryUrl(...)` for redirect behavior
|
||||
- `AnswerCallback()` for empty acknowledgement
|
||||
- `AnswerCallbackText(...)` for a short notice
|
||||
- `AnswerCallbackAlert(...)` for a visible alert
|
||||
- `AnswerCallbackURL(...)` for redirect behavior
|
||||
|
||||
Example:
|
||||
|
||||
```go
|
||||
func approve(ctx *laniakea.MsgContext, db laniakea.NoData) error {
|
||||
ctx.AnswerCbQueryText("Saved")
|
||||
ctx.AnswerCallbackText("Saved")
|
||||
ctx.EditCallback("Saved", 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
|
||||
|
||||
Methods like `EditCallback(...)` and `AnswerCbQueryText(...)` depend on callback-specific context.
|
||||
Methods like `EditCallback(...)` and `AnswerCallbackText(...)` depend on callback-specific context.
|
||||
|
||||
## A practical example
|
||||
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@ English version: [[Recipes]]
|
||||
Комбинируй:
|
||||
- `NewInlineKeyboard(...)`
|
||||
- обработчик данных callback;
|
||||
- `AnswerCbQuery...`;
|
||||
- `AnswerCallback...`;
|
||||
- `EditCallback(...)`
|
||||
|
||||
### 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)
|
||||
}))
|
||||
|
||||
admin.NewCommand(func(ctx *laniakea.MsgContext, app *App) error {
|
||||
admin.Command("reload", func(ctx *laniakea.MsgContext, app *App) error {
|
||||
ctx.Answer("Admin command executed")
|
||||
return nil
|
||||
}, "reload")
|
||||
})
|
||||
```
|
||||
|
||||
## Callback button flow
|
||||
@@ -31,17 +31,17 @@ Use a payload handler for inline keyboard callbacks.
|
||||
```go
|
||||
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.AddCallbackButton("Open settings", "settings")
|
||||
ctx.Keyboard("Choose an action", kb)
|
||||
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)
|
||||
return nil
|
||||
}, "settings")
|
||||
})
|
||||
```
|
||||
|
||||
## 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.
|
||||
|
||||
```go
|
||||
plugin.NewCommand(func(ctx *laniakea.MsgContext, db *App) error {
|
||||
plugin.Command("report", func(ctx *laniakea.MsgContext, db *App) error {
|
||||
report := buildLargePlainTextReport()
|
||||
ctx.AnswerLong(report)
|
||||
return nil
|
||||
}, "report")
|
||||
})
|
||||
```
|
||||
|
||||
If you need an inline keyboard on the final chunk, use `KeyboardLong(...)`.
|
||||
@@ -71,10 +71,10 @@ l10n := laniakea.NewL10n("en").
|
||||
|
||||
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"))
|
||||
return nil
|
||||
}, "start")
|
||||
})
|
||||
```
|
||||
|
||||
## 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.
|
||||
|
||||
```go
|
||||
plugin.NewCommand(func(ctx *laniakea.MsgContext, db *App) error {
|
||||
plugin.Command("build", func(ctx *laniakea.MsgContext, db *App) error {
|
||||
draft := ctx.NewDraft()
|
||||
if draft == nil {
|
||||
return nil
|
||||
@@ -111,7 +111,7 @@ plugin.NewCommand(func(ctx *laniakea.MsgContext, db *App) error {
|
||||
}
|
||||
|
||||
return draft.Flush()
|
||||
}, "build")
|
||||
})
|
||||
```
|
||||
|
||||
## 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.
|
||||
|
||||
```go
|
||||
plugin.NewCommand(func(ctx *laniakea.MsgContext, db *App) error {
|
||||
uploader := tgapi.NewUploader(ctx.Api)
|
||||
plugin.Command("upload", func(ctx *laniakea.MsgContext, db *App) error {
|
||||
uploader := tgapi.NewUploader(ctx.API)
|
||||
defer uploader.Close()
|
||||
|
||||
_, err := uploader.SendPhoto(tgapi.UploadPhoto{
|
||||
ChatID: ctx.Msg.Chat.ID,
|
||||
}, tgapi.NewUploaderFile("report.jpg", []byte("hello")))
|
||||
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`.
|
||||
|
||||
+4
-4
@@ -23,9 +23,9 @@ runner := laniakea.NewRunner("cleanup", fn)
|
||||
```
|
||||
|
||||
Потом конфигурируются методы builder:
|
||||
- `Onetime(bool)`
|
||||
- `Once(bool)`
|
||||
- `Async(bool)`
|
||||
- `Timeout(duration)`
|
||||
- `Every(duration)`
|
||||
|
||||
## Основные режимы
|
||||
|
||||
@@ -51,11 +51,11 @@ runner := laniakea.NewRunner("cleanup", fn)
|
||||
|
||||
Повторяющийся 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.
|
||||
|
||||
Main builder methods:
|
||||
- `Onetime(bool)`
|
||||
- `Once(bool)`
|
||||
- `Async(bool)`
|
||||
- `Timeout(duration)`
|
||||
- `Every(duration)`
|
||||
|
||||
## Creating a runner
|
||||
|
||||
@@ -43,7 +43,7 @@ There are three meaningful configurations.
|
||||
|
||||
```go
|
||||
runner := laniakea.NewRunner("warmup", fn).
|
||||
Onetime(true).
|
||||
Once(true).
|
||||
Async(false)
|
||||
```
|
||||
|
||||
@@ -58,7 +58,7 @@ Use this for startup work that must complete before the bot is considered ready.
|
||||
|
||||
```go
|
||||
runner := laniakea.NewRunner("prefetch", fn).
|
||||
Onetime(true)
|
||||
Once(true)
|
||||
```
|
||||
|
||||
Behavior:
|
||||
@@ -72,7 +72,7 @@ Use this for fire-and-forget startup work that is useful but not required before
|
||||
|
||||
```go
|
||||
runner := laniakea.NewRunner("cleanup", fn).
|
||||
Timeout(time.Minute)
|
||||
Every(time.Minute)
|
||||
```
|
||||
|
||||
Behavior:
|
||||
@@ -86,13 +86,13 @@ Use this for recurring background jobs.
|
||||
|
||||
One configuration is intentionally treated as invalid:
|
||||
|
||||
- `Onetime(false).Async(false)`
|
||||
- `Once(false).Async(false)`
|
||||
|
||||
That means:
|
||||
- synchronous repeating runners are skipped;
|
||||
- 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
|
||||
|
||||
@@ -106,7 +106,7 @@ Runners with an empty name are skipped with a warning, so always give them a sta
|
||||
|
||||
## 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.
|
||||
|
||||
@@ -134,7 +134,7 @@ If a runner must be fatal for startup, make it one-time synchronous and return a
|
||||
|
||||
## 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;
|
||||
- background repeating runners.
|
||||
|
||||
@@ -149,7 +149,7 @@ This means graceful shutdown includes runner shutdown, but only if your runner f
|
||||
```go
|
||||
cleanup := laniakea.NewRunner("cleanup", func(bot *laniakea.Bot[*App]) error {
|
||||
return bot.GetAppData().CleanupExpired()
|
||||
}).Timeout(5 * time.Minute)
|
||||
}).Every(5 * time.Minute)
|
||||
```
|
||||
|
||||
### Startup warmup
|
||||
@@ -157,7 +157,7 @@ cleanup := laniakea.NewRunner("cleanup", func(bot *laniakea.Bot[*App]) error {
|
||||
```go
|
||||
warmup := laniakea.NewRunner("warmup", func(bot *laniakea.Bot[*App]) error {
|
||||
return bot.GetAppData().WarmCaches()
|
||||
}).Onetime(true).Async(false)
|
||||
}).Once(true).Async(false)
|
||||
```
|
||||
|
||||
### Background metrics push
|
||||
@@ -165,14 +165,14 @@ warmup := laniakea.NewRunner("warmup", func(bot *laniakea.Bot[*App]) error {
|
||||
```go
|
||||
metrics := laniakea.NewRunner("metrics", func(bot *laniakea.Bot[*App]) error {
|
||||
return pushMetrics(bot.GetAppData())
|
||||
}).Timeout(30 * time.Second)
|
||||
}).Every(30 * time.Second)
|
||||
```
|
||||
|
||||
## Recommendations
|
||||
|
||||
- Use one-time sync runners only for short startup-critical work.
|
||||
- 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.
|
||||
- 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()`.
|
||||
- Области действия сессии на пользователя, чат или пару пользователь-чат.
|
||||
- Обработчики шагов, локальные команды сцены и резервный обработчик сообщений на уровне сцены.
|
||||
@@ -54,7 +54,7 @@ type SessionStore interface {
|
||||
Сцены регистрируются внутри плагина в том же стиле, что и команды с данными callback.
|
||||
|
||||
```go
|
||||
plugin.NewScene("signup").
|
||||
plugin.Scene("signup").
|
||||
SetScope(laniakea.SceneScopeUserChat).
|
||||
SetEntry("ask_name").
|
||||
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
|
||||
|
||||
- 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()`.
|
||||
- Per-user, per-chat, or per-user-chat session scopes.
|
||||
- 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.
|
||||
|
||||
```go
|
||||
plugin.NewScene("signup").
|
||||
plugin.Scene("signup").
|
||||
SetScope(laniakea.SceneScopeUserChat).
|
||||
SetEntry("ask_name").
|
||||
OnStep("ask_name", askName).
|
||||
|
||||
+1
-1
@@ -53,7 +53,7 @@ In broad terms:
|
||||
- patch version: backward-compatible fixes and clarifications;
|
||||
- `-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`
|
||||
- `VersionMajor`
|
||||
- `VersionMinor`
|
||||
|
||||
+44
-44
@@ -10,12 +10,12 @@ English version: [[Update-Routing-Model]]
|
||||
|
||||
Сейчас Laniakea маршрутизирует обновления так:
|
||||
|
||||
| Update type | Routing path |
|
||||
|---|---|
|
||||
| `message` | command flow |
|
||||
| `channel_post` | command flow |
|
||||
| `callback_query` | payload flow |
|
||||
| всё остальное | `AddUpdateHandler(...)` |
|
||||
| Update type | Routing path |
|
||||
|------------------|-------------------------|
|
||||
| `message` | command flow |
|
||||
| `channel_post` | command flow |
|
||||
| `callback_query` | payload flow |
|
||||
| всё остальное | `AddUpdateHandler(...)` |
|
||||
|
||||
Важно:
|
||||
- `message`, `channel_post` и `callback_query` зарезервированы и не должны регистрироваться через `AddUpdateHandler(...)`.
|
||||
@@ -65,7 +65,7 @@ Payload flow применяется только к:
|
||||
|
||||
Базовые гарантии:
|
||||
- `ctx.Update` всегда заполнен
|
||||
- `ctx.CallbackQueryId` заполнен
|
||||
- `ctx.CallbackQueryID` заполнен
|
||||
- `ctx.From` и `ctx.FromID` заполняются, если Telegram прислал user
|
||||
- `ctx.Args` заполняется из декодированных данных callback
|
||||
- `ctx.Logger` переключается на logger совпавшего плагина, если он задан
|
||||
@@ -80,15 +80,15 @@ Payload flow применяется только к:
|
||||
|
||||
Гарантии:
|
||||
- `ctx.Msg` заполнен
|
||||
- `ctx.CallbackMsgId` заполнен
|
||||
- `ctx.InlineMsgId == ""`
|
||||
- `ctx.CallbackMsgID` заполнен
|
||||
- `ctx.InlineMsgID == ""`
|
||||
|
||||
### Callback query, привязанный к inline message
|
||||
|
||||
Гарантии:
|
||||
- `ctx.Msg == nil`
|
||||
- `ctx.CallbackMsgId == 0`
|
||||
- `ctx.InlineMsgId` заполнен
|
||||
- `ctx.CallbackMsgID == 0`
|
||||
- `ctx.InlineMsgID` заполнен
|
||||
|
||||
## Контракт generic update handlers
|
||||
|
||||
@@ -110,14 +110,14 @@ Generic update handlers регистрируются через `Plugin.AddUpdat
|
||||
|
||||
### Update types, которые несут message
|
||||
|
||||
| Update type | `ctx.Msg` | `ctx.From` / `ctx.FromID` |
|
||||
|---|---|---|
|
||||
| `message` | да | да, если есть `Msg.From` |
|
||||
| `edited_message` | да | да, если есть `Msg.From` |
|
||||
| `channel_post` | да | только если есть `Msg.From` |
|
||||
| `edited_channel_post` | да | только если есть `Msg.From` |
|
||||
| `business_message` | да | да, если есть `Msg.From` |
|
||||
| `edited_business_message` | да | да, если есть `Msg.From` |
|
||||
| Update type | `ctx.Msg` | `ctx.From` / `ctx.FromID` |
|
||||
|---------------------------|-----------|-----------------------------|
|
||||
| `message` | да | да, если есть `Msg.From` |
|
||||
| `edited_message` | да | да, если есть `Msg.From` |
|
||||
| `channel_post` | да | только если есть `Msg.From` |
|
||||
| `edited_channel_post` | да | только если есть `Msg.From` |
|
||||
| `business_message` | да | да, если есть `Msg.From` |
|
||||
| `edited_business_message` | да | да, если есть `Msg.From` |
|
||||
|
||||
Важно:
|
||||
- message-backed не означает command-routed
|
||||
@@ -125,37 +125,37 @@ Generic update handlers регистрируются через `Plugin.AddUpdat
|
||||
|
||||
### Update types с user, но без message
|
||||
|
||||
| Update type | `ctx.Msg` | `ctx.From` / `ctx.FromID` |
|
||||
|---|---|---|
|
||||
| `inline_query` | нет | да |
|
||||
| `chosen_inline_result` | нет | да |
|
||||
| `shipping_query` | нет | да |
|
||||
| `pre_checkout_query` | нет | да |
|
||||
| `purchased_paid_media` | нет | да |
|
||||
| `my_chat_member` | нет | да |
|
||||
| `chat_member` | нет | да |
|
||||
| `chat_join_request` | нет | да |
|
||||
| `business_connection` | нет | да |
|
||||
| `poll_answer` | нет | да |
|
||||
| `message_reaction` | нет | да, если Telegram прислал `User` |
|
||||
| `chat_boost` | нет | да |
|
||||
| `removed_chat_boost` | нет | да |
|
||||
| Update type | `ctx.Msg` | `ctx.From` / `ctx.FromID` |
|
||||
|------------------------|-----------|----------------------------------|
|
||||
| `inline_query` | нет | да |
|
||||
| `chosen_inline_result` | нет | да |
|
||||
| `shipping_query` | нет | да |
|
||||
| `pre_checkout_query` | нет | да |
|
||||
| `purchased_paid_media` | нет | да |
|
||||
| `my_chat_member` | нет | да |
|
||||
| `chat_member` | нет | да |
|
||||
| `chat_join_request` | нет | да |
|
||||
| `business_connection` | нет | да |
|
||||
| `poll_answer` | нет | да |
|
||||
| `message_reaction` | нет | да, если Telegram прислал `User` |
|
||||
| `chat_boost` | нет | да |
|
||||
| `removed_chat_boost` | нет | да |
|
||||
|
||||
### Callback-specific update type
|
||||
|
||||
| Update type | `ctx.Msg` | `ctx.From` / `ctx.FromID` | Спецполя |
|
||||
|---|---|---|---|
|
||||
| `callback_query` с `Message` | да | да | `CallbackQueryId`, `CallbackMsgId` |
|
||||
| `callback_query` с `InlineMessageID` | нет | да | `CallbackQueryId`, `InlineMsgId` |
|
||||
| Update type | `ctx.Msg` | `ctx.From` / `ctx.FromID` | Спецполя |
|
||||
|--------------------------------------|-----------|---------------------------|------------------------------------|
|
||||
| `callback_query` с `Message` | да | да | `CallbackQueryID`, `CallbackMsgID` |
|
||||
| `callback_query` с `InlineMessageID` | нет | да | `CallbackQueryID`, `InlineMsgID` |
|
||||
|
||||
### Update types без нормализованных гарантий по user/message
|
||||
|
||||
| Update type | `ctx.Msg` | `ctx.From` / `ctx.FromID` |
|
||||
|---|---|---|
|
||||
| `poll` | нет | нет |
|
||||
| `message_reaction_count` | нет | нет |
|
||||
| `deleted_business_messages` | нет | нет |
|
||||
| `unknown` | нет | нет |
|
||||
| Update type | `ctx.Msg` | `ctx.From` / `ctx.FromID` |
|
||||
|-----------------------------|-----------|---------------------------|
|
||||
| `poll` | нет | нет |
|
||||
| `message_reaction_count` | нет | нет |
|
||||
| `deleted_business_messages` | нет | нет |
|
||||
| `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:
|
||||
|
||||
| Update type | Routing path |
|
||||
|---|---|
|
||||
| `message` | command flow |
|
||||
| `channel_post` | command flow |
|
||||
| `callback_query` | payload flow |
|
||||
| everything else | `AddUpdateHandler(...)` |
|
||||
| Update type | Routing path |
|
||||
|------------------|-------------------------|
|
||||
| `message` | command flow |
|
||||
| `channel_post` | command flow |
|
||||
| `callback_query` | payload flow |
|
||||
| everything else | `AddUpdateHandler(...)` |
|
||||
|
||||
Important:
|
||||
- `message`, `channel_post`, and `callback_query` are reserved from `AddUpdateHandler(...)`.
|
||||
@@ -65,7 +65,7 @@ Payload flow applies only to:
|
||||
|
||||
Base guarantees:
|
||||
- `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.Args` is populated from decoded payload args
|
||||
- `ctx.Logger` switches to the matched plugin logger when one exists
|
||||
@@ -80,15 +80,15 @@ There are two payload target shapes.
|
||||
|
||||
Guarantees:
|
||||
- `ctx.Msg` is present
|
||||
- `ctx.CallbackMsgId` is populated
|
||||
- `ctx.InlineMsgId == ""`
|
||||
- `ctx.CallbackMsgID` is populated
|
||||
- `ctx.InlineMsgID == ""`
|
||||
|
||||
### Callback query targeting an inline message
|
||||
|
||||
Guarantees:
|
||||
- `ctx.Msg == nil`
|
||||
- `ctx.CallbackMsgId == 0`
|
||||
- `ctx.InlineMsgId` is populated
|
||||
- `ctx.CallbackMsgID == 0`
|
||||
- `ctx.InlineMsgID` is populated
|
||||
|
||||
## Generic update handler contract
|
||||
|
||||
@@ -110,14 +110,14 @@ Important limitation:
|
||||
|
||||
### Message-backed update kinds
|
||||
|
||||
| Update type | `ctx.Msg` | `ctx.From` / `ctx.FromID` |
|
||||
|---|---|---|
|
||||
| `message` | yes | yes, when `Msg.From` exists |
|
||||
| `edited_message` | yes | yes, when `Msg.From` exists |
|
||||
| `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 |
|
||||
| `edited_business_message` | yes | yes, when `Msg.From` exists |
|
||||
| Update type | `ctx.Msg` | `ctx.From` / `ctx.FromID` |
|
||||
|---------------------------|-----------|-----------------------------|
|
||||
| `message` | yes | yes, when `Msg.From` exists |
|
||||
| `edited_message` | yes | yes, when `Msg.From` exists |
|
||||
| `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 |
|
||||
| `edited_business_message` | yes | yes, when `Msg.From` exists |
|
||||
|
||||
Important:
|
||||
- message-backed does **not** mean command-routed
|
||||
@@ -125,37 +125,37 @@ Important:
|
||||
|
||||
### User-backed but not message-backed update kinds
|
||||
|
||||
| Update type | `ctx.Msg` | `ctx.From` / `ctx.FromID` |
|
||||
|---|---|---|
|
||||
| `inline_query` | no | yes |
|
||||
| `chosen_inline_result` | no | yes |
|
||||
| `shipping_query` | no | yes |
|
||||
| `pre_checkout_query` | no | yes |
|
||||
| `purchased_paid_media` | no | yes |
|
||||
| `my_chat_member` | no | yes |
|
||||
| `chat_member` | no | yes |
|
||||
| `chat_join_request` | no | yes |
|
||||
| `business_connection` | no | yes |
|
||||
| `poll_answer` | no | yes |
|
||||
| `message_reaction` | no | yes, when Telegram includes `User` |
|
||||
| `chat_boost` | no | yes |
|
||||
| `removed_chat_boost` | no | yes |
|
||||
| Update type | `ctx.Msg` | `ctx.From` / `ctx.FromID` |
|
||||
|------------------------|-----------|------------------------------------|
|
||||
| `inline_query` | no | yes |
|
||||
| `chosen_inline_result` | no | yes |
|
||||
| `shipping_query` | no | yes |
|
||||
| `pre_checkout_query` | no | yes |
|
||||
| `purchased_paid_media` | no | yes |
|
||||
| `my_chat_member` | no | yes |
|
||||
| `chat_member` | no | yes |
|
||||
| `chat_join_request` | no | yes |
|
||||
| `business_connection` | no | yes |
|
||||
| `poll_answer` | no | yes |
|
||||
| `message_reaction` | no | yes, when Telegram includes `User` |
|
||||
| `chat_boost` | no | yes |
|
||||
| `removed_chat_boost` | no | yes |
|
||||
|
||||
### Callback-specific update kind
|
||||
|
||||
| Update type | `ctx.Msg` | `ctx.From` / `ctx.FromID` | Special fields |
|
||||
|---|---|---|---|
|
||||
| `callback_query` with `Message` | yes | yes | `CallbackQueryId`, `CallbackMsgId` |
|
||||
| `callback_query` with `InlineMessageID` | no | yes | `CallbackQueryId`, `InlineMsgId` |
|
||||
| Update type | `ctx.Msg` | `ctx.From` / `ctx.FromID` | Special fields |
|
||||
|-----------------------------------------|-----------|---------------------------|------------------------------------|
|
||||
| `callback_query` with `Message` | yes | yes | `CallbackQueryID`, `CallbackMsgID` |
|
||||
| `callback_query` with `InlineMessageID` | no | yes | `CallbackQueryID`, `InlineMsgID` |
|
||||
|
||||
### Update kinds without normalized user/message guarantees
|
||||
|
||||
| Update type | `ctx.Msg` | `ctx.From` / `ctx.FromID` |
|
||||
|---|---|---|
|
||||
| `poll` | no | no |
|
||||
| `message_reaction_count` | no | no |
|
||||
| `deleted_business_messages` | no | no |
|
||||
| `unknown` | no | no |
|
||||
| Update type | `ctx.Msg` | `ctx.From` / `ctx.FromID` |
|
||||
|-----------------------------|-----------|---------------------------|
|
||||
| `poll` | no | no |
|
||||
| `message_reaction_count` | no | no |
|
||||
| `deleted_business_messages` | no | no |
|
||||
| `unknown` | no | no |
|
||||
|
||||
## What is already stable enough to rely on
|
||||
|
||||
|
||||
+15
-15
@@ -2,11 +2,11 @@
|
||||
|
||||
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:
|
||||
- текущий публичный API использует историческое написание `WebHook` в идентификаторах вроде `RunWebHookWithContext(...)`, `RunWebHook(...)` и `BotWebHookOpts`;
|
||||
- в тексте страницы используется обычное слово "webhook", но в примерах остаются реальные имена Go API.
|
||||
- публичный API использует идиоматичное написание `Webhook` в идентификаторах вроде `RunWebhookWithContext(...)`, `RunWebhook(...)` и `BotWebhookOpts`;
|
||||
- в примерах остаются реальные имена Go API.
|
||||
|
||||
## Когда использовать webhook runtime
|
||||
|
||||
@@ -25,11 +25,11 @@ English version: [[Webhook-Runtime]]
|
||||
## Точки входа
|
||||
|
||||
Главные bot-level точки входа такие:
|
||||
- `RunWebHookWithContext(ctx, opts, tlsFiles...)`
|
||||
- `RunWebHook(opts, tlsFiles...)`
|
||||
- `NewBotWebHookOpts()`
|
||||
- `RunWebhookWithContext(ctx, opts, tlsFiles...)`
|
||||
- `RunWebhook(opts, tlsFiles...)`
|
||||
- `NewBotWebhookOpts()`
|
||||
|
||||
`RunWebHook(...)` — это просто короткая форма для `RunWebHookWithContext(context.Background(), ...)`.
|
||||
`RunWebhook(...)` — это просто короткая форма для `RunWebhookWithContext(context.Background(), ...)`.
|
||||
|
||||
Обычный шаблон выглядит так:
|
||||
|
||||
@@ -46,20 +46,20 @@ defer bot.Close()
|
||||
bot.SetAppData(app)
|
||||
bot.AddPlugins(plugin)
|
||||
|
||||
webhookOpts := laniakea.NewBotWebHookOpts().
|
||||
webhookOpts := laniakea.NewBotWebhookOpts().
|
||||
SetURL("https://bot.example.com").
|
||||
SetPath("/telegram").
|
||||
SetLocalPort(8080).
|
||||
SetSecretToken("shared-secret")
|
||||
|
||||
if err := bot.RunWebHookWithContext(ctx, webhookOpts); err != nil {
|
||||
if err := bot.RunWebhookWithContext(ctx, webhookOpts); err != nil {
|
||||
return err
|
||||
}
|
||||
```
|
||||
|
||||
## Что именно берет на себя bot-level runtime
|
||||
|
||||
`RunWebHookWithContext(...)` — это не просто обертка над Telegram `setWebhook`.
|
||||
`RunWebhookWithContext(...)` — это не просто обертка над Telegram `setWebhook`.
|
||||
|
||||
Он:
|
||||
- валидирует bot-level условия старта, например prefixes и наличие зарегистрированных plugins;
|
||||
@@ -86,7 +86,7 @@ Webhook runtime использует те же основные гарантии
|
||||
|
||||
## Основные webhook options
|
||||
|
||||
`BotWebHookOpts` управляет и регистрацией webhook у Telegram, и локальным HTTP server.
|
||||
`BotWebhookOpts` управляет и регистрацией webhook у Telegram, и локальным HTTP server.
|
||||
|
||||
Поля, которые важны в первую очередь:
|
||||
|
||||
@@ -167,12 +167,12 @@ Webhook runtime использует те же основные гарантии
|
||||
|
||||
## Поведение HTTP и TLS
|
||||
|
||||
По умолчанию `RunWebHookWithContext(...)` поднимает обычный HTTP server на `LocalPort`.
|
||||
По умолчанию `RunWebhookWithContext(...)` поднимает обычный HTTP server на `LocalPort`.
|
||||
|
||||
Если передать два TLS-файла, локально стартует HTTPS.
|
||||
|
||||
Важно:
|
||||
- текущий публичный API ожидает существующий порядок аргументов `key, cert` при вызове `RunWebHookWithContext(...)`;
|
||||
- текущий публичный API ожидает существующий порядок аргументов `key, cert` при вызове `RunWebhookWithContext(...)`;
|
||||
- это отличается от более привычной ментальной модели `cert, key`, которую многие Go-разработчики ожидают от `ListenAndServeTLS`.
|
||||
|
||||
Поэтому в реальном setup лучше писать этот вызов максимально явно.
|
||||
@@ -202,7 +202,7 @@ Webhook runtime использует те же основные гарантии
|
||||
- `URL` — это то, что видит Telegram;
|
||||
- `Path` и `LocalPort` — это то, что реально обслуживает твой бот;
|
||||
- в production эти значения часто относятся к разным слоям инфраструктуры.
|
||||
- если ты переводишь работающий deployment с webhook-режима на polling, сначала удали webhook через `CloseWebHook()` или `tgapi.DeleteWebhook(...)`; Telegram не прекращает webhook-доставку автоматически.
|
||||
- если ты переводишь работающий deployment с webhook-режима на polling, сначала удали webhook через `CloseWebhook()` или `tgapi.DeleteWebhook(...)`; Telegram не прекращает webhook-доставку автоматически.
|
||||
|
||||
## Связь с webhook methods в `tgapi`
|
||||
|
||||
@@ -221,7 +221,7 @@ Webhook runtime использует те же основные гарантии
|
||||
когда тебе нужна собственная инфраструктура вокруг webhook path и ты не хочешь, чтобы сам бот владел HTTP server.
|
||||
|
||||
То есть:
|
||||
- `RunWebHookWithContext(...)` — это framework runtime API;
|
||||
- `RunWebhookWithContext(...)` — это framework runtime API;
|
||||
- webhook methods из `tgapi` — это низкоуровневые transport primitives.
|
||||
|
||||
## Частые ошибки
|
||||
|
||||
+15
-15
@@ -2,11 +2,11 @@
|
||||
|
||||
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:
|
||||
- the current public API uses the historical `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.
|
||||
- the public API uses idiomatic `Webhook` spelling in identifiers such as `RunWebhookWithContext(...)`, `RunWebhook(...)`, and `BotWebhookOpts`;
|
||||
- examples keep the actual Go API names.
|
||||
|
||||
## When to use webhook runtime
|
||||
|
||||
@@ -25,11 +25,11 @@ Use polling when:
|
||||
## Entry points
|
||||
|
||||
The main bot-level entry points are:
|
||||
- `RunWebHookWithContext(ctx, opts, tlsFiles...)`
|
||||
- `RunWebHook(opts, tlsFiles...)`
|
||||
- `NewBotWebHookOpts()`
|
||||
- `RunWebhookWithContext(ctx, opts, tlsFiles...)`
|
||||
- `RunWebhook(opts, tlsFiles...)`
|
||||
- `NewBotWebhookOpts()`
|
||||
|
||||
`RunWebHook(...)` is only a shorthand for `RunWebHookWithContext(context.Background(), ...)`.
|
||||
`RunWebhook(...)` is only a shorthand for `RunWebhookWithContext(context.Background(), ...)`.
|
||||
|
||||
The usual pattern looks like:
|
||||
|
||||
@@ -46,20 +46,20 @@ defer bot.Close()
|
||||
bot.SetAppData(app)
|
||||
bot.AddPlugins(plugin)
|
||||
|
||||
webhookOpts := laniakea.NewBotWebHookOpts().
|
||||
webhookOpts := laniakea.NewBotWebhookOpts().
|
||||
SetURL("https://bot.example.com").
|
||||
SetPath("/telegram").
|
||||
SetLocalPort(8080).
|
||||
SetSecretToken("shared-secret")
|
||||
|
||||
if err := bot.RunWebHookWithContext(ctx, webhookOpts); err != nil {
|
||||
if err := bot.RunWebhookWithContext(ctx, webhookOpts); err != nil {
|
||||
return err
|
||||
}
|
||||
```
|
||||
|
||||
## 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:
|
||||
- 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
|
||||
|
||||
`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:
|
||||
|
||||
@@ -167,12 +167,12 @@ Use this only when you specifically need Telegram's `ip_address` webhook option.
|
||||
|
||||
## 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.
|
||||
|
||||
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`.
|
||||
|
||||
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;
|
||||
- `Path` and `LocalPort` are what your bot actually serves;
|
||||
- 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
|
||||
|
||||
@@ -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.
|
||||
|
||||
In other words:
|
||||
- `RunWebHookWithContext(...)` is the framework runtime API;
|
||||
- `RunWebhookWithContext(...)` is the framework runtime API;
|
||||
- `tgapi` webhook methods are the lower-level transport primitives.
|
||||
|
||||
## Common mistakes
|
||||
|
||||
+1
-1
@@ -82,7 +82,7 @@ Use the context-aware variants when cancellation, deadlines, or graceful shutdow
|
||||
Useful options include:
|
||||
- `SetHTTPClient(...)`
|
||||
- `UseTestServer(...)`
|
||||
- `SetAPIUrl(...)`
|
||||
- `SetAPIURL(...)`
|
||||
- `SetLimiter(...)`
|
||||
- `SetLimiterDrop(...)`
|
||||
|
||||
|
||||
Reference in New Issue
Block a user