REPOSITORY / ScuroNeko/Laniakea

Wiki

KNOWLEDGE REPOSITORY

(doc): merge conflict resolution + restore v1.0.0 backlog checklist

- accept remote (beee616) for all API rename conflicts
- restore [1.0.0] pre-release checklist (M1-M11, minor items, tests) to both Framework-Backlog pages
2026-05-20 13:28:29 +03:00
38 changed files with 471 additions and 350 deletions
+3 -3
@@ -43,7 +43,7 @@ The normal pattern is to finish all structural configuration before starting the
- `SetDraftProvider(...)`, `SetSessionStore(...)`, and `SetSceneScopePriority(...)` replace runtime helpers.
- `SetErrorTemplate(...)` adjusts centralized user-facing error text.
For an overview of handlers and plugins, see [[Commands-and-Plugins]]. For context helpers available inside handlers, see [[MsgContext]].
For an overview of handlers and plugins, see [[Commands-and-Plugins]]. For context helpers available inside handlers, see [[MessageContext]].
## `AddPlugins(...)` is a configuration commit point
@@ -97,10 +97,10 @@ if err != nil {
defer bot.Close()
plugin := laniakea.NewPlugin[laniakea.NoData]("main")
plugin.Command("ping", func(ctx *laniakea.MsgContext, db laniakea.NoData) error {
plugin.NewCommand(func(ctx *laniakea.MessageContext, db laniakea.NoData) error {
ctx.Answer("pong")
return nil
})
}, "ping")
bot.AddPlugins(plugin)
+7 -7
@@ -47,11 +47,11 @@ plugin := laniakea.NewPlugin[laniakea.NoData]("admin")
Сигнатура обработчика команды такая:
```go
func(ctx *laniakea.MsgContext, db T) error
func(ctx *laniakea.MessageContext, db T) error
```
Где:
- `ctx` — текущий `MsgContext`;
- `ctx` — текущий `MessageContext`;
- `db` — значение generic-параметра `T`, которое ты передал в `Bot`.
Возвращай:
@@ -61,7 +61,7 @@ func(ctx *laniakea.MsgContext, db T) error
Пример:
```go
func start(ctx *laniakea.MsgContext, db *App) error {
func start(ctx *laniakea.MessageContext, db *App) error {
ctx.Answer("Welcome")
return nil
}
@@ -125,7 +125,7 @@ plugin.Command(
Пример:
```go
func confirmDelete(ctx *laniakea.MsgContext, db *App) error {
func confirmDelete(ctx *laniakea.MessageContext, db *App) error {
ctx.EditCallback("Deleted", nil)
return nil
}
@@ -147,7 +147,7 @@ plugin.Payload("delete.confirm", confirmDelete)
Пример:
```go
plugin.AddUpdateHandler(tgapi.UpdateTypeInlineQuery, func(ctx *laniakea.MsgContext, db *App) error {
plugin.AddUpdateHandler(tgapi.UpdateTypeInlineQuery, func(ctx *laniakea.MessageContext, db *App) error {
return nil
})
```
@@ -170,7 +170,7 @@ plugin.AddUpdateHandler(tgapi.UpdateTypeInlineQuery, func(ctx *laniakea.MsgConte
Для текстовой команды поток примерно такой:
1. Приходит Telegram update.
2. `Bot` готовит `MsgContext`.
2. `Bot` готовит `MessageContext`.
3. Выполняется middleware бота.
4. Находится подходящий плагин.
5. Выполняется middleware плагина.
@@ -258,5 +258,5 @@ return nil
- [[Getting-Started-RU]]
- [[Bot-Lifecycle-RU]]
- [[Commands-and-Plugins]]
- [[MsgContext]]
- [[MessageContext]]
- [[Inline-Keyboards-and-Payloads]]
+25 -25
@@ -49,7 +49,7 @@ That keeps command registration and middleware ownership clear.
The command handler signature is:
```go
func(ctx *laniakea.MsgContext, db T) error
func(ctx *laniakea.MessageContext, db T) error
```
Where:
@@ -63,7 +63,7 @@ Return:
Example:
```go
func start(ctx *laniakea.MsgContext, db *App) error {
func start(ctx *laniakea.MessageContext, db *App) error {
ctx.Answer("Welcome")
return nil
}
@@ -71,11 +71,11 @@ func start(ctx *laniakea.MsgContext, db *App) error {
## Registering commands
Create and register a command with `Plugin.Command(...)`:
Create a command with `NewCommand(...)` and add it to a plugin:
```go
plugin := laniakea.NewPlugin[*App]("main")
plugin.Command("start", start)
plugin.AddCommand(plugin.NewCommand(start, "start"))
```
The command name:
@@ -89,7 +89,7 @@ So:
## The easiest command example
```go
func echo(ctx *laniakea.MsgContext, db laniakea.NoData) error {
func echo(ctx *laniakea.MessageContext, db laniakea.NoData) error {
if ctx.Text == "" {
ctx.Answer("Usage: /echo <text>")
return nil
@@ -99,7 +99,7 @@ func echo(ctx *laniakea.MsgContext, db laniakea.NoData) error {
return nil
}
plugin.Command("echo", echo)
plugin.AddCommand(plugin.NewCommand(echo, "echo"))
```
For `/echo hello world`:
@@ -113,12 +113,12 @@ You can declare command arguments using `CommandArg`.
Example:
```go
plugin.Command(
"ban",
banUser,
plugin.AddCommand(
plugin.NewCommand(banUser, "ban",
laniakea.NewCommandArg("user_id").
SetValueType(laniakea.CommandValueInt).
SetValueType(laniakea.CommandValueIntType).
SetRequired(),
),
)
```
@@ -133,15 +133,15 @@ 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 `Plugin.Payload(...)` or `AddPayload(...)`:
Register them with `NewPayload(...)` or `AddPayload(...)`:
```go
func confirmDelete(ctx *laniakea.MsgContext, db *App) error {
func confirmDelete(ctx *laniakea.MessageContext, db *App) error {
ctx.EditCallback("Deleted", nil)
return nil
}
plugin.Payload("delete.confirm", confirmDelete)
plugin.AddPayload(plugin.NewPayload(confirmDelete, "delete.confirm"))
```
Payload handlers:
@@ -158,7 +158,7 @@ Update handlers are for Telegram updates outside the normal command/payload flow
Register them with `AddUpdateHandler(...)`:
```go
plugin.AddUpdateHandler(tgapi.UpdateTypeInlineQuery, func(ctx *laniakea.MsgContext, db *App) error {
plugin.AddUpdateHandler(tgapi.UpdateTypeInlineQuery, func(ctx *laniakea.MessageContext, db *App) error {
// handle inline query here
return nil
})
@@ -183,7 +183,7 @@ stay on the command/payload flow and are not meant to be registered through `Add
For text commands:
1. Telegram update arrives
2. bot prepares `MsgContext`
2. bot prepares `MessageContext`
3. bot middleware runs
4. matching plugin is found
5. plugin middleware runs
@@ -213,7 +213,7 @@ Use this for logic shared by most handlers in the plugin.
Added with:
```go
plugin.Command("name", handler).Use(middleware)
plugin.NewCommand(handler, "name").Use(middleware)
```
Use this when only one command or payload needs the check.
@@ -235,7 +235,7 @@ Avoid one giant plugin for the entire bot unless the bot is very small.
```go
admin := laniakea.NewPlugin[*App]("admin")
admin.AddMiddleware(laniakea.NewMiddleware("admin-only", func(ctx *laniakea.MsgContext, app *App) bool {
admin.AddMiddleware(laniakea.NewMiddleware("admin-only", func(ctx *laniakea.MessageContext, app *App) bool {
if !app.IsAdmin(ctx.FromID) {
ctx.Answer("Access denied")
return false
@@ -243,20 +243,20 @@ admin.AddMiddleware(laniakea.NewMiddleware("admin-only", func(ctx *laniakea.MsgC
return true
}))
admin.Command("ban", func(ctx *laniakea.MsgContext, app *App) error {
admin.AddCommand(admin.NewCommand(func(ctx *laniakea.MessageContext, app *App) error {
ctx.Answer("Banned")
return nil
})
}, "ban"))
```
### Example: payload handler for inline keyboard callback
```go
plugin.Payload("approve", func(ctx *laniakea.MsgContext, app *App) error {
ctx.AnswerCallbackText("Accepted")
plugin.AddPayload(plugin.NewPayload(func(ctx *laniakea.MessageContext, app *App) error {
ctx.AnswerCbQueryText("Accepted")
ctx.EditCallback("Done", nil)
return nil
})
}, "approve"))
```
## Common mistakes
@@ -266,13 +266,13 @@ plugin.Payload("approve", func(ctx *laniakea.MsgContext, app *App) error {
Wrong:
```go
plugin.Command("/start", start)
plugin.NewCommand(start, "/start")
```
Right:
```go
plugin.Command("start", start)
plugin.NewCommand(start, "start")
```
### Treating payloads like commands
@@ -304,6 +304,6 @@ Use:
- command middleware for narrow, local checks
## Where to go next
- Read [[MsgContext]] next to understand what handlers can do once they are triggered.
- Read [[MessageContext]] next to understand what handlers can do once they are triggered.
- Read [[Inline-Keyboards-and-Payloads]] if you are starting to use buttons and callback data.
- Read [[Middleware]] for execution-order and async details.
+1 -1
@@ -75,6 +75,6 @@ draft.Flush()
## Что читать дальше
- [[MsgContext-RU]]
- [[MessageContext-RU]]
- [[Bot-Lifecycle-RU]]
- [[Drafts]]
+4 -4
@@ -23,9 +23,9 @@ The draft system has two layers:
The provider is safe for concurrent use. Individual drafts are intended for single-goroutine use unless you add your own synchronization.
## The easiest entry point: `MsgContext.NewDraft()`
## The easiest entry point: `MessageContext.NewDraft()`
Inside a handler, the usual entry point is `MsgContext.NewDraft()` or `MsgContext.NewDraftMarkdown()`.
Inside a handler, the usual entry point is `MessageContext.NewDraft()` or `MessageContext.NewDraftMarkdown()`.
Those helpers:
- create a draft from the bot's configured `DraftProvider`;
@@ -35,7 +35,7 @@ Those helpers:
Typical usage:
```go
func report(ctx *laniakea.MsgContext, db *App) error {
func report(ctx *laniakea.MessageContext, db *App) error {
draft := ctx.NewDraft()
if draft == nil {
return nil
@@ -174,5 +174,5 @@ Prefer direct reply helpers such as `Answer(...)` or `AnswerLong(...)` when:
## Related pages
- [[MsgContext]] for handler-scoped reply and draft helpers
- [[MessageContext]] for handler-scoped reply and draft helpers
- [[Bot-Lifecycle]] for draft-provider attachment through the bot
+1 -1
@@ -25,7 +25,7 @@ English version: [[Error-Handling]]
## Как это выглядит
```go
func ping(ctx *laniakea.MsgContext, db *App) error {
func ping(ctx *laniakea.MessageContext, db *App) error {
ctx.Answer("pong")
return nil
}
+12 -12
@@ -16,7 +16,7 @@ That applies to:
The basic pattern is:
```go
func ping(ctx *laniakea.MsgContext, db *App) error {
func ping(ctx *laniakea.MessageContext, db *App) error {
ctx.Answer("pong")
return nil
}
@@ -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:
- `AnswerCallback()`
- `AnswerCallbackText(...)`
- `AnswerCallbackAlert(...)`
- `AnswerCallbackURL(...)`
- `AnswerCbQuery()`
- `AnswerCbQueryText(...)`
- `AnswerCbQueryAlert(...)`
- `AnswerCbQueryUrl(...)`
and return `nil`.
@@ -183,38 +183,38 @@ Related page:
### Centralized command failure
```go
plugin.Command("report", func(ctx *laniakea.MsgContext, db *App) error {
plugin.NewCommand(func(ctx *laniakea.MessageContext, 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.Command("admin", func(ctx *laniakea.MsgContext, db *App) error {
plugin.NewCommand(func(ctx *laniakea.MessageContext, 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.Payload("start", func(ctx *laniakea.MsgContext, db *App) error {
plugin.NewPayload(func(ctx *laniakea.MessageContext, db *App) error {
if !ready {
ctx.AnswerCallbackAlert("This action is not available yet")
ctx.AnswerCbQueryAlert("This action is not available yet")
return nil
}
return nil
})
}, "start")
```
## Recommendations
+56 -17
@@ -4,6 +4,45 @@
## Done
### [1.0.0]
#### Major — закрыто до тега 1.0.0
- [X] **M1. `BotPayloadType*` были `var`, должны быть `const`.**
- [X] **M2. Асимметрия именования методов `Observer`**`OnReceiveUpdate``OnUpdateReceived`; `OnHandledUpdate``OnUpdateHandled`.
- [X] **M3. Uploader возвращал ad-hoc строку ошибки вместо `*ResponseError`.**
- [X] **M4. `BotOptsFileJSON` не сохранял `PollTimeout`** — поле терялось при round-trip.
- [X] **M5. Устаревший godoc `Bot.Updates`** — утверждал "30-second timeout" и "empty slice if none".
- [X] **M6. Противоречивый godoc `NewRandomDraftProvider`** — говорил "cryptographically secure", но использовал `math/rand/v2`.
- [X] **M7. Godoc `Draft.Delete` говорил "internal method"** — метод экспортирован.
- [X] **M8. Русские комментарии в production-коде**`msg_handler.go`, `tgapi/uploader_api.go`.
- [X] **M9. Godoc `MessageContext.Error` ссылался на неэкспортированный хелпер.**
- [X] **M10. `Scene` и `SceneSession` смешивали экспортированные поля с setter-ами**`Scene.PluginName` закрыт, `SceneSession.Data` закрыт.
- [X] **M11. Constant-time compare для webhook secret** — использован `subtle.ConstantTimeCompare`.
#### Minor — закрыто до тега 1.0.0
- [X] Убраны godoc-комментарии с неэкспортированных функций.
- [X] Godoc `Plugin.AddCommand` ссылался на `.command`.
- [X] Builder раннеров: `Onetime`/`Timeout``Every`/`Async`; `Once()` удалён.
- [X] Опечатка в webhook: "must between" → "must be between".
- [X] Inline `errors.New(...)` в webhook → `Err*` sentinel-ы.
- [X] Добавлен godoc для `tgapi.UpdateTypeManagedBot`, `Bot.GetAPI`, `Bot.GetUploader`, `InlineKeyboard.GetMaxRow`.
- [X] Godoc `Bot.L10n` исправлен — возвращает ключ, а не пустую строку.
- [X] Panic в `Bot.handle` теперь эмитирует `ErrorEvent` через observer.
- [X] Выравнена логика plugin-logger в `handleCallback` и `handleMessage`.
- [X] Godoc `SetCallbackData` уточняет поведение zero `BotPayloadType`.
- [X] `commands.go` пустой `case CommandValueAny:` объединён с `default`.
- [X] `Bot.SetDebug` не вызывает `configMutable` — задокументировано в godoc.
#### Тесты, добавленные после правок
- [X] Round-trip `BotOptsFileJSON` для `PollTimeout`.
- [X] Uploader 4xx/429 возвращает `*tgapi.ResponseError`.
- [X] Panic в `Bot.handle` → observer получает `ErrorEvent`.
- [X] Webhook `/status` с неверным `SecretToken` возвращает 404, smoke-тест constant-time compare.
- [X] Table-driven тесты `parseCommand` для `/cmd@botname`.
### [1.0.0-rc.14] Модель выполнения webhook
Текущее состояние:
@@ -65,7 +104,7 @@
Текущее состояние:
- Во фреймворке теперь есть `Policy[T]` как явная переиспользуемая модель правила доступа, работающая поверх нормализованного `MsgContext` и общих данных приложения.
- Во фреймворке теперь есть `Policy[T]` как явная переиспользуемая модель правила доступа, работающая поверх нормализованного `MessageContext` и общих данных приложения.
- Политики интегрируются в уже существующую модель выполнения через `RequirePolicy(...)`, поэтому авторизация остаётся на middleware-пути и не создаёт второй pipeline маршрутизации.
- У бота и плагинов появились явные helpers для регистрации политик на уровне конфигурации.
@@ -82,7 +121,7 @@
- `Bot.UsePolicy(...)` и `Plugin.UsePolicy(...)` для удобной регистрации.
- Встроенные Telegram-aware helpers: `RequirePrivateChat(...)`, `RequireGroupChat(...)`, `RequireSupergroupChat(...)`, `RequireChatAdmin(...)`, `RequireChatCreator(...)`, `RequireBotAdmin(...)` и `RequireCallbackFromUser(...)`.
- Комбинаторы `AllPolicies(...)`, `AnyPolicy(...)` и `NotPolicy(...)`.
- Расширенная нормализация `MsgContext` для `Chat` и `ChatID`, а также регрессионные тесты на поведение политик и нормализованного контекста.
- Расширенная нормализация `MessageContext` для `Chat` и `ChatID`, а также регрессионные тесты на поведение политик и нормализованного контекста.
Практическая цель:
@@ -106,7 +145,7 @@
- `AsUserError(...)` и `AsInternalError(...)` для явной классификации возвращаемых ошибок.
- `IsUserError(...)` и `IsInternalError(...)` для проверки этой классификации на стороне фреймворка.
- Обновлённое поведение `MsgContext.Error(...)`: все ошибки по-прежнему логируются, но для внутренних ошибок автоматический ответ пользователю подавляется.
- Обновлённое поведение `MessageContext.Error(...)`: все ошибки по-прежнему логируются, но для внутренних ошибок автоматический ответ пользователю подавляется.
- Регрессионные тесты для message и callback потоков.
Практическая цель:
@@ -141,30 +180,30 @@
Текущее состояние:
- Нормализация обновлений уже существовала, но теперь она описана и протестирована как явный контракт уровня фреймворка.
- Категории маршрутизации и гарантии заполнения `MsgContext` теперь рассматриваются как полноценная часть публичной модели.
- Категории маршрутизации и гарантии заполнения `MessageContext` теперь рассматриваются как полноценная часть публичной модели.
Почему это важно:
- Код обработчиков должен понимать, на какие поля `MsgContext` можно безопасно опираться в каждом потоке обновлений.
- Код обработчиков должен понимать, на какие поля `MessageContext` можно безопасно опираться в каждом потоке обновлений.
- Без явного контракта обработка обновлений остаётся понятной только через чтение реализации.
Что теперь есть:
- Задокументированная модель маршрутизации для command flow, payload flow и generic update handlers.
- Явные комментарии на полях `MsgContext`, описывающие гарантии для update-backed, callback-backed и message-backed контекстов.
- Явные комментарии на полях `MessageContext`, описывающие гарантии для update-backed, callback-backed и message-backed контекстов.
- Table-driven регрессионные тесты для нормализованного update contract, включая callback target semantics и non-command update flows.
Практическая цель:
- Сделать маршрутизацию обновлений и гарантии `MsgContext` достаточно явными, чтобы на них можно было опираться как на стабильный контракт `1.0`.
- Сделать маршрутизацию обновлений и гарантии `MessageContext` достаточно явными, чтобы на них можно было опираться как на стабильный контракт `1.0`.
### [1.0.0-rc.12] Conversation / Scene Model
Текущее состояние:
- Фреймворк хорошо обрабатывает одно обновление через команды, данные callback, middleware и обработчики обновлений.
- В нём уже есть полезные низкоуровневые строительные блоки: `MsgContext`, черновики, маршрутизация данных callback, плагины и обработчики обновлений.
- Теперь в нём уже есть реализованная начальная модель сцен для долгоживущих интерактивных сценариев: сцены можно регистрировать в плагинах, запускать через `MsgContext`, сохранять через `SessionStore` и маршрутизировать раньше обычной обработки команд.
- В нём уже есть полезные низкоуровневые строительные блоки: `MessageContext`, черновики, маршрутизация данных callback, плагины и обработчики обновлений.
- Теперь в нём уже есть реализованная начальная модель сцен для долгоживущих интерактивных сценариев: сцены можно регистрировать в плагинах, запускать через `MessageContext`, сохранять через `SessionStore` и маршрутизировать раньше обычной обработки команд.
Почему это важно:
@@ -176,7 +215,7 @@
- Маршрутизация активной сцены раньше обычной маршрутизации команд.
- Области действия сессии на пользователя, чат и пару пользователь-чат.
- Явный вход и выход через `MsgContext`.
- Явный вход и выход через `MessageContext`.
- Обработчики шагов, локальные команды сцены и `OnMessage(...)`.
- Встроенное in-memory-хранилище по умолчанию и интерфейс `SessionStore` для собственного постоянного хранения.
@@ -189,8 +228,8 @@
Текущее направление API:
- `Scene`, `SceneContext`, `SceneSession` и `SessionStore`.
- `Plugin.Scene(...)` и `Plugin.AddScene(...)`.
- `MsgContext.EnterScene(...)`, `EnterSceneStep(...)` и `ExitScene(...)`.
- `Plugin.NewScene(...)` и `Plugin.AddScene(...)`.
- `MessageContext.EnterScene(...)`, `EnterSceneStep(...)` и `ExitScene(...)`.
- `SceneContext.Stay()`, `Next(...)`, `Exit()`, `Pass()`, `BindData(...)` и `SaveData(...)`.
- Состояние на пользователя или чат с хранением в `SessionStore` и чистым интерфейсом для собственного постоянного хранения.
@@ -198,7 +237,7 @@
- Это должно быть опциональным и расширяющим текущую модель.
- Это не должно заменять плагины, команды или обработчики как обычные точки входа во фреймворк.
- Это должно работать поверх существующих middleware и `MsgContext`, а не вводить вторую несовместимую модель выполнения.
- Это должно работать поверх существующих middleware и `MessageContext`, а не вводить вторую несовместимую модель выполнения.
Практическая цель:
@@ -240,7 +279,7 @@ type BanInput struct {
Reason string
}
func ban(ctx *laniakea.MsgContext, db *App) error {
func ban(ctx *laniakea.MessageContext, db *App) error {
var input BanInput
if err := ctx.BindArgs(&input); err != nil {
return err
@@ -281,14 +320,14 @@ func ban(ctx *laniakea.MsgContext, db *App) error {
Возможное направление API:
- Предпочесть non-breaking подход и выдавать context через `MsgContext`, например `ctx.Context()`.
- Предпочесть non-breaking подход и выдавать context через `MessageContext`, например `ctx.Context()`.
- Строить context из жизненного цикла обработки обновления, чтобы он оставался полезным во время корректного завершения.
- Сделать естественной передачу этого context в методы базы данных, HTTP-клиенты и `tgapi.WithContext(...)`.
Почему это, скорее всего, не должно быть изменением сигнатуры:
- Изменение сигнатур обработчиков на прямой `context.Context` было бы публичным ломающим изменением.
- Аксессор на `MsgContext` сохраняет совместимость и при этом даёт обработчикам идиоматичный Go-путь для отмены выполнения.
- Аксессор на `MessageContext` сохраняет совместимость и при этом даёт обработчикам идиоматичный Go-путь для отмены выполнения.
Практическая цель:
@@ -297,7 +336,7 @@ func ban(ctx *laniakea.MsgContext, db *App) error {
Связанные страницы:
- [[Scenes]]
- [[MsgContext]]
- [[MessageContext]]
- [[Bot-Lifecycle]]
- [[Migration]]
+42 -47
@@ -6,7 +6,7 @@ This page tracks framework-level backlog items that are about missing concepts i
### [1.0.0]
### Major — close before 1.0.0 tag
#### Major — closed 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.
@@ -15,39 +15,34 @@ This page tracks framework-level backlog items that are about missing concepts i
- [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`.
- [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`. Rewrite to describe the centralized handler error path and `IsUserError` gating.
- [X] **M10. `Scene` and `SceneSession` mix exported fields with setters**`Scene.PluginName` unexported; `SceneSession.Data` unexported, use accessor helpers.
- [X] **M11. Constant-time compare for webhook secret**`bot_webhook.go`. Use `subtle.ConstantTimeCompare`.
### Minor — can slip to 1.0.x
#### Minor — closed before 1.0.0 tag
- [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.
- [X] Strip `// Internal helper …` godoc from unexported funcs.
- [X] `Plugin.AddCommand` godoc references unexported field `.command`.
- [X] `Runner` builder: `Onetime`/`Timeout``Every`/`Async`; `Once()` removed.
- [X] Typo in webhook error string: "MaxConnections must between 1 and 100" → "must be between".
- [X] `RunWebhookWithContext` inline `errors.New(...)` `Err*` sentinels.
- [X] `tgapi.UpdateTypeManagedBot` missing godoc.
- [X] `Bot.GetAPI`, `Bot.GetUploader`, `InlineKeyboard.GetMaxRow` missing godoc.
- [X] `Bot.L10n` godoc says "Returns empty string if translation not found"; actually returns the key.
- [X] `Bot.handle` panic recovery only logs — emit `ErrorEvent` so observers see panics.
- [X] `handleCallback` vs `handleMessage` plugin-logger assignment asymmetry — aligned.
- [X] `SetCallbackData` godoc: zero `BotPayloadType` behavior documented explicitly.
- [X] `commands.go` empty `case CommandValueAny:` merged with `default`.
- [X] `Bot.SetDebug` does not call `configMutable` noted in godoc.
### Tests to add after the fixes
#### Tests added 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.
- [X] `BotOptsFileJSON` round-trip for `PollTimeout`.
- [X] Uploader 4xx/429 surfaces `*tgapi.ResponseError`.
- [X] `Bot.handle` panic → observer receives `ErrorEvent`.
- [X] Webhook `/status` with wrong `SecretToken` returns 404, constant-time-compare smoke test.
- [X] Table-driven `parseCommand` cases for `/cmd@botname` stripping.
### [1.0.0-rc.14] Webhook runtime model
@@ -110,7 +105,7 @@ Practical target:
Current state:
- The framework now exposes `Policy[T]` as a first-class reusable authorization rule that runs against the normalized `MsgContext` and injected app data.
- The framework now exposes `Policy[T]` as a first-class reusable authorization rule that runs against the normalized `MessageContext` and injected app data.
- Policies integrate with the existing execution model through `RequirePolicy(...)`, so authorization stays on the middleware path instead of introducing a second routing pipeline.
- Bot-level and plugin-level registration helpers now make policy usage explicit in configuration.
@@ -127,7 +122,7 @@ What is now present:
- `Bot.UsePolicy(...)` and `Plugin.UsePolicy(...)` for registration ergonomics.
- Built-in Telegram-aware helpers such as `RequirePrivateChat(...)`, `RequireGroupChat(...)`, `RequireSupergroupChat(...)`, `RequireChatAdmin(...)`, `RequireChatCreator(...)`, `RequireBotAdmin(...)`, and `RequireCallbackFromUser(...)`.
- Composition helpers `AllPolicies(...)`, `AnyPolicy(...)`, and `NotPolicy(...)`.
- Extended `MsgContext` normalization for `Chat` and `ChatID`, plus regression coverage for policy and normalization behavior.
- Extended `MessageContext` normalization for `Chat` and `ChatID`, plus regression coverage for policy and normalization behavior.
Practical target:
@@ -151,7 +146,7 @@ What is now present:
- `AsUserError(...)` and `AsInternalError(...)` to classify returned handler errors explicitly.
- `IsUserError(...)` and `IsInternalError(...)` for framework-side inspection.
- Updated centralized `MsgContext.Error(...)` behavior that still logs all errors but suppresses the automatic user reply for internal-only failures.
- Updated centralized `MessageContext.Error(...)` behavior that still logs all errors but suppresses the automatic user reply for internal-only failures.
- Regression coverage for both message and callback flows.
Practical target:
@@ -186,30 +181,30 @@ Practical target:
Current state:
- Update normalization already existed, but it is now described and tested as an explicit framework-level contract.
- Routing categories and `MsgContext` population guarantees are now treated as a first-class part of the public model.
- Routing categories and `MessageContext` population guarantees are now treated as a first-class part of the public model.
Why this matters:
- Handler code needs to know which `MsgContext` fields are safe to rely on for each update path.
- Handler code needs to know which `MessageContext` fields are safe to rely on for each update path.
- Without a formal contract, update handling remains understandable only by reading implementation details.
What is now present:
- A documented routing model for command flow, payload flow, and generic update handlers.
- Explicit `MsgContext` field comments for update-backed, callback-backed, and message-backed contexts.
- Explicit `MessageContext` field comments for update-backed, callback-backed, and message-backed contexts.
- Table-driven regression coverage for the normalized update contract, including callback target semantics and non-command update flows.
Practical target:
- Make update routing and `MsgContext` guarantees explicit enough to serve as a stable `1.0` public contract.
- Make update routing and `MessageContext` guarantees explicit enough to serve as a stable `1.0` public contract.
### [1.0.0-rc.12] Conversation / Scene Model
Current state:
- The framework is strong at handling a single update through commands, payloads, middleware, and update handlers.
- It already has useful lower-level building blocks such as `MsgContext`, drafts, payload routing, plugins, and update handlers.
- It now provides an implemented initial scene model for long-lived user interaction flows: scenes can be registered in plugins, entered through `MsgContext`, persisted through `SessionStore`, and routed before normal command handling.
- It already has useful lower-level building blocks such as `MessageContext`, drafts, payload routing, plugins, and update handlers.
- It now provides an implemented initial scene model for long-lived user interaction flows: scenes can be registered in plugins, entered through `MessageContext`, persisted through `SessionStore`, and routed before normal command handling.
Why this matters:
@@ -221,7 +216,7 @@ What is already present:
- Active-scene routing before normal command flow.
- Per-user, per-chat, and per-user-chat session scopes.
- Explicit scene entry and exit through `MsgContext`.
- Explicit scene entry and exit through `MessageContext`.
- Step handlers, scene-local commands, and `OnMessage(...)`.
- In-memory session storage by default, plus the `SessionStore` interface for custom persistence.
@@ -234,8 +229,8 @@ What is still missing or not yet settled:
Current API direction:
- `Scene`, `SceneContext`, `SceneSession`, and `SessionStore`.
- `Plugin.Scene(...)` and `Plugin.AddScene(...)`.
- `MsgContext.EnterScene(...)`, `EnterSceneStep(...)`, and `ExitScene(...)`.
- `Plugin.NewScene(...)` and `Plugin.AddScene(...)`.
- `MessageContext.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.
@@ -243,7 +238,7 @@ Important design constraints:
- This should be additive and optional.
- It should not replace plugins, commands, or handlers as the normal framework entry points.
- It should work with existing middleware and `MsgContext` instead of introducing a second incompatible execution model.
- It should work with existing middleware and `MessageContext` instead of introducing a second incompatible execution model.
Practical target:
@@ -285,7 +280,7 @@ type BanInput struct {
Reason string
}
func ban(ctx *laniakea.MsgContext, db *App) error {
func ban(ctx *laniakea.MessageContext, db *App) error {
var input BanInput
if err := ctx.BindArgs(&input); err != nil {
return err
@@ -326,14 +321,14 @@ What is missing:
Possible API direction:
- Prefer a non-breaking approach by exposing context through `MsgContext`, for example `ctx.Context()`.
- Prefer a non-breaking approach by exposing context through `MessageContext`, for example `ctx.Context()`.
- Build the context from the update-processing lifecycle so it is meaningful during graceful shutdown.
- Make it natural to pass that context into database methods, HTTP clients, and `tgapi.WithContext(...)` calls.
Why this should probably not be a signature change:
- Changing handler signatures to accept `context.Context` directly would be a public breaking change.
- A `MsgContext` accessor would preserve compatibility while still giving handlers an idiomatic Go cancellation path.
- A `MessageContext` accessor would preserve compatibility while still giving handlers an idiomatic Go cancellation path.
Practical target:
@@ -342,7 +337,7 @@ Practical target:
Related pages:
- [[Scenes]]
- [[MsgContext]]
- [[MessageContext]]
- [[Bot-Lifecycle]]
- [[Migration]]
+4 -4
@@ -33,7 +33,7 @@ import (
"git.scuroneko.dev/scuroneko/laniakea"
)
func ping(ctx *laniakea.MsgContext, db laniakea.NoData) error {
func ping(ctx *laniakea.MessageContext, db laniakea.NoData) error {
ctx.Answer("Pong")
return nil
}
@@ -111,7 +111,7 @@ bot.AddPlugins(plugin)
Сигнатура хендлера:
```go
func(ctx *laniakea.MsgContext, db T) error
func(ctx *laniakea.MessageContext, db T) error
```
То есть:
@@ -121,7 +121,7 @@ func(ctx *laniakea.MsgContext, db T) error
Пример:
```go
func profile(ctx *laniakea.MsgContext, db *App) error {
func profile(ctx *laniakea.MessageContext, db *App) error {
user, err := db.LoadUser(ctx.FromID)
if err != nil {
return err
@@ -199,4 +199,4 @@ defer bot.Close()
- [[Bot-Lifecycle-RU]]
- [[Getting-Started]]
- [[Commands-and-Plugins]]
- [[MsgContext]]
- [[MessageContext]]
+5 -5
@@ -34,7 +34,7 @@ import (
"git.scuroneko.dev/scuroneko/laniakea"
)
func ping(ctx *laniakea.MsgContext, db laniakea.NoData) error {
func ping(ctx *laniakea.MessageContext, db laniakea.NoData) error {
ctx.Answer("Pong")
return nil
}
@@ -113,7 +113,7 @@ See [[Commands-and-Plugins]] for the full model.
The command handler signature is:
```go
func(ctx *laniakea.MsgContext, db T) error
func(ctx *laniakea.MessageContext, db T) error
```
This means:
@@ -124,7 +124,7 @@ This means:
Example:
```go
func profile(ctx *laniakea.MsgContext, db *App) error {
func profile(ctx *laniakea.MessageContext, db *App) error {
user, err := db.LoadUser(ctx.FromID)
if err != nil {
return err
@@ -184,7 +184,7 @@ import (
type App struct{}
func echo(ctx *laniakea.MsgContext, app *App) error {
func echo(ctx *laniakea.MessageContext, app *App) error {
if ctx.Text == "" {
ctx.Answer("Send some text after the command.")
return nil
@@ -259,5 +259,5 @@ Prefer pointer types unless you have a strong reason not to.
## Where to go next
- Read [[Commands-and-Plugins]] next if you want to build the handler layer correctly.
- Read [[MsgContext]] next if you want to understand reply, edit, callback, and draft helpers.
- Read [[MessageContext]] next if you want to understand reply, edit, callback, and draft helpers.
- Read [[Bot-Lifecycle]] if you need shutdown, worker, or startup details.
+3 -3
@@ -9,7 +9,7 @@ English version: [[Home]]
- [[Getting-Started-RU]]
- [[Bot-Options-and-Configuration-RU]]
- [[Commands-and-Plugins-RU]]
- [[MsgContext-RU]]
- [[MessageContext-RU]]
## Выполнение и архитектура
@@ -50,7 +50,7 @@ English version: [[Home]]
- [[Getting-Started]]
- [[Bot-Options-and-Configuration]]
- [[Commands-and-Plugins]]
- [[MsgContext]]
- [[MessageContext]]
- [[Bot-Lifecycle]]
- [[Inline-Keyboards-and-Payloads]]
- [[tgapi-Overview]]
@@ -60,7 +60,7 @@ English version: [[Home]]
Рекомендуемый маршрут для русскоязычного пользователя:
1. Прочитать [[Getting-Started-RU]].
2. Прочитать [[Commands-and-Plugins-RU]] и [[MsgContext-RU]].
2. Прочитать [[Commands-and-Plugins-RU]] и [[MessageContext-RU]].
3. Перейти в страницы про выполнение и поведение во время работы, например [[Bot-Lifecycle-RU]] и [[Middleware-RU]].
4. При необходимости открыть специализированные страницы вроде [[Drafts-RU]], [[Rate-Limiting-RU]] или [[tgapi-Overview-RU]].
5. Для максимальной точности переходить в соответствующую англоязычную страницу.
+1 -1
@@ -15,7 +15,7 @@ Use this wiki as the structured companion to the README: start with setup, then
- [[Getting-Started]]
- [[Bot-Options-and-Configuration]]
- [[Commands-and-Plugins]]
- [[MsgContext]]
- [[MessageContext]]
## Bot Runtime
- [[Bot-Lifecycle]]
+1 -1
@@ -99,5 +99,5 @@ kb := ctx.NewInlineKeyboard(2).
## Что читать дальше
- [[Commands-and-Plugins-RU]]
- [[MsgContext-RU]]
- [[MessageContext-RU]]
- [[Inline-Keyboards-and-Payloads]]
+22 -22
@@ -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")
```
@@ -91,13 +91,13 @@ Inside the payload handler, those values are available through:
- `ctx.Args` for the decoded argument list;
- the payload command name used to choose the matched handler.
There is no separate `ctx.Payload` object in the current API. Payload handlers receive the same `MsgContext` structure used elsewhere, with `ctx.Args` populated from callback data.
There is no separate `ctx.Payload` object in the current API. Payload handlers receive the same `MessageContext` structure used elsewhere, with `ctx.Args` populated from callback data.
## JSON vs Base64 payloads
Laniakea supports two payload encodings:
- `BotPayloadJSON`
- `BotPayloadJson`
- `BotPayloadBase64`
JSON is easier to inspect in logs and tests.
@@ -126,13 +126,13 @@ The bot has a default callback encoding:
bot.SetPayloadType(laniakea.BotPayloadBase64)
```
That default is copied into `MsgContext`, so `ctx.NewInlineKeyboard(...)` starts with the bots current payload type.
That default is copied into `MessageContext`, so `ctx.NewInlineKeyboard(...)` starts with the bots current payload type.
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
`InlineKeyboardButtonBuilder` is the flexible path when you want button-specific styling or custom emoji icons.
`InlineKbButtonBuilder` is the flexible path when you want button-specific styling or custom emoji icons.
Example:
```go
button := laniakea.NewInlineKeyboardButton("Confirm").
button := laniakea.NewInlineKbButton("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(...)`
- `InlineKeyboardButtonBuilder.SetStyle(...)`
- `InlineKbButtonBuilder.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 Telegrams message length limit, use:
## Routing payloads to handlers
Payloads are registered on plugins with:
- `Plugin.Payload(...)`
- `Plugin.NewPayload(...)`
- `Plugin.AddPayload(...)`
Example:
```go
plugin.Payload("inspect", func(ctx *laniakea.MsgContext, db *App) error {
plugin.NewPayload(func(ctx *laniakea.MessageContext, db *App) error {
id := ctx.Args[0]
ctx.AnswerCallbackText("Handled " + id)
ctx.AnswerCbQueryText("Handled " + id)
return nil
})
}, "inspect")
```
When a callback arrives:
@@ -275,5 +275,5 @@ Related page:
## Related pages
- [[Commands-and-Plugins]] for payload handler registration and routing.
- [[MsgContext]] for reply, edit, and callback helpers.
- [[MessageContext]] for reply, edit, and callback helpers.
- [[tgapi-Overview]] for lower-level Telegram method access when keyboard helpers are not enough.
+5 -5
@@ -77,11 +77,11 @@ bot.SetL10n(l10n)
From then on:
- `bot.L10n(lang, key)` is available for manual lookups;
- `MsgContext.Translate(key)` becomes the ergonomic handler-level helper.
- `MessageContext.Translate(key)` becomes the ergonomic handler-level helper.
If `SetL10n(nil)` is called, the bot logs a warning and keeps the existing localization provider unchanged.
## `MsgContext.Translate`
## `MessageContext.Translate`
`ctx.Translate(key)` is the usual choice inside handlers.
@@ -93,7 +93,7 @@ It:
Example:
```go
func start(ctx *laniakea.MsgContext, db *App) error {
func start(ctx *laniakea.MessageContext, db *App) error {
ctx.Answer(ctx.Translate("greeting"))
return nil
}
@@ -111,7 +111,7 @@ Use `bot.L10n(lang, key)` when you need a translation outside handler flow or wh
Examples:
- preparing background-runner messages;
- rendering text for stored user preferences;
- translating outside `MsgContext`.
- translating outside `MessageContext`.
## Concurrency and mutation safety
@@ -150,5 +150,5 @@ l10n.
## Related pages
- [[Getting-Started]] for basic bot setup
- [[MsgContext]] for handler helpers
- [[MessageContext]] for handler helpers
- [[Recipes]] for localized handler examples
+2 -2
@@ -55,13 +55,13 @@ Ways plugin loggers are set:
- explicitly with `Plugin.SetLogger(...)` before `AddPlugins(...)`;
- implicitly by the bot, which creates a default logger if the plugin has none at registration time.
During handler execution, `MsgContext.Logger` is set to:
During handler execution, `MessageContext.Logger` is set to:
- the matched plugin logger when one exists;
- otherwise the bot logger.
That means handler-local logs naturally follow plugin boundaries when possible.
## `MsgContext.Logger`
## `MessageContext.Logger`
Inside handlers and middleware, the easiest logger to use is `ctx.Logger`.
+4 -4
@@ -45,7 +45,7 @@ Middleware позволяет запускать логику до обрабо
Сигнатура:
```go
func(ctx *laniakea.MsgContext, db T) bool
func(ctx *laniakea.MessageContext, db T) bool
```
Возвращает:
@@ -66,7 +66,7 @@ func(ctx *laniakea.MsgContext, db T) bool
- middleware идет в goroutine;
- выполнение цепочки продолжается сразу;
- возвращаемое значение `bool` игнорируется;
- middleware получает копию `MsgContext`.
- middleware получает копию `MessageContext`.
Поэтому async middleware подходит только для:
- телеметрии;
@@ -79,7 +79,7 @@ func(ctx *laniakea.MsgContext, db T) bool
- обязательной валидации;
- логики, которая должна переписать `ctx` и повлиять на обработчик.
## Почему async middleware получает копию `MsgContext`
## Почему async middleware получает копию `MessageContext`
Так библиотека избегает очевидных гонок данных между goroutine middleware и основной цепочкой обработки.
@@ -105,5 +105,5 @@ Middleware уровня плагина и команды сохраняют по
## Что читать дальше
- [[Commands-and-Plugins-RU]]
- [[MsgContext-RU]]
- [[MessageContext-RU]]
- [[Middleware]]
+13 -13
@@ -11,11 +11,11 @@ Middleware is useful when the same check or side effect should apply in more tha
Typical uses:
- reject updates from unauthorized users;
- log incoming commands and callback payloads;
- attach derived values to `MsgContext`;
- attach derived values to `MessageContext`;
- stop processing early when a precondition is not met;
- run non-blocking side effects such as analytics or audit logging.
For the handler and plugin model around middleware, see [[Commands-and-Plugins]]. For the fields you can read or update on the context, see [[MsgContext]].
For the handler and plugin model around middleware, see [[Commands-and-Plugins]]. For the fields you can read or update on the context, see [[MessageContext]].
## Middleware levels
@@ -47,7 +47,7 @@ For non-command update handlers registered through `AddUpdateHandler(...)`, the
Important detail:
- bot-level middleware runs once per update before routing;
- for non-command update handlers, each matching plugin receives its own cloned `MsgContext`, so one plugin's mutations do not leak into the next plugin's handler chain.
- for non-command update handlers, each matching plugin receives its own cloned `MessageContext`, so one plugin's mutations do not leak into the next plugin's handler chain.
## Synchronous middleware
@@ -56,7 +56,7 @@ By default, middleware is synchronous.
The executor signature is:
```go
func(ctx *laniakea.MsgContext, db T) bool
func(ctx *laniakea.MessageContext, db T) bool
```
Return values mean:
@@ -72,7 +72,7 @@ This makes synchronous middleware the right choice for:
Example:
```go
auth := laniakea.NewMiddleware("auth", func(ctx *laniakea.MsgContext, db *App) bool {
auth := laniakea.NewMiddleware("auth", func(ctx *laniakea.MessageContext, db *App) bool {
if ctx.From == nil || !db.Allowed(ctx.From.ID) {
ctx.Answer("Access denied")
return false
@@ -86,7 +86,7 @@ auth := laniakea.NewMiddleware("auth", func(ctx *laniakea.MsgContext, db *App) b
Middleware can also run asynchronously with `SetAsync(true)`.
```go
audit := laniakea.NewMiddleware("audit", func(ctx *laniakea.MsgContext, db *App) bool {
audit := laniakea.NewMiddleware("audit", func(ctx *laniakea.MessageContext, db *App) bool {
db.Audit(ctx.Update.UpdateID, ctx.Text)
return true
}).SetAsync(true)
@@ -96,7 +96,7 @@ Async middleware behaves differently:
- it runs in a goroutine;
- execution always continues immediately;
- its boolean return value is ignored;
- it receives a copied `MsgContext`, not the original pointer.
- it receives a copied `MessageContext`, not the original pointer.
That means async middleware is appropriate for:
- fire-and-forget logging;
@@ -112,7 +112,7 @@ It is not appropriate for:
## Why async middleware gets a copied context
When middleware is async, the library copies `MsgContext` before starting the goroutine. This prevents obvious data races against the handler path.
When middleware is async, the library copies `MessageContext` before starting the goroutine. This prevents obvious data races against the handler path.
Practical consequence:
- changes you make to the copied `ctx` inside async middleware are local to that goroutine;
@@ -121,7 +121,7 @@ Practical consequence:
So this pattern does not work:
```go
bad := laniakea.NewMiddleware("bad", func(ctx *laniakea.MsgContext, db *App) bool {
bad := laniakea.NewMiddleware("bad", func(ctx *laniakea.MessageContext, db *App) bool {
ctx.Text = "rewritten"
return false
}).SetAsync(true)
@@ -177,7 +177,7 @@ This usually means:
```go
bot.AddMiddleware(
laniakea.NewMiddleware("private-only", func(ctx *laniakea.MsgContext, db *App) bool {
laniakea.NewMiddleware("private-only", func(ctx *laniakea.MessageContext, db *App) bool {
if ctx.Chat == nil || ctx.Chat.Type != "private" {
return false
}
@@ -191,7 +191,7 @@ bot.AddMiddleware(
```go
admin := laniakea.NewPlugin[*App]("admin")
admin.AddMiddleware(
laniakea.NewMiddleware("admin-only", func(ctx *laniakea.MsgContext, db *App) bool {
laniakea.NewMiddleware("admin-only", func(ctx *laniakea.MessageContext, db *App) bool {
return ctx.From != nil && db.IsAdmin(ctx.From.ID)
}),
)
@@ -200,8 +200,8 @@ admin.AddMiddleware(
### Command-specific validation
```go
ban := admin.Command("ban", banUser)
ban.Use(laniakea.NewMiddleware("require-reply", func(ctx *laniakea.MsgContext, db *App) bool {
ban := admin.NewCommand(banUser, "ban")
ban.Use(laniakea.NewMiddleware("require-reply", func(ctx *laniakea.MessageContext, db *App) bool {
if ctx.Msg == nil || ctx.Msg.ReplyToMessage == nil {
ctx.Answer("Reply to a user message first")
return false
+64 -1
@@ -25,7 +25,70 @@ When jumping across multiple RC versions:
5. Revisit any direct `tgapi` calls and renamed types.
6. Run tests against realistic update payloads and callback data.
The largest migration points in the current history are `rc.4`, `rc.7`, `rc.10`, and `rc.12`.
The largest migration points in the current history are `rc.4`, `rc.7`, `rc.10`, `rc.12`, and the `v1.0.0` stable release.
## `v1.0.0`
`v1.0.0` is a public-API hygiene release. Most changes are renames and type-system tightenings that cause compile errors and are easy to fix mechanically.
### Handler type rename: `MsgContext``MessageContext`
Every handler signature and every explicit type reference to `MsgContext` must be renamed to `MessageContext`.
```go
// before
func ping(ctx *laniakea.MsgContext, db *App) error { ... }
// after
func ping(ctx *laniakea.MessageContext, db *App) error { ... }
```
This includes `CommandExecutor`, `MiddlewareExecutor`, `SceneContext.MessageContext`, and any local variable type annotations.
### Runner builder change: `Onetime` and `Timeout` removed
The `Onetime(bool)` and `Timeout(duration)` builder methods have been replaced by `Every(duration)` and `Async(bool)`.
```go
// before — one-time sync
runner.Onetime(true).Async(false)
// after — one-time sync
runner.Async(false)
// before — periodic
runner.Timeout(5 * time.Minute)
// after — periodic
runner.Every(5 * time.Minute)
```
The default remains async one-shot (`Every(0).Async(true)`), so runners without a builder call are unaffected.
### Observer method renames
If you implement the `Observer` interface directly, rename the two affected methods:
| Before | After |
|---|---|
| `OnReceiveUpdate(UpdateReceivedEvent)` | `OnUpdateReceived(UpdateReceivedEvent)` |
| `OnHandledUpdate(UpdateHandledEvent)` | `OnUpdateHandled(UpdateHandledEvent)` |
### `Scene.PluginName` unexported
`Scene.PluginName` was a mutable public field. It is now unexported. Remove any reads or writes to this field; the framework assigns it during `AddPlugins(...)` registration.
### `SceneSession.Data` unexported
`SceneSession.Data []byte` was a public field. It is now unexported. Use the accessor helpers: `HasData()`, `BindData(...)`, `SaveData(...)`, `ClearData()`.
### `BotPayloadType*` constants are now `const`
`BotPayloadTypeJSON`, `BotPayloadTypeBase64`, `BotPayloadTypeCompact`, and `BotPayloadTypeCompactBase64` were `var`. They are now `const`. Any code assigning to them will fail to compile.
### Webhook error sentinels
Inline `errors.New(...)` error values returned from webhook startup have been replaced by exported sentinels. If you were comparing webhook startup errors with `==`, switch to `errors.Is(...)`.
## `v1.0.0-rc.12`
+9 -9
@@ -1,12 +1,12 @@
# MsgContext RU
# MessageContext RU
English version: [[MsgContext]]
English version: [[MessageContext]]
Это краткая русскоязычная версия страницы про `MsgContext`. Полная и наиболее актуальная страница: [[MsgContext]].
Это краткая русскоязычная версия страницы про `MessageContext`. Полная и наиболее актуальная страница: [[MessageContext]].
## Что такое `MsgContext`
## Что такое `MessageContext`
`MsgContext` — это объект времени выполнения, который приходит в:
`MessageContext` — это объект времени выполнения, который приходит в:
- обработчики команд;
- обработчики данных callback;
- middleware;
@@ -18,7 +18,7 @@ English version: [[MsgContext]]
- разобранные аргументы команд и callback;
- вспомогательные методы для reply, edit, delete, callback, drafts и localization.
Полную матрицу маршрутизации и гарантий по полям `MsgContext` для разных update types смотри в [[Update-Routing-Model-RU]].
Полную матрицу маршрутизации и гарантий по полям `MessageContext` для разных update types смотри в [[Update-Routing-Model-RU]].
## Поля, которые используются чаще всего
@@ -107,12 +107,12 @@ English version: [[MsgContext]]
## Drafts и localization
У `MsgContext` есть:
У `MessageContext` есть:
- `NewDraft()`
- `NewDraftMarkdown()`
- `Translate(key)`
Это делает `MsgContext` основной удобной точкой доступа почти для всего кода обработчиков.
Это делает `MessageContext` основной удобной точкой доступа почти для всего кода обработчиков.
## `NewInlineKeyboard(...)`
@@ -130,4 +130,4 @@ kb := ctx.NewInlineKeyboard(2)
- [[Inline-Keyboards-and-Payloads-RU]]
- [[Drafts-RU]]
- [[Localization-RU]]
- [[MsgContext]]
- [[MessageContext]]
+23 -23
@@ -1,8 +1,8 @@
# MsgContext
# MessageContext
Russian version: [[MsgContext-RU]]
Russian version: [[MessageContext-RU]]
`MsgContext` is the runtime object passed into command handlers, payload handlers, middleware, and update handlers.
`MessageContext` is the runtime object passed into command handlers, payload handlers, middleware, and update handlers.
It gives you access to:
- the incoming update
@@ -10,7 +10,7 @@ It gives you access to:
- parsed command or payload arguments
- reply, edit, delete, callback, draft, and localization helpers
If you write handlers, `MsgContext` is the API surface you will use most often.
If you write handlers, `MessageContext` is the API surface you will use most often.
For the full routing and field-guarantee matrix by update kind, see [[Update-Routing-Model]].
@@ -63,7 +63,7 @@ Use `FromID` when you only need the identifier and do not want to keep checking
Use `Answer(...)` for the normal “reply with text” case.
```go
func start(ctx *laniakea.MsgContext, db laniakea.NoData) error {
func start(ctx *laniakea.MessageContext, db laniakea.NoData) error {
ctx.Answer("Welcome")
return nil
}
@@ -76,7 +76,7 @@ This is the default high-level reply helper for plain text.
Use `AnswerLong(...)` when plain text may exceed Telegrams message limit.
```go
func help(ctx *laniakea.MsgContext, db laniakea.NoData) error {
func help(ctx *laniakea.MessageContext, db laniakea.NoData) error {
ctx.AnswerLong(buildLargeHelpText())
return nil
}
@@ -92,7 +92,7 @@ Important:
Use `Keyboard(...)` when you want to send a message with an inline keyboard.
```go
func menu(ctx *laniakea.MsgContext, db laniakea.NoData) error {
func menu(ctx *laniakea.MessageContext, db laniakea.NoData) error {
kb := ctx.NewInlineKeyboard(2).
AddCallbackButton("Profile", "profile.open").
AddCallbackButton("Settings", "settings.open")
@@ -127,7 +127,7 @@ Use `laniakea.EscapeMarkdownV2(...)` for this.
Example:
```go
func whoami(ctx *laniakea.MsgContext, db laniakea.NoData) error {
func whoami(ctx *laniakea.MessageContext, db laniakea.NoData) error {
name := laniakea.EscapeMarkdownV2(ctx.From.FirstName)
ctx.AnswerMarkdown("*User:* " + name)
return nil
@@ -141,7 +141,7 @@ Once you already have an `AnswerMessage`, you can edit or delete it.
Example:
```go
func slowTask(ctx *laniakea.MsgContext, db laniakea.NoData) error {
func slowTask(ctx *laniakea.MessageContext, db laniakea.NoData) error {
msg := ctx.Answer("Working...")
if msg == nil {
return nil
@@ -173,27 +173,27 @@ When handling inline button callbacks, these helpers are especially useful.
Edits the callback-linked message.
```go
func approve(ctx *laniakea.MsgContext, db laniakea.NoData) error {
func approve(ctx *laniakea.MessageContext, db laniakea.NoData) error {
ctx.EditCallback("Approved", nil)
return nil
}
```
### `AnswerCallback`
### `AnswerCbQuery`
Acknowledges the callback query itself.
Use:
- `AnswerCallback()` for empty acknowledgement
- `AnswerCallbackText(...)` for a short notice
- `AnswerCallbackAlert(...)` for a visible alert
- `AnswerCallbackURL(...)` for redirect behavior
- `AnswerCbQuery()` for empty acknowledgement
- `AnswerCbQueryText(...)` for a short notice
- `AnswerCbQueryAlert(...)` for a visible alert
- `AnswerCbQueryUrl(...)` for redirect behavior
Example:
```go
func approve(ctx *laniakea.MsgContext, db laniakea.NoData) error {
ctx.AnswerCallbackText("Saved")
func approve(ctx *laniakea.MessageContext, db laniakea.NoData) error {
ctx.AnswerCbQueryText("Saved")
ctx.EditCallback("Saved", nil)
return nil
}
@@ -220,7 +220,7 @@ Caption rules differ from normal message text:
## Drafts
`MsgContext` also exposes draft creation helpers:
`MessageContext` also exposes draft creation helpers:
- `NewDraft()`
- `NewDraftMarkdown()`
@@ -246,7 +246,7 @@ This looks up text using the current users language when available and falls
Example:
```go
func ping(ctx *laniakea.MsgContext, db laniakea.NoData) error {
func ping(ctx *laniakea.MessageContext, db laniakea.NoData) error {
ctx.Answer(ctx.Translate("ping.answer"))
return nil
}
@@ -275,7 +275,7 @@ Use `SendAction(...)` to show activity like typing or uploading.
Example:
```go
func report(ctx *laniakea.MsgContext, db *App) error {
func report(ctx *laniakea.MessageContext, db *App) error {
ctx.SendAction(tgapi.ChatActionTyping)
text, err := db.BuildReport(ctx.FromID)
if err != nil {
@@ -293,7 +293,7 @@ This is especially useful for slower handlers.
A common pattern is:
```go
func profile(ctx *laniakea.MsgContext, db *App) error {
func profile(ctx *laniakea.MessageContext, db *App) error {
user, err := db.LoadUser(ctx.FromID)
if err != nil {
return err
@@ -333,12 +333,12 @@ Do not pass raw user input to MarkdownV2 methods without escaping.
### Callback helpers only make sense in callback flow
Methods like `EditCallback(...)` and `AnswerCallbackText(...)` depend on callback-specific context.
Methods like `EditCallback(...)` and `AnswerCbQueryText(...)` depend on callback-specific context.
## A practical example
```go
func settings(ctx *laniakea.MsgContext, db *App) error {
func settings(ctx *laniakea.MessageContext, db *App) error {
kb := ctx.NewInlineKeyboard(1).
AddCallbackButton("Enable notifications", "settings.notifications.enable").
AddCallbackButton("Disable notifications", "settings.notifications.disable")
+4 -4
@@ -1,6 +1,6 @@
# Policies
Policies — это полноценная модель правил авторизации в Laniakea. Политика работает поверх нормализованного `MsgContext` и переданных `AppData`, возвращает `nil`, если доступ разрешён, и возвращает ошибку, если действие нужно запретить или сама проверка не смогла корректно выполниться.
Policies — это полноценная модель правил авторизации в Laniakea. Политика работает поверх нормализованного `MessageContext` и переданных `AppData`, возвращает `nil`, если доступ разрешён, и возвращает ошибку, если действие нужно запретить или сама проверка не смогла корректно выполниться.
Policies не вводят вторую модель выполнения. Они встраиваются в уже существующий middleware pipeline через `RequirePolicy(...)`, поэтому авторизация остаётся на том же пути маршрутизации, что и остальной фреймворк.
@@ -15,7 +15,7 @@ Policies не вводят вторую модель выполнения. Он
## Основной API
```go
type Policy[T laniakea.AppData] func(ctx *laniakea.MsgContext, data T) error
type Policy[T laniakea.AppData] func(ctx *laniakea.MessageContext, data T) error
func RequirePolicy[T laniakea.AppData](name string, p Policy[T]) Middleware[T]
@@ -62,7 +62,7 @@ plugin.UsePolicy(
- `RequireBotAdmin(...)`
- `RequireCallbackFromUser(...)`
Эти helpers используют нормализованные данные `MsgContext`. В частности, chat-aware политики опираются на `Chat` и `ChatID`, которые теперь заполняются не только для message-backed обновлений.
Эти helpers используют нормализованные данные `MessageContext`. В частности, chat-aware политики опираются на `Chat` и `ChatID`, которые теперь заполняются не только для message-backed обновлений.
## Композиция
@@ -133,6 +133,6 @@ policy := laniakea.NotPolicy(laniakea.RequirePrivateChat())
Связанные страницы:
- [[Middleware-RU]]
- [[MsgContext-RU]]
- [[MessageContext-RU]]
- [[Commands-and-Plugins-RU]]
- [[Framework-Backlog-RU]]
+4 -4
@@ -1,6 +1,6 @@
# Policies
Policies are Laniakea's first-class authorization rules. A policy runs against the normalized `MsgContext` and injected `AppData`, returns `nil` when access is allowed, and returns an error when access should be denied or when the check itself fails.
Policies are Laniakea's first-class authorization rules. A policy runs against the normalized `MessageContext` and injected `AppData`, returns `nil` when access is allowed, and returns an error when access should be denied or when the check itself fails.
Policies do not introduce a second execution model. They plug into the existing middleware pipeline through `RequirePolicy(...)`, so authorization stays on the same routing path as the rest of the framework.
@@ -15,7 +15,7 @@ Policies do not introduce a second execution model. They plug into the existing
## Core API
```go
type Policy[T laniakea.AppData] func(ctx *laniakea.MsgContext, data T) error
type Policy[T laniakea.AppData] func(ctx *laniakea.MessageContext, data T) error
func RequirePolicy[T laniakea.AppData](name string, p Policy[T]) Middleware[T]
@@ -62,7 +62,7 @@ The current built-in helpers focus on common Telegram-specific access checks:
- `RequireBotAdmin(...)`
- `RequireCallbackFromUser(...)`
These helpers use normalized `MsgContext` data. In particular, chat-aware policies rely on `Chat` and `ChatID`, which are now populated for more update kinds than only message-backed ones.
These helpers use normalized `MessageContext` data. In particular, chat-aware policies rely on `Chat` and `ChatID`, which are now populated for more update kinds than only message-backed ones.
## Composition
@@ -133,6 +133,6 @@ Those features can be added later if the existing `Policy[T]` model proves too s
Related pages:
- [[Middleware]]
- [[MsgContext]]
- [[MessageContext]]
- [[Commands-and-Plugins]]
- [[Framework-Backlog]]
+17 -17
@@ -14,14 +14,14 @@ type App struct{}
func (a *App) IsAdmin(userID int64) bool { return userID == 42 }
admin := laniakea.NewPlugin[*App]("admin")
admin.AddMiddleware(laniakea.NewMiddleware("admin-only", func(ctx *laniakea.MsgContext, app *App) bool {
admin.AddMiddleware(laniakea.NewMiddleware("admin-only", func(ctx *laniakea.MessageContext, app *App) bool {
return ctx.From != nil && app.IsAdmin(ctx.From.ID)
}))
admin.Command("reload", func(ctx *laniakea.MsgContext, app *App) error {
admin.NewCommand(func(ctx *laniakea.MessageContext, 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.Command("menu", func(ctx *laniakea.MsgContext, db laniakea.NoData) error {
menu.NewCommand(func(ctx *laniakea.MessageContext, db laniakea.NoData) error {
kb := ctx.NewInlineKeyboard(1)
kb.AddCallbackButton("Open settings", "settings")
ctx.Keyboard("Choose an action", kb)
return nil
})
}, "menu")
menu.Payload("settings", func(ctx *laniakea.MsgContext, db laniakea.NoData) error {
menu.NewPayload(func(ctx *laniakea.MessageContext, db laniakea.NoData) error {
ctx.EditCallback("Settings screen", nil)
return nil
})
}, "settings")
```
## Long plain-text reply
@@ -49,11 +49,11 @@ menu.Payload("settings", func(ctx *laniakea.MsgContext, db laniakea.NoData) erro
Use `AnswerLong(...)` when you want explicit splitting into multiple safe Telegram messages.
```go
plugin.Command("report", func(ctx *laniakea.MsgContext, db *App) error {
plugin.NewCommand(func(ctx *laniakea.MessageContext, 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.Command("start", func(ctx *laniakea.MsgContext, db *App) error {
plugin.NewCommand(func(ctx *laniakea.MessageContext, db *App) error {
ctx.Answer(ctx.Translate("greeting"))
return nil
})
}, "start")
```
## Non-command update handler
@@ -82,7 +82,7 @@ plugin.Command("start", func(ctx *laniakea.MsgContext, db *App) error {
Use `AddUpdateHandler(...)` for Telegram update types that are outside the command and payload flow.
```go
plugin.AddUpdateHandler(tgapi.UpdateTypeInlineQuery, func(ctx *laniakea.MsgContext, db *App) error {
plugin.AddUpdateHandler(tgapi.UpdateTypeInlineQuery, func(ctx *laniakea.MessageContext, db *App) error {
if ctx.From != nil {
ctx.Logger.Infoln("inline query from", ctx.From.ID)
}
@@ -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.Command("build", func(ctx *laniakea.MsgContext, db *App) error {
plugin.NewCommand(func(ctx *laniakea.MessageContext, db *App) error {
draft := ctx.NewDraft()
if draft == nil {
return nil
@@ -111,7 +111,7 @@ plugin.Command("build", func(ctx *laniakea.MsgContext, db *App) error {
}
return draft.Flush()
})
}, "build")
```
## File upload with `tgapi`
@@ -119,15 +119,15 @@ plugin.Command("build", 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.Command("upload", func(ctx *laniakea.MsgContext, db *App) error {
uploader := tgapi.NewUploader(ctx.API)
plugin.NewCommand(func(ctx *laniakea.MessageContext, 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`.
+49 -15
@@ -22,36 +22,45 @@ Runner — это фоновая или одноразовая задача, к
runner := laniakea.NewRunner("cleanup", fn)
```
Потом конфигурируются методы builder:
- `Once(bool)`
- `Async(bool)`
- `Every(duration)`
Методы builder:
- `Every(time.Duration)` — интервал повторного запуска. Ноль (по умолчанию) означает одноразовый запуск; положительное значение — периодический.
- `Async(bool)` — если `true` (по умолчанию), запускается в goroutine; если `false`, блокирует запуск runtime.
## Основные режимы
### Одноразовый sync
### Одноразовый async (по умолчанию)
- выполняется один раз;
- блокирует запуск;
- полезен для работы, критичной на старте.
### Одноразовый async
```go
runner := laniakea.NewRunner("prefetch", fn)
```
- выполняется один раз;
- стартует в goroutine;
- не блокирует запуск.
### Одноразовый sync
```go
runner := laniakea.NewRunner("warmup", fn).Async(false)
```
- выполняется один раз;
- блокирует запуск до завершения;
- полезен для работы, критичной на старте.
### Повторяющийся async
- работает циклически;
```go
runner := laniakea.NewRunner("cleanup", fn).Every(time.Minute)
```
- работает циклически с заданным интервалом;
- использует ticker;
- живет до `ctx.Done()`.
## Невалидная конфигурация
Повторяющийся synchronous runner считается невалидным и пропускается с предупреждением.
Также повторяющийся async runner без `Every(...)` пропускается.
Повторяющийся synchronous runner (`Every(d > 0).Async(false)`) считается невалидным и пропускается с предупреждением — блокировать запуск бесконечно никогда не имеет смысла.
## Когда стартуют фоновые задачи
@@ -59,6 +68,13 @@ runner := laniakea.NewRunner("cleanup", fn)
Это часть фазы выполнения, а не фазы сборки конфигурации.
## Обработка ошибок
Если runner возвращает ошибку:
- бот логирует предупреждение;
- через observer отправляется `ErrorEvent`;
- бот продолжает работу.
## Семантика остановки
При корректной остановке бот:
@@ -67,9 +83,27 @@ runner := laniakea.NewRunner("cleanup", fn)
Поэтому код runner должен завершаться достаточно быстро.
## Практические примеры
### Периодическая очистка
```go
cleanup := laniakea.NewRunner("cleanup", func(bot *laniakea.Bot[*App]) error {
return bot.GetAppData().CleanupExpired()
}).Every(5 * time.Minute)
```
### Блокирующий запуск при старте
```go
warmup := laniakea.NewRunner("warmup", func(bot *laniakea.Bot[*App]) error {
return bot.GetAppData().WarmCaches()
}).Async(false)
```
## Рекомендации
- Для периодических задач используй повторяющийся async runner с timeout.
- Для периодических задач используй повторяющийся async runner с `Every(...)`.
- Для критичной стартовой работы используй одноразовый sync runner.
- Не держи сложную бизнес-логику внутри runner; лучше делегируй ее в обычные сервисы приложения.
+33 -43
@@ -13,10 +13,9 @@ Each runner is built from:
- a function `func(*Bot[T]) error`;
- execution flags configured through builder methods.
Main builder methods:
- `Once(bool)`
- `Async(bool)`
- `Every(duration)`
Builder methods:
- `Every(time.Duration)` — sets the repeat interval. Zero (default) means run once; positive means repeat.
- `Async(bool)` — if `true` (default), the runner runs in a goroutine; if `false`, it blocks runtime startup.
## Creating a runner
@@ -29,36 +28,21 @@ cleanup := laniakea.NewRunner("cleanup", func(bot *laniakea.Bot[*App]) error {
```
By default, a new runner is:
- asynchronous;
- not one-time;
- configured with zero timeout.
- asynchronous (`Async(true)`);
- one-shot (`Every(0)`).
That default means you almost always want to finish configuration before adding it to the bot.
That default fires the runner once in a goroutine when the bot starts.
## Runner execution modes
There are three meaningful configurations.
### One-time synchronous
### One-time asynchronous (default)
```go
runner := laniakea.NewRunner("warmup", fn).
Once(true).
Async(false)
```
Behavior:
- runs once;
- blocks startup until it finishes;
- logs a warning if it takes longer than two seconds.
Use this for startup work that must complete before the bot is considered ready.
### One-time asynchronous
```go
runner := laniakea.NewRunner("prefetch", fn).
Once(true)
runner := laniakea.NewRunner("prefetch", fn)
// or explicitly:
runner := laniakea.NewRunner("prefetch", fn).Every(0).Async(true)
```
Behavior:
@@ -68,31 +52,37 @@ Behavior:
Use this for fire-and-forget startup work that is useful but not required before handling updates.
### Repeating asynchronous
### One-time synchronous
```go
runner := laniakea.NewRunner("cleanup", fn).
Every(time.Minute)
runner := laniakea.NewRunner("warmup", fn).Async(false)
```
Behavior:
- runs on a ticker;
- runs once;
- blocks startup until it finishes;
- logs a warning if it takes longer than two seconds.
Use this for startup work that must complete before the bot is considered ready.
### Repeating asynchronous
```go
runner := laniakea.NewRunner("cleanup", fn).Every(time.Minute)
```
Behavior:
- runs on a ticker with the configured interval;
- keeps running until `ctx.Done()` from the bot runtime context;
- is awaited during graceful shutdown.
Use this for recurring background jobs.
## Invalid configuration
## Invalid configurations
One configuration is intentionally treated as invalid:
One configuration is intentionally treated as invalid and skipped with a warning:
- `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 `Every(0)` are skipped with a warning.
- `Every(d > 0).Async(false)` — a periodic sync runner blocks startup indefinitely, which is never correct.
## Registration
@@ -125,6 +115,7 @@ Runner functions return `error`.
When a runner returns a non-nil error:
- the bot logs a warning;
- an `ErrorEvent` is emitted through the observer;
- the process continues;
- the bot does not crash automatically.
@@ -152,12 +143,12 @@ cleanup := laniakea.NewRunner("cleanup", func(bot *laniakea.Bot[*App]) error {
}).Every(5 * time.Minute)
```
### Startup warmup
### Startup warmup (blocking)
```go
warmup := laniakea.NewRunner("warmup", func(bot *laniakea.Bot[*App]) error {
return bot.GetAppData().WarmCaches()
}).Once(true).Async(false)
}).Async(false)
```
### Background metrics push
@@ -179,8 +170,7 @@ metrics := laniakea.NewRunner("metrics", func(bot *laniakea.Bot[*App]) error {
## Caveats
- Runners do not receive `context.Context` directly; they receive `*Bot[T]`.
- Repeating sync runners are skipped.
- Repeating async runners without timeout are skipped.
- Periodic sync runners are skipped with a warning.
- Slow one-time sync runners delay bot startup.
## Related pages
+8 -8
@@ -1,11 +1,11 @@
# Scenes
Scenes — это слой маршрутизации Laniakea с сохранением состояния для многошаговых и модальных диалогов. Сцена регистрируется внутри плагина, запускается через `MsgContext`, хранится через `SessionStore` и получает обновления раньше обычной маршрутизации команд, пока её сессия активна.
Scenes — это слой маршрутизации Laniakea с сохранением состояния для многошаговых и модальных диалогов. Сцена регистрируется внутри плагина, запускается через `MessageContext`, хранится через `SessionStore` и получает обновления раньше обычной маршрутизации команд, пока её сессия активна.
## Что дают сцены
- Регистрацию через `Plugin.Scene(...)` и `Plugin.AddScene(...)`.
- Явный вход и выход через `MsgContext.EnterScene(...)`, `EnterSceneStep(...)` и `ExitScene()`.
- Регистрацию через `Plugin.NewScene(...)` и `Plugin.AddScene(...)`.
- Явный вход и выход через `MessageContext.EnterScene(...)`, `EnterSceneStep(...)` и `ExitScene()`.
- Области действия сессии на пользователя, чат или пару пользователь-чат.
- Обработчики шагов, локальные команды сцены и резервный обработчик сообщений на уровне сцены.
- JSON-состояние сцены через `SceneContext.BindData(...)` и `SaveData(...)`.
@@ -54,7 +54,7 @@ type SessionStore interface {
Сцены регистрируются внутри плагина в том же стиле, что и команды с данными callback.
```go
plugin.Scene("signup").
plugin.NewScene("signup").
SetScope(laniakea.SceneScopeUserChat).
SetEntry("ask_name").
OnStep("ask_name", askName).
@@ -67,13 +67,13 @@ plugin.Scene("signup").
## Модель обработчиков
Обычные команды используют `*MsgContext`. Обработчики сцен используют `*SceneContext`.
Обычные команды используют `*MessageContext`. Обработчики сцен используют `*SceneContext`.
```go
type SceneHandler[T any] func(ctx *SceneContext, db T) (SceneResult, error)
```
`SceneContext` встраивает `*MsgContext` и добавляет вспомогательные методы для сцен:
`SceneContext` встраивает `*MessageContext` и добавляет вспомогательные методы для сцен:
- `ctx.Stay()`
- `ctx.Next(step)`
@@ -137,7 +137,7 @@ func askName(ctx *laniakea.SceneContext, db *App) (laniakea.SceneResult, error)
Точка входа из команды:
```go
func startSignup(ctx *laniakea.MsgContext, db *App) error {
func startSignup(ctx *laniakea.MessageContext, db *App) error {
return ctx.EnterScene("signup")
}
```
@@ -174,6 +174,6 @@ func cancelSignup(ctx *laniakea.SceneContext, db *App) (laniakea.SceneResult, er
Связанные страницы:
- [[Commands-and-Plugins-RU]]
- [[MsgContext-RU]]
- [[MessageContext-RU]]
- [[Middleware-RU]]
- [[Bot-Lifecycle-RU]]
+8 -8
@@ -1,11 +1,11 @@
# Scenes
Scenes are Laniakea's stateful routing layer for multi-step and modal bot flows. A scene is registered inside a plugin, entered through `MsgContext`, stored through `SessionStore`, and routed before normal command handling while the session is active.
Scenes are Laniakea's stateful routing layer for multi-step and modal bot flows. A scene is registered inside a plugin, entered through `MessageContext`, stored through `SessionStore`, and routed before normal command handling while the session is active.
## What scenes give you
- Scene registration through `Plugin.Scene(...)` and `Plugin.AddScene(...)`.
- Explicit entry and exit through `MsgContext.EnterScene(...)`, `EnterSceneStep(...)`, and `ExitScene()`.
- Scene registration through `Plugin.NewScene(...)` and `Plugin.AddScene(...)`.
- Explicit entry and exit through `MessageContext.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.
- JSON-backed scene state through `SceneContext.BindData(...)` and `SaveData(...)`.
@@ -54,7 +54,7 @@ Recommended default:
Scenes are registered inside plugins in the same style as commands and payloads.
```go
plugin.Scene("signup").
plugin.NewScene("signup").
SetScope(laniakea.SceneScopeUserChat).
SetEntry("ask_name").
OnStep("ask_name", askName).
@@ -67,13 +67,13 @@ plugin.Scene("signup").
## Handler model
Normal commands use `*MsgContext`. Scene handlers use `*SceneContext`.
Normal commands use `*MessageContext`. Scene handlers use `*SceneContext`.
```go
type SceneHandler[T any] func(ctx *SceneContext, db T) (SceneResult, error)
```
`SceneContext` embeds `*MsgContext` and adds scene helpers:
`SceneContext` embeds `*MessageContext` and adds scene helpers:
- `ctx.Stay()`
- `ctx.Next(step)`
@@ -137,7 +137,7 @@ The store contract stays intentionally small because `Data []byte` is storage-ag
Command entry:
```go
func startSignup(ctx *laniakea.MsgContext, db *App) error {
func startSignup(ctx *laniakea.MessageContext, db *App) error {
return ctx.EnterScene("signup")
}
```
@@ -174,6 +174,6 @@ func cancelSignup(ctx *laniakea.SceneContext, db *App) (laniakea.SceneResult, er
Related pages:
- [[Commands-and-Plugins]]
- [[MsgContext]]
- [[MessageContext]]
- [[Middleware]]
- [[Bot-Lifecycle]]
+3 -3
@@ -10,7 +10,7 @@ Laniakea хорошо тестируется обычными Go unit tests.
В репозитории уже используются паттерны вроде:
- fake HTTP transport для `tgapi`;
- прямые тесты для вспомогательных методов `MsgContext`;
- прямые тесты для вспомогательных методов `MessageContext`;
- routing tests для commands и payloads;
- runner tests.
@@ -35,7 +35,7 @@ Laniakea хорошо тестируется обычными Go unit tests.
## Тесты для логики обработчиков
Для тестов уровня обработчика обычно полезно:
- собрать `MsgContext`;
- собрать `MessageContext`;
- вызвать обработчик напрямую;
- проверить побочные эффекты и ответы.
@@ -63,5 +63,5 @@ Laniakea хорошо тестируется обычными Go unit tests.
## Что читать дальше
- [[Runners-RU]]
- [[MsgContext-RU]]
- [[MessageContext-RU]]
- [[Testing-Bots-with-Laniakea]]
+7 -7
@@ -21,15 +21,15 @@ Good test targets include:
The most practical approach is:
1. isolate one behavior;
2. create a small bot, plugin, or `MsgContext`;
2. create a small bot, plugin, or `MessageContext`;
3. use a fake HTTP client when you need to inspect Telegram requests;
4. assert the outgoing request shape or returned behavior directly.
This keeps tests fast and independent from real Telegram infrastructure.
## Testing `MsgContext` helpers
## Testing `MessageContext` helpers
Many helper methods can be tested by constructing a `MsgContext` directly.
Many helper methods can be tested by constructing a `MessageContext` directly.
Common ingredients:
- a fake `tgapi.API` with a custom `http.Client`;
@@ -39,7 +39,7 @@ Common ingredients:
Example pattern:
```go
ctx := &laniakea.MsgContext{
ctx := &laniakea.MessageContext{
Api: api,
Msg: &tgapi.Message{Chat: &tgapi.Chat{ID: 42, Type: string(tgapi.ChatTypePrivate)}},
Logger: slog.CreateLogger(),
@@ -64,7 +64,7 @@ This is especially useful for:
- split long-message requests;
- keyboard attachment behavior.
The repository already uses this pattern extensively for `MsgContext` helper tests.
The repository already uses this pattern extensively for `MessageContext` helper tests.
## Testing long replies
@@ -96,7 +96,7 @@ For routing tests, create a small bot with:
Then call the bot's handling path in a focused test and assert:
- which handler was called;
- what `MsgContext` fields were populated;
- what `MessageContext` fields were populated;
- whether context mutations leaked across plugins.
This is particularly useful for:
@@ -184,4 +184,4 @@ When you fix a bug, consider adding a test for:
- [[Commands-and-Plugins]]
- [[Error-Handling]]
- [[Runners]]
- [[MsgContext]]
- [[MessageContext]]
+7 -7
@@ -2,9 +2,9 @@
English version: [[Update-Routing-Model]]
Эта страница фиксирует текущий контракт маршрутизации обновлений в Laniakea: какие Telegram update types идут через команды, какие через данные callback, какие через generic update handlers, и какие гарантии по `MsgContext` существуют в каждом потоке.
Эта страница фиксирует текущий контракт маршрутизации обновлений в Laniakea: какие Telegram update types идут через команды, какие через данные callback, какие через generic update handlers, и какие гарантии по `MessageContext` существуют в каждом потоке.
Это описание текущего поведения фреймворка, а не отдельная новая API-поверхность. Цель страницы — сделать уже существующую маршрутизацию и нормализацию `MsgContext` явной и тестируемой.
Это описание текущего поведения фреймворка, а не отдельная новая API-поверхность. Цель страницы — сделать уже существующую маршрутизацию и нормализацию `MessageContext` явной и тестируемой.
## Верхнеуровневая маршрутизация
@@ -23,7 +23,7 @@ English version: [[Update-Routing-Model]]
## Что делает `prepareUpdateCtx(...)`
До маршрутизации обработчиков Laniakea нормализует `MsgContext` из входящего Telegram update.
До маршрутизации обработчиков Laniakea нормализует `MessageContext` из входящего Telegram update.
Эта нормализация намеренно шире, чем command и payload routing:
- некоторые update types заполняют `ctx.Msg`
@@ -101,12 +101,12 @@ Generic update handlers регистрируются через `Plugin.AddUpdat
- `ctx.Text` не нормализуется
- `ctx.Args` не нормализуется
- `ctx.Prefix` не нормализуется
- каждый plugin update handler получает свою копию `MsgContext` struct
- каждый plugin update handler получает свою копию `MessageContext` struct
Важное ограничение:
- копируется сам `MsgContext`, но не обещается глубокая копия всех вложенных Telegram-структур
- копируется сам `MessageContext`, но не обещается глубокая копия всех вложенных Telegram-структур
## Нормализованные поля `MsgContext` по видам update
## Нормализованные поля `MessageContext` по видам update
### Update types, которые несут message
@@ -176,6 +176,6 @@ Generic update handlers регистрируются через `Plugin.AddUpdat
## Связанные страницы
- [[Commands-and-Plugins-RU]]
- [[MsgContext-RU]]
- [[MessageContext-RU]]
- [[Scenes-RU]]
- [[Bot-Lifecycle-RU]]
+6 -6
@@ -2,9 +2,9 @@
Russian version: [[Update-Routing-Model-RU]]
This page defines the current update-routing contract in Laniakea: which Telegram update kinds go through command routing, which go through payload routing, which go through generic update handlers, and what `MsgContext` guarantees exist in each path.
This page defines the current update-routing contract in Laniakea: which Telegram update kinds go through command routing, which go through payload routing, which go through generic update handlers, and what `MessageContext` guarantees exist in each path.
This is a model of current framework behavior, not a second API surface. The goal is to make the existing routing and `MsgContext` normalization explicit and testable.
This is a model of current framework behavior, not a second API surface. The goal is to make the existing routing and `MessageContext` normalization explicit and testable.
## Top-level routing
@@ -23,7 +23,7 @@ Important:
## What `prepareUpdateCtx(...)` does
Before handler routing, Laniakea normalizes a `MsgContext` from the incoming Telegram update.
Before handler routing, Laniakea normalizes a `MessageContext` from the incoming Telegram update.
That normalization is intentionally broader than command and payload routing:
- some update kinds populate `ctx.Msg`
@@ -101,12 +101,12 @@ General guarantees:
- `ctx.Text` is not normalized
- `ctx.Args` is not normalized
- `ctx.Prefix` is not normalized
- each plugin update handler receives its own copied `MsgContext` struct
- each plugin update handler receives its own copied `MessageContext` struct
Important limitation:
- the copied context is an isolated struct copy, not a deep copy of all nested Telegram payload objects
## Normalized `MsgContext` fields by update kind
## Normalized `MessageContext` fields by update kind
### Message-backed update kinds
@@ -176,6 +176,6 @@ Important:
## Related pages
- [[Commands-and-Plugins]]
- [[MsgContext]]
- [[MessageContext]]
- [[Scenes]]
- [[Bot-Lifecycle]]
+2 -2
@@ -5,8 +5,8 @@ English version: [[Webhook-Runtime]]
Эта страница объясняет bot-level webhook runtime в Laniakea: как работает `RunWebhookWithContext(...)`, чем он отличается от низкоуровневых webhook-вызовов в `tgapi` и какие runtime-гарантии он делит с polling-режимом.
Важно про naming:
- публичный API использует идиоматичное написание `Webhook` в идентификаторах вроде `RunWebhookWithContext(...)`, `RunWebhook(...)` и `BotWebhookOpts`;
- в примерах остаются реальные имена Go API.
- текущий публичный API использует историческое написание `WebHook` в идентификаторах вроде `RunWebhookWithContext(...)`, `RunWebhook(...)` и `BotWebhookOpts`;
- в тексте страницы используется обычное слово "webhook", но в примерах остаются реальные имена Go API.
## Когда использовать webhook runtime
+2 -2
@@ -5,8 +5,8 @@ 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.
Important naming note:
- the public API uses idiomatic `Webhook` spelling in identifiers such as `RunWebhookWithContext(...)`, `RunWebhook(...)`, and `BotWebhookOpts`;
- examples keep the actual Go API names.
- 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.
## When to use webhook runtime
+1 -1
@@ -6,7 +6,7 @@
- [[Getting-Started]]
- [[Bot-Options-and-Configuration]]
- [[Commands-and-Plugins]]
- [[MsgContext]]
- [[MessageContext]]
## Runtime and Architecture
- [[Bot-Lifecycle]]
+4 -4
@@ -21,14 +21,14 @@ English version: [[tgapi-Overview]]
Это разделение специально сделано, чтобы JSON methods и file upload methods не смешивались в одну слишком размытую abstraction.
## Когда использовать `MsgContext`, а когда `tgapi`
## Когда использовать `MessageContext`, а когда `tgapi`
Используй `MsgContext`, когда:
Используй `MessageContext`, когда:
- ты уже внутри handler'а;
- нужен обычный поток reply/edit/delete/callback.
Используй `tgapi`, когда:
- у `MsgContext` нет нужного вспомогательного метода;
- у `MessageContext` нет нужного вспомогательного метода;
- ты работаешь вне потока обработчика;
- нужен более низкоуровневый контроль;
- нужно работать с uploads/downloads напрямую.
@@ -100,7 +100,7 @@ _, err := uploader.SendPhoto(tgapi.UploadPhoto{
## Что читать дальше
- [[MsgContext-RU]]
- [[MessageContext-RU]]
- [[Inline-Keyboards-and-Payloads-RU]]
- [[Rate-Limiting-RU]]
- [[tgapi-Overview]]
+6 -6
@@ -2,7 +2,7 @@
Russian version: [[tgapi-Overview-RU]]
`tgapi` is the low-level Telegram Bot API layer used under Laniakeas higher-level bot runtime. Use it when you need direct access to Telegram methods, explicit parameter structs, upload control, or raw request building that sits below plugins and `MsgContext` helpers.
`tgapi` is the low-level Telegram Bot API layer used under Laniakeas higher-level bot runtime. Use it when you need direct access to Telegram methods, explicit parameter structs, upload control, or raw request building that sits below plugins and `MessageContext` helpers.
## The important split first
@@ -20,7 +20,7 @@ If you stay on the typed method surface, you usually do not need to think about
## The normal layering
In practice, Laniakea has three levels:
- high-level handler helpers on `MsgContext`;
- high-level handler helpers on `MessageContext`;
- runtime structure on `Bot`, plugins, and middleware;
- low-level Telegram access in `tgapi`.
@@ -29,7 +29,7 @@ In practice, Laniakea has three levels:
## When to use `tgapi` directly
Use `tgapi` directly when:
- a `MsgContext` helper does not expose the Telegram feature you need;
- a `MessageContext` helper does not expose the Telegram feature you need;
- you need a Telegram method outside the high-level command/payload flow;
- you want explicit control over params, parse modes, message edits, or uploads;
- you are writing infrastructure code rather than command logic.
@@ -43,7 +43,7 @@ Typical examples where `tgapi` is the better tool:
- setting bot metadata or command scopes directly;
- file downloads and streaming;
- multipart uploads;
- one-off Telegram methods that do not have a `MsgContext` wrapper.
- one-off Telegram methods that do not have a `MessageContext` wrapper.
## Typed methods first
@@ -244,7 +244,7 @@ Related page:
As a rule of thumb:
- use `MsgContext` when responding to the current update;
- use `MessageContext` when responding to the current update;
- use `Bot` and plugins when structuring runtime behavior;
- use `tgapi` when you need direct Telegram method control;
- use raw `NewRequest` or `NewUploaderRequest` only as the final fallback.
@@ -254,7 +254,7 @@ That boundary keeps normal bot code ergonomic without hiding Telegram-specific c
## Related pages
- [[Getting-Started]] for the normal high-level bot setup path.
- [[MsgContext]] for handler-time reply helpers.
- [[MessageContext]] for handler-time reply helpers.
- [[Inline-Keyboards-and-Payloads]] for callback button construction.
- [[Bot-Lifecycle]] for runtime startup and shutdown responsibilities around `Bot`, `API`, and `Uploader`.
- [[Rate-Limiting]] for limiter and `retry_after` behavior.