REPOSITORY / ScuroNeko/Laniakea
Wiki
Expand and organize Laniakea wiki
Add pages for bot configuration, auto-generated commands, runners, errors, logging, and testing Fill core docs for lifecycle, middleware, drafts, localization, rate limiting, payloads, and tgapi Rework Home and Page-Priority to reflect a mostly complete, maintainable wiki structure
+1
@@ -37,6 +37,7 @@ It should complement the main `README.md` rather than duplicate it line-by-line.
|
|||||||
- When adding a new page, update `Home.md` if the page is user-relevant.
|
- When adding a new page, update `Home.md` if the page is user-relevant.
|
||||||
- Prefer editing existing pages over creating overlapping pages.
|
- Prefer editing existing pages over creating overlapping pages.
|
||||||
- Keep terminology aligned with the code and README.
|
- Keep terminology aligned with the code and README.
|
||||||
|
- Wiki-only changes do not require updating the main repository `CHANGELOG.md`.
|
||||||
|
|
||||||
## Commit discipline
|
## Commit discipline
|
||||||
- Wiki commit messages should follow the same repository rule:
|
- Wiki commit messages should follow the same repository rule:
|
||||||
|
|||||||
@@ -0,0 +1,188 @@
|
|||||||
|
# Auto-Generated Commands
|
||||||
|
|
||||||
|
This page explains how Laniakea derives Telegram command metadata from registered plugins and publishes it through the Bot API. It covers `AutoGenerateCommands(...)`, scope-specific registration, skip controls, and the command-name rules Telegram enforces.
|
||||||
|
|
||||||
|
## What auto-generation does
|
||||||
|
|
||||||
|
Laniakea can scan registered plugins, collect eligible commands, and publish them through Telegram's `setMyCommands` API.
|
||||||
|
|
||||||
|
This is useful when you want Telegram clients to show:
|
||||||
|
- command lists;
|
||||||
|
- slash-command suggestions;
|
||||||
|
- per-scope command metadata.
|
||||||
|
|
||||||
|
The feature works from command definitions you already registered in plugins, so you do not need to maintain a second separate command list by hand.
|
||||||
|
|
||||||
|
## Entry points
|
||||||
|
|
||||||
|
There are two main methods:
|
||||||
|
- `AutoGenerateCommands()`
|
||||||
|
- `AutoGenerateCommandsForScope(scope)`
|
||||||
|
|
||||||
|
Use `AutoGenerateCommands()` when you want the same generated commands across the default built-in scopes used by the library.
|
||||||
|
|
||||||
|
Use `AutoGenerateCommandsForScope(...)` when you want to manage one explicit scope yourself.
|
||||||
|
|
||||||
|
## What `AutoGenerateCommands()` publishes
|
||||||
|
|
||||||
|
`AutoGenerateCommands()`:
|
||||||
|
- gathers eligible commands from registered plugins;
|
||||||
|
- validates the total command count;
|
||||||
|
- deletes existing Telegram commands;
|
||||||
|
- registers the new command set for three scopes:
|
||||||
|
- private chats;
|
||||||
|
- group chats;
|
||||||
|
- all chat administrators.
|
||||||
|
|
||||||
|
This gives you a simple one-call setup for common bots.
|
||||||
|
|
||||||
|
## What `AutoGenerateCommandsForScope(...)` publishes
|
||||||
|
|
||||||
|
`AutoGenerateCommandsForScope(scope)` does the same gathering and validation, but only for the specific scope you pass in.
|
||||||
|
|
||||||
|
Use it when:
|
||||||
|
- you want different commands for different audiences;
|
||||||
|
- you want to manage scopes one by one;
|
||||||
|
- you are integrating command registration into a custom deployment or setup flow.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```go
|
||||||
|
scope := &tgapi.BotCommandScope{Type: tgapi.BotCommandScopePrivateType}
|
||||||
|
if err := bot.AutoGenerateCommandsForScope(scope); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Where generated commands come from
|
||||||
|
|
||||||
|
The generator walks registered plugins and collects commands from them.
|
||||||
|
|
||||||
|
A command is included only if:
|
||||||
|
- the plugin itself is not marked with `SkipCommandAutoGen()`;
|
||||||
|
- the command itself is not marked with `SkipCommandAutoGen()`;
|
||||||
|
- the command name matches Telegram's allowed format.
|
||||||
|
|
||||||
|
Payload handlers are not part of auto-generated slash commands. This feature only targets actual command registrations.
|
||||||
|
|
||||||
|
## Command descriptions
|
||||||
|
|
||||||
|
Generated command descriptions are built from:
|
||||||
|
- the command description, if one is set;
|
||||||
|
- the command arguments, rendered into usage text.
|
||||||
|
|
||||||
|
If a command has a description and arguments, the generated result looks like:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Short description. Usage: /command <required> [optional]
|
||||||
|
```
|
||||||
|
|
||||||
|
If no description is set, the generated text still includes a usage line.
|
||||||
|
|
||||||
|
That means commands are more useful in Telegram menus when you provide:
|
||||||
|
- `SetDescription(...)`;
|
||||||
|
- meaningful `CommandArg` names.
|
||||||
|
|
||||||
|
## Telegram command name rules
|
||||||
|
|
||||||
|
Laniakea validates command names against Telegram's command-registration rules before publishing.
|
||||||
|
|
||||||
|
The allowed pattern is:
|
||||||
|
- lowercase letters `a-z`
|
||||||
|
- digits `0-9`
|
||||||
|
- underscore `_`
|
||||||
|
- length `1..32`
|
||||||
|
|
||||||
|
Commands that do not match that pattern are skipped during auto-generation.
|
||||||
|
|
||||||
|
Important nuance:
|
||||||
|
- a command can still exist in your internal routing logic with a name that is not suitable for Telegram command menus;
|
||||||
|
- it just will not be exported through auto-generation.
|
||||||
|
|
||||||
|
## Command count limit
|
||||||
|
|
||||||
|
Telegram limits the number of published bot commands to `100`.
|
||||||
|
|
||||||
|
Laniakea checks this before making API calls. If the generated command set is larger than `100`, auto-generation returns `ErrTooManyCommands`.
|
||||||
|
|
||||||
|
This early validation is helpful because it fails before any delete or set command request is sent.
|
||||||
|
|
||||||
|
## Skip controls
|
||||||
|
|
||||||
|
There are two skip levels:
|
||||||
|
|
||||||
|
### Skip a single command
|
||||||
|
|
||||||
|
```go
|
||||||
|
plugin.NewCommand(exec, "internal").
|
||||||
|
SetDescription("Internal only").
|
||||||
|
SkipCommandAutoGen()
|
||||||
|
```
|
||||||
|
|
||||||
|
Use this when a command should remain callable but should not appear in Telegram's published menu.
|
||||||
|
|
||||||
|
### Skip an entire plugin
|
||||||
|
|
||||||
|
```go
|
||||||
|
plugin.SkipCommandAutoGen()
|
||||||
|
```
|
||||||
|
|
||||||
|
Use this when all commands in the plugin are internal, temporary, admin-only, or otherwise not meant for global command publication.
|
||||||
|
|
||||||
|
## Ordering behavior
|
||||||
|
|
||||||
|
Within a plugin, commands are gathered in sorted order by command name.
|
||||||
|
|
||||||
|
This makes published command lists deterministic, which is useful for:
|
||||||
|
- predictable diffs and tests;
|
||||||
|
- stable behavior across runs;
|
||||||
|
- easier reasoning when command sets grow.
|
||||||
|
|
||||||
|
## Typical usage
|
||||||
|
|
||||||
|
Call auto-generation after:
|
||||||
|
- creating the bot;
|
||||||
|
- registering all plugins;
|
||||||
|
- finalizing command descriptions and argument definitions.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```go
|
||||||
|
bot.AddPlugins(mainPlugin, adminPlugin)
|
||||||
|
|
||||||
|
if err := bot.AutoGenerateCommands(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If plugin configuration changes after registration, remember that plugin state is snapshotted at `AddPlugins(...)`, so finish command setup before registering plugins with the bot.
|
||||||
|
|
||||||
|
## Good command metadata practices
|
||||||
|
|
||||||
|
To make generated commands useful in Telegram clients:
|
||||||
|
- keep command names short and stable;
|
||||||
|
- provide `SetDescription(...)` for user-facing commands;
|
||||||
|
- give `CommandArg` values readable names such as `user`, `count`, or `reason`;
|
||||||
|
- hide internal commands with `SkipCommandAutoGen()`.
|
||||||
|
|
||||||
|
## Common reasons to use scope-specific registration
|
||||||
|
|
||||||
|
`AutoGenerateCommandsForScope(...)` is especially useful when:
|
||||||
|
- private chats should expose a richer command set than groups;
|
||||||
|
- admin commands should only be visible to administrators;
|
||||||
|
- you want a staged rollout of command menus.
|
||||||
|
|
||||||
|
In those cases, manage each relevant scope explicitly instead of using the all-scopes helper.
|
||||||
|
|
||||||
|
## Caveats
|
||||||
|
|
||||||
|
- Auto-generation talks to Telegram and can return API errors.
|
||||||
|
- It deletes existing commands in the target scope before setting the new list.
|
||||||
|
- Only commands, not payload handlers, participate.
|
||||||
|
- Invalid command names are skipped rather than forcefully normalized.
|
||||||
|
|
||||||
|
## Related pages
|
||||||
|
|
||||||
|
- [[Commands-and-Plugins]]
|
||||||
|
- [[Bot-Options-and-Configuration]]
|
||||||
|
- [[Migration]]
|
||||||
+185
-6
@@ -1,9 +1,188 @@
|
|||||||
# Bot Lifecycle
|
# Bot Lifecycle
|
||||||
|
|
||||||
This page explains how a bot is configured, started, stopped, and why a `Bot` instance is single-use.
|
This page explains how a `Bot` is created, configured, started, stopped, and retired. The important rule is that a `Bot` instance is single-use: configure it fully, run it once, then create a new instance for the next session.
|
||||||
|
|
||||||
## This page should cover
|
## Lifecycle at a glance
|
||||||
- `BotOpts`, defaults, and startup validation;
|
1. Build `BotOpts` and call `NewBot`.
|
||||||
- `Run`, `RunWithContext`, `Close`, and `CloseRemote`;
|
2. Configure the bot instance: prefixes, plugins, middleware, runners, localization, payload defaults, and optional database context.
|
||||||
- workers, polling, and shutdown behavior;
|
3. Start it with `RunWithContext(ctx)` or `Run()`.
|
||||||
- which configuration must be finalized before `Run`.
|
4. Cancel the context or let `Run()` return.
|
||||||
|
5. Call `Close()` to release local resources.
|
||||||
|
6. Create a new `Bot` if you need to run again.
|
||||||
|
|
||||||
|
## Creating a bot
|
||||||
|
|
||||||
|
`NewBot[T](opts)` is the construction step. It validates the required options, creates the internal Telegram API client and uploader, initializes loggers, prepares a default draft provider, and calls `getMe` to verify the token and fetch the bot username.
|
||||||
|
|
||||||
|
Construction fails immediately when:
|
||||||
|
- `opts` is `nil`;
|
||||||
|
- `opts.Token` is empty;
|
||||||
|
- Telegram rejects the token or `getMe` fails during startup.
|
||||||
|
|
||||||
|
Useful defaults applied during construction:
|
||||||
|
- command prefixes default to `"/"` when `BotOpts.Prefixes` is empty;
|
||||||
|
- the default callback payload encoding is Base64;
|
||||||
|
- `MaxWorkers` defaults to `32` when not set;
|
||||||
|
- the default error template is `"%s"`;
|
||||||
|
- a random draft provider is installed unless you replace it.
|
||||||
|
|
||||||
|
## Configuration before `Run`
|
||||||
|
|
||||||
|
The normal pattern is to finish all structural configuration before starting the bot:
|
||||||
|
|
||||||
|
- `DatabaseContext(...)` injects your shared dependency container or database handle.
|
||||||
|
- `AddPrefixes(...)` extends the accepted command prefixes.
|
||||||
|
- `SetPayloadType(...)` and `SetStrictPayloadType(...)` control callback payload decoding defaults.
|
||||||
|
- `AddPlugins(...)` registers command, payload, and update handlers.
|
||||||
|
- `AddMiddleware(...)` adds bot-level middleware, sorted by order and then by name.
|
||||||
|
- `AddRunner(...)` registers background or one-time tasks.
|
||||||
|
- `AddL10n(...)` replaces the localization provider.
|
||||||
|
- `SetDraftProvider(...)` replaces the default draft ID strategy.
|
||||||
|
- `ErrorTemplate(...)` and `Debug(...)` adjust runtime behavior and logging.
|
||||||
|
|
||||||
|
For an overview of handlers and plugins, see [[Commands-and-Plugins]]. For context helpers available inside handlers, see [[MsgContext]].
|
||||||
|
|
||||||
|
## `AddPlugins` is a configuration commit point
|
||||||
|
|
||||||
|
`Bot.AddPlugins(...)` clones the plugin configuration into the bot. That means the original `*Plugin` should be treated as finished before registration.
|
||||||
|
|
||||||
|
In practice:
|
||||||
|
- add commands, payloads, update handlers, middleware, logger, and `OnClose` callbacks before `AddPlugins`;
|
||||||
|
- do not rely on mutating the original plugin after registration;
|
||||||
|
- later edits to the original plugin may not appear in the bot, because the bot keeps its own internal snapshot.
|
||||||
|
|
||||||
|
This is especially important for:
|
||||||
|
- command descriptions used by auto-generated command metadata;
|
||||||
|
- plugin middleware;
|
||||||
|
- custom plugin loggers;
|
||||||
|
- plugin shutdown hooks.
|
||||||
|
|
||||||
|
## Minimal startup pattern
|
||||||
|
|
||||||
|
```go
|
||||||
|
opts := &laniakea.BotOpts{Token: "TOKEN"}
|
||||||
|
|
||||||
|
bot, err := laniakea.NewBot[laniakea.NoDB](opts)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer bot.Close()
|
||||||
|
|
||||||
|
plugin := laniakea.NewPlugin[laniakea.NoDB]("main")
|
||||||
|
plugin.NewCommand(func(ctx *laniakea.MsgContext, db laniakea.NoDB) error {
|
||||||
|
ctx.Answer("pong")
|
||||||
|
return nil
|
||||||
|
}, "ping")
|
||||||
|
|
||||||
|
bot.AddPlugins(plugin)
|
||||||
|
|
||||||
|
if err := bot.Run(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## `RunWithContext` versus `Run`
|
||||||
|
|
||||||
|
`RunWithContext(ctx)` is the main runtime entry point.
|
||||||
|
|
||||||
|
It:
|
||||||
|
- rejects startup when no prefixes are defined;
|
||||||
|
- rejects startup when no plugins are registered;
|
||||||
|
- rejects a second start with `ErrBotAlreadyRun`;
|
||||||
|
- starts registered runners;
|
||||||
|
- begins long polling with `getUpdates`;
|
||||||
|
- dispatches updates through a worker pool;
|
||||||
|
- stops polling when the context is canceled.
|
||||||
|
|
||||||
|
`Run()` is only a shorthand for `RunWithContext(context.Background())`.
|
||||||
|
|
||||||
|
Use `RunWithContext` for production services so you can stop the bot gracefully on `SIGINT` or `SIGTERM`.
|
||||||
|
|
||||||
|
## What happens during runtime
|
||||||
|
|
||||||
|
After startup, the bot runs three main pieces of work:
|
||||||
|
|
||||||
|
- a polling loop fetches updates from Telegram;
|
||||||
|
- an internal queue buffers updates before processing;
|
||||||
|
- a worker pool handles updates concurrently.
|
||||||
|
|
||||||
|
Behavior worth knowing:
|
||||||
|
- polling retry uses exponential backoff, capped at 30 seconds, when `getUpdates` keeps failing;
|
||||||
|
- the worker pool size is controlled by `BotOpts.MaxWorkers`;
|
||||||
|
- when the shutdown context is canceled, polling stops first, then queued work is drained, then runners are awaited.
|
||||||
|
|
||||||
|
## Runners and shutdown semantics
|
||||||
|
|
||||||
|
Runners start from `RunWithContext`, not from `NewBot`.
|
||||||
|
|
||||||
|
Supported runner modes:
|
||||||
|
- one-time async runners start in their own goroutine;
|
||||||
|
- one-time sync runners block startup until they finish;
|
||||||
|
- repeating async runners run on a ticker until the bot context is canceled.
|
||||||
|
|
||||||
|
Important caveats:
|
||||||
|
- repeating synchronous runners are treated as invalid and skipped;
|
||||||
|
- repeating async runners without a timeout are skipped with a warning;
|
||||||
|
- one-time synchronous runners that take more than two seconds log a warning because they delay startup.
|
||||||
|
|
||||||
|
## Graceful shutdown
|
||||||
|
|
||||||
|
Canceling the `RunWithContext` context tells the bot to stop polling and finish work already accepted into the queue. `RunWithContext` then waits for:
|
||||||
|
|
||||||
|
- all queued update handlers to finish;
|
||||||
|
- one-time async runners to finish;
|
||||||
|
- background runners to exit after noticing `ctx.Done()`.
|
||||||
|
|
||||||
|
`RunWithContext` does not automatically release API, uploader, or logger resources. You still need to call `Close()`.
|
||||||
|
|
||||||
|
## `Close()` versus `CloseRemote()`
|
||||||
|
|
||||||
|
`Close()` releases local process resources owned by the bot. It closes, in order:
|
||||||
|
- registered plugins via `Plugin.Close()`;
|
||||||
|
- the uploader;
|
||||||
|
- the local API client internals;
|
||||||
|
- the optional request logger;
|
||||||
|
- the main bot logger.
|
||||||
|
|
||||||
|
Call `Close()` once you are done with the bot instance, usually with `defer` right after successful construction.
|
||||||
|
|
||||||
|
`CloseRemote(ctx)` is different. It sends Telegram's Bot API `close` request for the remote session. It does not replace `Close()`, and `Close()` does not replace it.
|
||||||
|
|
||||||
|
Use `CloseRemote(ctx)` only when you specifically need Telegram-side session shutdown behavior. Most bots only need `Close()`.
|
||||||
|
|
||||||
|
## Single-use rule
|
||||||
|
|
||||||
|
A `Bot` cannot be started twice. After `Run()` or `RunWithContext()` returns, later start attempts fail with `ErrBotAlreadyRun`.
|
||||||
|
|
||||||
|
That means:
|
||||||
|
- do not call `Run()` again after a graceful stop;
|
||||||
|
- do not reuse a bot instance across tests that start it;
|
||||||
|
- create a fresh `Bot` for every new run cycle.
|
||||||
|
|
||||||
|
## Recommended production pattern
|
||||||
|
|
||||||
|
```go
|
||||||
|
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||||
|
defer stop()
|
||||||
|
|
||||||
|
bot, err := laniakea.NewBot[*sql.DB](opts)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer bot.Close()
|
||||||
|
|
||||||
|
bot.DatabaseContext(db)
|
||||||
|
bot.AddPlugins(plugin)
|
||||||
|
|
||||||
|
if err := bot.RunWithContext(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common mistakes
|
||||||
|
|
||||||
|
- Mutating a plugin after `AddPlugins(...)` and expecting the bot to see the change.
|
||||||
|
- Calling `Run()` twice on the same bot instance.
|
||||||
|
- Using `Run()` in long-lived services where graceful shutdown matters.
|
||||||
|
- Forgetting to call `Close()` after `RunWithContext(...)` returns.
|
||||||
|
- Registering background runners without a timeout.
|
||||||
|
|||||||
@@ -0,0 +1,334 @@
|
|||||||
|
# Bot Options and Configuration
|
||||||
|
|
||||||
|
This page explains how to configure a bot before calling `NewBot(...)`. It focuses on `BotOpts`, environment-based configuration, and the practical meaning of the most important knobs.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
`BotOpts` is the construction-time configuration object for `Bot`.
|
||||||
|
|
||||||
|
Use it when you want to control:
|
||||||
|
- the Telegram token and API endpoint;
|
||||||
|
- update types and command prefixes;
|
||||||
|
- logging behavior;
|
||||||
|
- request throttling behavior;
|
||||||
|
- payload decoding strictness;
|
||||||
|
- worker pool size.
|
||||||
|
|
||||||
|
The normal flow is:
|
||||||
|
|
||||||
|
1. create `BotOpts` manually or via `LoadOptsFromEnv()`;
|
||||||
|
2. optionally refine it with setter methods;
|
||||||
|
3. pass it to `NewBot(...)`.
|
||||||
|
|
||||||
|
For runtime configuration after bot creation, see [[Bot-Lifecycle]].
|
||||||
|
|
||||||
|
## Two ways to build `BotOpts`
|
||||||
|
|
||||||
|
### Manual configuration
|
||||||
|
|
||||||
|
Use manual construction when settings are mostly static in code.
|
||||||
|
|
||||||
|
```go
|
||||||
|
opts := (&laniakea.BotOpts{}).
|
||||||
|
SetToken("TOKEN").
|
||||||
|
SetPrefixes("/", "!").
|
||||||
|
SetRateLimit(30).
|
||||||
|
SetMaxWorkers(32)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Environment-based configuration
|
||||||
|
|
||||||
|
Use `LoadOptsFromEnv()` when your deployment environment should provide the values.
|
||||||
|
|
||||||
|
```go
|
||||||
|
opts := laniakea.LoadOptsFromEnv()
|
||||||
|
|
||||||
|
bot, err := laniakea.NewBot[*App](opts)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This is often the easiest approach for containers, CI, and production services.
|
||||||
|
|
||||||
|
## Required setting
|
||||||
|
|
||||||
|
Only one field is strictly required:
|
||||||
|
- `Token`
|
||||||
|
|
||||||
|
If `Token` is empty, `NewBot(...)` returns `ErrTokenRequired`.
|
||||||
|
|
||||||
|
## Defaults worth knowing
|
||||||
|
|
||||||
|
Some important defaults are applied either by `BotOpts` loading helpers or by `NewBot(...)` itself:
|
||||||
|
|
||||||
|
- `Prefixes` defaults to `["/"]`
|
||||||
|
- `RateLimit` defaults to `30`
|
||||||
|
- `MaxWorkers` defaults to `32`
|
||||||
|
- `ErrorTemplate` falls back to `"%s"` in the bot when unset
|
||||||
|
- request logging and file logging are off by default
|
||||||
|
- strict payload decoding is off by default
|
||||||
|
|
||||||
|
That means a very small config can still produce a usable bot.
|
||||||
|
|
||||||
|
## Core fields
|
||||||
|
|
||||||
|
### `Token`
|
||||||
|
|
||||||
|
Telegram bot token. Required.
|
||||||
|
|
||||||
|
Setter:
|
||||||
|
- `SetToken(token)`
|
||||||
|
|
||||||
|
Env:
|
||||||
|
- `TG_TOKEN`
|
||||||
|
|
||||||
|
### `UpdateTypes`
|
||||||
|
|
||||||
|
Controls which update types the bot requests from Telegram.
|
||||||
|
|
||||||
|
If empty, Telegram may return all update types. Restricting this list can reduce noise and unnecessary processing when your bot only needs a subset.
|
||||||
|
|
||||||
|
Setter:
|
||||||
|
- `SetUpdateTypes(types...)`
|
||||||
|
|
||||||
|
Env:
|
||||||
|
- `UPDATE_TYPES`, semicolon-separated
|
||||||
|
|
||||||
|
Related page:
|
||||||
|
- [[Commands-and-Plugins]]
|
||||||
|
|
||||||
|
### `Prefixes`
|
||||||
|
|
||||||
|
Defines command prefixes such as `/` or `!`.
|
||||||
|
|
||||||
|
Setter:
|
||||||
|
- `SetPrefixes(prefixes...)`
|
||||||
|
|
||||||
|
Env:
|
||||||
|
- `PREFIXES`, semicolon-separated
|
||||||
|
|
||||||
|
If unset, the default is `/`.
|
||||||
|
|
||||||
|
### `ErrorTemplate`
|
||||||
|
|
||||||
|
Controls how returned handler errors are presented to users.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```go
|
||||||
|
opts.SetErrorTemplate("Error\n\n%s")
|
||||||
|
```
|
||||||
|
|
||||||
|
Setter:
|
||||||
|
- `SetErrorTemplate(template)`
|
||||||
|
|
||||||
|
Env:
|
||||||
|
- `ERROR_TEMPLATE`
|
||||||
|
|
||||||
|
Related page:
|
||||||
|
- [[FAQ]]
|
||||||
|
|
||||||
|
## Logging configuration
|
||||||
|
|
||||||
|
### `Debug`
|
||||||
|
|
||||||
|
Enables debug-level logging.
|
||||||
|
|
||||||
|
Setter:
|
||||||
|
- `SetDebug(true)`
|
||||||
|
|
||||||
|
Env:
|
||||||
|
- `DEBUG=true`
|
||||||
|
|
||||||
|
### `UseRequestLogger`
|
||||||
|
|
||||||
|
Enables request-level logging for Telegram API traffic.
|
||||||
|
|
||||||
|
Setter:
|
||||||
|
- `SetUseRequestLogger(true)`
|
||||||
|
|
||||||
|
Env:
|
||||||
|
- `USE_REQ_LOG=true`
|
||||||
|
|
||||||
|
### `WriteToFile`
|
||||||
|
|
||||||
|
Enables file logging for bot and request logs.
|
||||||
|
|
||||||
|
Setter:
|
||||||
|
- `SetWriteToFile(true)`
|
||||||
|
|
||||||
|
Env:
|
||||||
|
- `WRITE_TO_FILE=true`
|
||||||
|
|
||||||
|
### `LoggerBasePath`
|
||||||
|
|
||||||
|
Controls where log files are written when file logging is enabled.
|
||||||
|
|
||||||
|
Setter:
|
||||||
|
- `SetLoggerBasePath(path)`
|
||||||
|
|
||||||
|
Env:
|
||||||
|
- `LOGGER_BASE_PATH`
|
||||||
|
|
||||||
|
If unset, it defaults to `./`.
|
||||||
|
|
||||||
|
## API endpoint configuration
|
||||||
|
|
||||||
|
### `UseTestServer`
|
||||||
|
|
||||||
|
Routes requests to Telegram's test server mode.
|
||||||
|
|
||||||
|
Setter:
|
||||||
|
- `SetUseTestServer(true)`
|
||||||
|
|
||||||
|
Env:
|
||||||
|
- `USE_TEST_SERVER=true`
|
||||||
|
|
||||||
|
Use this only for development and testing scenarios that explicitly target Telegram's test environment.
|
||||||
|
|
||||||
|
### `APIUrl`
|
||||||
|
|
||||||
|
Overrides the default Telegram API base URL.
|
||||||
|
|
||||||
|
Setter:
|
||||||
|
- `SetAPIUrl(url)`
|
||||||
|
|
||||||
|
Env:
|
||||||
|
- `API_URL`
|
||||||
|
|
||||||
|
This is useful for:
|
||||||
|
- proxies;
|
||||||
|
- self-hosted gateways;
|
||||||
|
- test infrastructure.
|
||||||
|
|
||||||
|
## Rate limiting configuration
|
||||||
|
|
||||||
|
### `RateLimit`
|
||||||
|
|
||||||
|
Controls the global request-per-second limiter used by the bot's Telegram API client.
|
||||||
|
|
||||||
|
Setter:
|
||||||
|
- `SetRateLimit(limit)`
|
||||||
|
|
||||||
|
Env:
|
||||||
|
- `RATE_LIMIT`
|
||||||
|
|
||||||
|
The default is `30`.
|
||||||
|
|
||||||
|
### `DropRLOverflow`
|
||||||
|
|
||||||
|
Switches the limiter into immediate rejection mode instead of waiting.
|
||||||
|
|
||||||
|
Setter:
|
||||||
|
- `SetDropRLOverflow(true)`
|
||||||
|
|
||||||
|
Env:
|
||||||
|
- `DROP_RL_OVERFLOW=true`
|
||||||
|
|
||||||
|
This is a policy decision:
|
||||||
|
- `false` favors reliable delivery;
|
||||||
|
- `true` favors responsiveness under overload.
|
||||||
|
|
||||||
|
Related page:
|
||||||
|
- [[Rate-Limiting]]
|
||||||
|
|
||||||
|
## Payload decoding configuration
|
||||||
|
|
||||||
|
### `StrictPayloadType`
|
||||||
|
|
||||||
|
Controls whether callback payload decoding accepts only the configured default payload type or tolerates other supported formats.
|
||||||
|
|
||||||
|
Setter:
|
||||||
|
- `SetStrictPayloadType(true)`
|
||||||
|
|
||||||
|
Env:
|
||||||
|
- `STRICT_PAYLOAD_TYPE=true`
|
||||||
|
|
||||||
|
Enable it when you want payload-type mismatches to fail loudly instead of being decoded permissively.
|
||||||
|
|
||||||
|
Related page:
|
||||||
|
- [[Inline-Keyboards-and-Payloads]]
|
||||||
|
|
||||||
|
## Concurrency configuration
|
||||||
|
|
||||||
|
### `MaxWorkers`
|
||||||
|
|
||||||
|
Controls the maximum number of update handlers that may run concurrently.
|
||||||
|
|
||||||
|
Setter:
|
||||||
|
- `SetMaxWorkers(workers)`
|
||||||
|
|
||||||
|
Env:
|
||||||
|
- `MAX_WORKERS`
|
||||||
|
|
||||||
|
Default:
|
||||||
|
- `32`
|
||||||
|
|
||||||
|
Higher values can help I/O-heavy bots, but they also increase concurrency against your own dependencies such as databases and external APIs.
|
||||||
|
|
||||||
|
Related page:
|
||||||
|
- [[Bot-Lifecycle]]
|
||||||
|
|
||||||
|
## Environment variables supported by `LoadOptsFromEnv()`
|
||||||
|
|
||||||
|
`LoadOptsFromEnv()` reads:
|
||||||
|
- `TG_TOKEN`
|
||||||
|
- `UPDATE_TYPES`
|
||||||
|
- `DEBUG`
|
||||||
|
- `ERROR_TEMPLATE`
|
||||||
|
- `PREFIXES`
|
||||||
|
- `LOGGER_BASE_PATH`
|
||||||
|
- `USE_REQ_LOG`
|
||||||
|
- `WRITE_TO_FILE`
|
||||||
|
- `USE_TEST_SERVER`
|
||||||
|
- `API_URL`
|
||||||
|
- `RATE_LIMIT`
|
||||||
|
- `DROP_RL_OVERFLOW`
|
||||||
|
- `STRICT_PAYLOAD_TYPE`
|
||||||
|
- `MAX_WORKERS`
|
||||||
|
|
||||||
|
List-valued fields use semicolon-separated values.
|
||||||
|
|
||||||
|
## Recommended configurations
|
||||||
|
|
||||||
|
### Minimal local setup
|
||||||
|
|
||||||
|
```go
|
||||||
|
opts := (&laniakea.BotOpts{}).
|
||||||
|
SetToken("TOKEN")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Typical production setup
|
||||||
|
|
||||||
|
```go
|
||||||
|
opts := (&laniakea.BotOpts{}).
|
||||||
|
SetToken(os.Getenv("TG_TOKEN")).
|
||||||
|
SetPrefixes("/").
|
||||||
|
SetRateLimit(30).
|
||||||
|
SetMaxWorkers(32).
|
||||||
|
SetErrorTemplate("Error\n\n%s")
|
||||||
|
```
|
||||||
|
|
||||||
|
### More defensive callback setup
|
||||||
|
|
||||||
|
```go
|
||||||
|
opts := (&laniakea.BotOpts{}).
|
||||||
|
SetToken(os.Getenv("TG_TOKEN")).
|
||||||
|
SetStrictPayloadType(true)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Recommendations
|
||||||
|
|
||||||
|
- Start with the defaults unless you already know your traffic pattern.
|
||||||
|
- Prefer `LoadOptsFromEnv()` for deployment-oriented apps.
|
||||||
|
- Set `StrictPayloadType` only when you are ready to enforce one payload encoding policy.
|
||||||
|
- Increase `MaxWorkers` carefully and based on actual handler workload.
|
||||||
|
- Turn on request logging selectively, because it is useful for debugging but noisy in normal operation.
|
||||||
|
|
||||||
|
## Related pages
|
||||||
|
|
||||||
|
- [[Bot-Lifecycle]]
|
||||||
|
- [[Rate-Limiting]]
|
||||||
|
- [[Inline-Keyboards-and-Payloads]]
|
||||||
|
- [[Auto-Generated-Commands]]
|
||||||
+173
-6
@@ -1,9 +1,176 @@
|
|||||||
# Drafts
|
# Drafts
|
||||||
|
|
||||||
Drafts provide a staged way to accumulate and then flush messages.
|
Drafts provide a staged way to accumulate message text and send it later as a final message. They are useful when you want to build a response incrementally instead of sending each intermediate state directly to the chat.
|
||||||
|
|
||||||
## This page should cover
|
## When drafts are useful
|
||||||
- `DraftProvider`, `Draft`, `Push`, and `Flush`;
|
|
||||||
- random vs linear draft IDs;
|
Drafts are a good fit when:
|
||||||
- validation and send-time behavior;
|
- you collect output in several steps and want to send only the final result;
|
||||||
- when drafts are better than direct replies.
|
- you want a stable draft identifier while building a message;
|
||||||
|
- you need explicit control over when the final message is published;
|
||||||
|
- you want to update Telegram-side draft state during message construction.
|
||||||
|
|
||||||
|
They are usually better than direct replies when your message is assembled progressively or may be canceled before final delivery.
|
||||||
|
|
||||||
|
## Main pieces
|
||||||
|
|
||||||
|
The draft system has two layers:
|
||||||
|
|
||||||
|
- `DraftProvider`, which owns drafts and generates their IDs;
|
||||||
|
- `Draft`, which holds one staged message.
|
||||||
|
|
||||||
|
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()`
|
||||||
|
|
||||||
|
Inside a handler, the usual entry point is `MsgContext.NewDraft()` or `MsgContext.NewDraftMarkdown()`.
|
||||||
|
|
||||||
|
Those helpers:
|
||||||
|
- create a draft from the bot's configured `DraftProvider`;
|
||||||
|
- automatically bind it to the current chat;
|
||||||
|
- preserve the current message thread when applicable.
|
||||||
|
|
||||||
|
Typical usage:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func report(ctx *laniakea.MsgContext, db *App) error {
|
||||||
|
draft := ctx.NewDraft()
|
||||||
|
if draft == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := draft.Push("Collecting data...\n"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := draft.Push("Building summary...\n"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return draft.Flush()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `NewDraftMarkdown()` when the final message should use `MarkdownV2`.
|
||||||
|
|
||||||
|
## Creating a provider manually
|
||||||
|
|
||||||
|
If you need explicit provider control, create one yourself and attach it to the bot with `SetDraftProvider(...)`.
|
||||||
|
|
||||||
|
Available constructors:
|
||||||
|
- `NewRandomDraftProvider(api)` for random IDs;
|
||||||
|
- `NewLinearDraftProvider(api, startValue)` for monotonic IDs.
|
||||||
|
|
||||||
|
Random IDs are the default choice. Linear IDs are useful when:
|
||||||
|
- you want predictable ordering in logs;
|
||||||
|
- you want to persist or resume draft IDs across restarts;
|
||||||
|
- you want deterministic behavior in tests or debugging.
|
||||||
|
|
||||||
|
## Draft lifecycle
|
||||||
|
|
||||||
|
A typical draft goes through these steps:
|
||||||
|
|
||||||
|
1. create the draft;
|
||||||
|
2. set the target chat if needed;
|
||||||
|
3. append text with `Push(...)`;
|
||||||
|
4. optionally inspect or clear it;
|
||||||
|
5. publish it with `Flush()` or discard it with `Delete()`.
|
||||||
|
|
||||||
|
Main APIs:
|
||||||
|
- `SetChat(chatID, messageThreadID)` overrides the target chat and thread;
|
||||||
|
- `SetEntities(...)` sets explicit message entities;
|
||||||
|
- `Push(text)` appends text and updates the server-side draft;
|
||||||
|
- `GetMessage()` returns the current accumulated text;
|
||||||
|
- `Clear()` empties local content;
|
||||||
|
- `Flush()` sends the final message and removes the draft from the provider;
|
||||||
|
- `Delete()` removes the draft without sending it.
|
||||||
|
|
||||||
|
## `Push(...)` versus `Flush()`
|
||||||
|
|
||||||
|
`Push(...)`:
|
||||||
|
- appends text to `Draft.Message`;
|
||||||
|
- validates the resulting text length;
|
||||||
|
- sends an update to the Telegram-side draft API;
|
||||||
|
- keeps the draft alive for later changes.
|
||||||
|
|
||||||
|
`Flush()`:
|
||||||
|
- validates the final message again;
|
||||||
|
- sends a normal final message with `SendMessage`;
|
||||||
|
- deletes the draft from the provider only on success;
|
||||||
|
- leaves the draft intact when sending fails so you can retry.
|
||||||
|
|
||||||
|
If the draft message is empty, `Flush()` returns `nil` and does not call the API.
|
||||||
|
|
||||||
|
## Validation behavior
|
||||||
|
|
||||||
|
Drafts now validate message size before sending invalid requests.
|
||||||
|
|
||||||
|
Important rules:
|
||||||
|
- a draft must have a non-zero chat ID before `Push(...)` or `Flush()`;
|
||||||
|
- oversized message text is rejected before sending;
|
||||||
|
- `Push(...)` updates the local `Message` field before returning the validation error.
|
||||||
|
|
||||||
|
That last detail matters: if a `Push(...)` call makes the draft too large, you still have the accumulated content available to inspect or adjust.
|
||||||
|
|
||||||
|
## Draft IDs
|
||||||
|
|
||||||
|
Each draft gets a provider-generated `ID`.
|
||||||
|
|
||||||
|
ID generation modes:
|
||||||
|
- random IDs from `RandomDraftIdGenerator`;
|
||||||
|
- monotonic IDs from `LinearDraftIdGenerator`.
|
||||||
|
|
||||||
|
The ID is mainly useful when:
|
||||||
|
- correlating draft activity in logs;
|
||||||
|
- storing or restoring draft metadata outside the process;
|
||||||
|
- addressing drafts through `DraftProvider.GetDraft(id)`.
|
||||||
|
|
||||||
|
## Managing drafts through the provider
|
||||||
|
|
||||||
|
The provider can also manage drafts directly:
|
||||||
|
|
||||||
|
- `NewDraft(parseMode)` creates a new draft;
|
||||||
|
- `GetDraft(id)` looks up an existing draft;
|
||||||
|
- `FlushAll()` tries to flush every pending draft.
|
||||||
|
|
||||||
|
`FlushAll()` is best-effort:
|
||||||
|
- it attempts all known drafts;
|
||||||
|
- it returns the first encountered error;
|
||||||
|
- successful drafts are still removed as they flush successfully.
|
||||||
|
|
||||||
|
This is useful for controlled shutdown or batch publishing flows.
|
||||||
|
|
||||||
|
## Entities and parse mode
|
||||||
|
|
||||||
|
Each draft stores:
|
||||||
|
- a parse mode;
|
||||||
|
- optional message entities;
|
||||||
|
- target chat and message thread metadata.
|
||||||
|
|
||||||
|
Two details are easy to miss:
|
||||||
|
- `SetEntities(...)` stores the slice by reference, so pass a copy if you plan to mutate your original slice later;
|
||||||
|
- `NewDraftMarkdown()` sets `MarkdownV2`, but the same escaping rules still apply to user input.
|
||||||
|
|
||||||
|
## When to prefer drafts over `Answer(...)`
|
||||||
|
|
||||||
|
Prefer drafts when:
|
||||||
|
- you are building a message in phases;
|
||||||
|
- intermediate state should not be visible as separate chat messages;
|
||||||
|
- you may want to cancel or discard the response before publication.
|
||||||
|
|
||||||
|
Prefer direct reply helpers such as `Answer(...)` or `AnswerLong(...)` when:
|
||||||
|
- you already have the final text;
|
||||||
|
- you want immediate delivery;
|
||||||
|
- there is no value in staging or revising the message first.
|
||||||
|
|
||||||
|
## Caveats
|
||||||
|
|
||||||
|
- A draft needs a valid target chat before it can be pushed or flushed.
|
||||||
|
- Draft providers are concurrency-safe; individual drafts are not automatically safe for concurrent mutation.
|
||||||
|
- `Delete()` removes the draft locally and clears its message, but does not send anything.
|
||||||
|
- Successful `Flush()` removes the draft from the provider.
|
||||||
|
- Failed `Flush()` keeps the draft so you can retry.
|
||||||
|
|
||||||
|
## Related pages
|
||||||
|
|
||||||
|
- [[MsgContext]] for handler-scoped reply and draft helpers
|
||||||
|
- [[Bot-Lifecycle]] for draft-provider attachment through the bot
|
||||||
|
|||||||
+205
@@ -0,0 +1,205 @@
|
|||||||
|
# Error Handling
|
||||||
|
|
||||||
|
Laniakea uses error-returning handlers so command, payload, and update-handler failures can flow through one consistent mechanism. This page explains when to return errors, when to answer manually, and how `ErrorTemplate(...)` affects what users see.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Handlers in Laniakea return `error`.
|
||||||
|
|
||||||
|
That applies to:
|
||||||
|
- command handlers;
|
||||||
|
- payload handlers;
|
||||||
|
- non-command update handlers registered with `AddUpdateHandler(...)`.
|
||||||
|
|
||||||
|
The basic pattern is:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func ping(ctx *laniakea.MsgContext, db *App) error {
|
||||||
|
ctx.Answer("pong")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
When a handler returns an error, the bot routes it through the context error helper instead of ignoring it.
|
||||||
|
|
||||||
|
## What happens when an error is returned
|
||||||
|
|
||||||
|
The context error flow does three things:
|
||||||
|
- formats the error text using the bot's `errorTemplate`;
|
||||||
|
- sends the formatted error back to the user;
|
||||||
|
- logs the original error with the current logger.
|
||||||
|
|
||||||
|
Behavior depends on the update kind:
|
||||||
|
- for callback queries, Laniakea answers the callback query with notification text;
|
||||||
|
- for message-driven flows, Laniakea sends a plain text reply.
|
||||||
|
|
||||||
|
## `ErrorTemplate(...)`
|
||||||
|
|
||||||
|
Use `Bot.ErrorTemplate(...)` or `BotOpts.ErrorTemplate` to control the user-facing error format.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```go
|
||||||
|
bot.ErrorTemplate("Error\n\n%s")
|
||||||
|
```
|
||||||
|
|
||||||
|
The `%s` placeholder is replaced with `err.Error()`.
|
||||||
|
|
||||||
|
If no explicit template is set, the bot uses `"%s"`.
|
||||||
|
|
||||||
|
## Returning an error versus replying manually
|
||||||
|
|
||||||
|
There are two valid styles.
|
||||||
|
|
||||||
|
### Return an error
|
||||||
|
|
||||||
|
Use this when:
|
||||||
|
- the failure should go through centralized formatting;
|
||||||
|
- you want consistent behavior across handlers;
|
||||||
|
- the error itself is already the right user-facing message.
|
||||||
|
|
||||||
|
```go
|
||||||
|
if !allowed {
|
||||||
|
return errors.New("access denied")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Reply manually and return `nil`
|
||||||
|
|
||||||
|
Use this when:
|
||||||
|
- you want full control over the response shape;
|
||||||
|
- you want to send a richer or more specific message than a generic error template;
|
||||||
|
- the situation is not really an exceptional failure.
|
||||||
|
|
||||||
|
```go
|
||||||
|
if !allowed {
|
||||||
|
ctx.Answer("Access denied")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Good places to return errors
|
||||||
|
|
||||||
|
Returning an error is a good fit for:
|
||||||
|
- failed validation that should be presented uniformly;
|
||||||
|
- database or API failures that should surface to the user;
|
||||||
|
- command execution failures where the user should see a standard error response.
|
||||||
|
|
||||||
|
It is especially useful when many handlers share the same style of failure reporting.
|
||||||
|
|
||||||
|
## Good places to avoid returning errors
|
||||||
|
|
||||||
|
Avoid returning errors when:
|
||||||
|
- you already produced the desired user-facing response;
|
||||||
|
- the condition is expected control flow rather than a failure;
|
||||||
|
- the action is best handled silently or via logging only.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
- permission gate that intentionally returns no output;
|
||||||
|
- callback interaction where you already answered with a custom alert;
|
||||||
|
- middleware-based rejection path handled separately.
|
||||||
|
|
||||||
|
## Callback-specific behavior
|
||||||
|
|
||||||
|
For callback-driven flows, returned errors become callback-query answers rather than regular chat messages.
|
||||||
|
|
||||||
|
This matters because callback error UX is different:
|
||||||
|
- the user sees a callback notification;
|
||||||
|
- no new chat message is posted for the error path.
|
||||||
|
|
||||||
|
If you want a different callback UX, answer manually with:
|
||||||
|
- `AnswerCbQuery()`
|
||||||
|
- `AnswerCbQueryText(...)`
|
||||||
|
- `AnswerCbQueryAlert(...)`
|
||||||
|
- `AnswerCbQueryUrl(...)`
|
||||||
|
|
||||||
|
and return `nil`.
|
||||||
|
|
||||||
|
## Validation and helper errors
|
||||||
|
|
||||||
|
Some failures never reach your handler because helpers validate earlier.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
- empty or oversized messages are rejected before Telegram API requests are sent;
|
||||||
|
- invalid command argument shapes can stop command execution before the handler body runs.
|
||||||
|
|
||||||
|
That means not every user-visible failure should be modeled as a returned handler error. Some are enforced by helpers and command plumbing before your business logic starts.
|
||||||
|
|
||||||
|
## Middleware and errors
|
||||||
|
|
||||||
|
Middleware does not return `error`; it returns `bool`.
|
||||||
|
|
||||||
|
So middleware has a different contract:
|
||||||
|
- `true` continues the chain;
|
||||||
|
- `false` stops the chain.
|
||||||
|
|
||||||
|
If middleware wants to show an error-like response, it should do so explicitly and then return `false`.
|
||||||
|
|
||||||
|
Related page:
|
||||||
|
- [[Middleware]]
|
||||||
|
|
||||||
|
## Logging behavior
|
||||||
|
|
||||||
|
Returned handler errors are logged through the current context logger.
|
||||||
|
|
||||||
|
Depending on the route, that may be:
|
||||||
|
- the plugin logger;
|
||||||
|
- or the bot logger if the plugin has no dedicated logger.
|
||||||
|
|
||||||
|
This is one reason returning errors is useful: it keeps operational visibility aligned with the same handler path that produced the user-facing failure.
|
||||||
|
|
||||||
|
Related page:
|
||||||
|
- [[Logging]]
|
||||||
|
|
||||||
|
## Practical patterns
|
||||||
|
|
||||||
|
### Centralized command failure
|
||||||
|
|
||||||
|
```go
|
||||||
|
plugin.NewCommand(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 {
|
||||||
|
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 {
|
||||||
|
if !ready {
|
||||||
|
ctx.AnswerCbQueryAlert("This action is not available yet")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}, "start")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Recommendations
|
||||||
|
|
||||||
|
- Return `error` when you want centralized error formatting and logging.
|
||||||
|
- Reply manually and return `nil` when the response is part of normal control flow.
|
||||||
|
- Keep user-facing error strings clear and action-oriented.
|
||||||
|
- Use one consistent style across a plugin when possible.
|
||||||
|
|
||||||
|
## Related pages
|
||||||
|
|
||||||
|
- [[FAQ]]
|
||||||
|
- [[Middleware]]
|
||||||
|
- [[Logging]]
|
||||||
|
- [[Commands-and-Plugins]]
|
||||||
+140
-7
@@ -1,10 +1,143 @@
|
|||||||
# FAQ
|
# FAQ
|
||||||
|
|
||||||
This page should answer the recurring design and usage questions around Laniakea.
|
This page collects recurring questions about why Laniakea behaves the way it does. It focuses on design choices that are easy to miss when you only look at examples.
|
||||||
|
|
||||||
## This page should cover
|
## Why do handlers return `error`?
|
||||||
- why handlers return `error`;
|
|
||||||
- why `Bot` is single-use;
|
Handlers return `error` so failure reporting can be centralized instead of being reimplemented in every command or payload callback.
|
||||||
- why `AnswerLong` is separate from `Answer`;
|
|
||||||
- why both JSON and Base64 payloads exist;
|
That gives you a few benefits:
|
||||||
- how to choose between high-level helpers and `tgapi`.
|
- command handlers can stay focused on business logic;
|
||||||
|
- bot-wide error presentation can be controlled through `ErrorTemplate(...)`;
|
||||||
|
- non-command update handlers and payload handlers follow the same contract;
|
||||||
|
- callers can distinguish normal completion from a failure without relying on side effects.
|
||||||
|
|
||||||
|
A typical handler now looks like this:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func ping(ctx *laniakea.MsgContext, db *App) error {
|
||||||
|
ctx.Answer("pong")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If you want to fully control the user-facing error reply yourself, you can still do that and return `nil`.
|
||||||
|
|
||||||
|
## Why is `Bot` single-use?
|
||||||
|
|
||||||
|
`Bot` is single-use because a run session owns real runtime state:
|
||||||
|
- polling lifecycle;
|
||||||
|
- worker pool lifecycle;
|
||||||
|
- update offsets;
|
||||||
|
- background runner execution;
|
||||||
|
- API and logger resources.
|
||||||
|
|
||||||
|
Allowing the same instance to be restarted would make shutdown and resource ownership much harder to reason about. The current model is simpler and safer:
|
||||||
|
|
||||||
|
1. construct a bot;
|
||||||
|
2. configure it;
|
||||||
|
3. run it once;
|
||||||
|
4. close it;
|
||||||
|
5. create a new bot for the next run.
|
||||||
|
|
||||||
|
See [[Bot-Lifecycle]] for the full runtime model.
|
||||||
|
|
||||||
|
## Why is `AnswerLong(...)` separate from `Answer(...)`?
|
||||||
|
|
||||||
|
`Answer(...)` intentionally keeps the simple one-message semantic. It validates that the text fits into a single Telegram message and does not silently change delivery shape.
|
||||||
|
|
||||||
|
`AnswerLong(...)` exists separately so long-message splitting is explicit.
|
||||||
|
|
||||||
|
That separation is useful because splitting changes behavior:
|
||||||
|
- the user receives multiple messages instead of one;
|
||||||
|
- partial success becomes possible if later chunks fail;
|
||||||
|
- inline keyboards can only be attached to one chunk, so `KeyboardLong(...)` attaches it only to the last part.
|
||||||
|
|
||||||
|
Keeping long-message behavior explicit makes handlers easier to reason about and avoids surprising message fan-out from otherwise simple helper calls.
|
||||||
|
|
||||||
|
## Why do both JSON and Base64 payload formats exist?
|
||||||
|
|
||||||
|
Laniakea supports both because they solve different problems.
|
||||||
|
|
||||||
|
`BotPayloadJson` is useful when:
|
||||||
|
- you want readable callback payloads during debugging;
|
||||||
|
- you value transparency over compactness;
|
||||||
|
- your payloads are already small.
|
||||||
|
|
||||||
|
`BotPayloadBase64` is useful when:
|
||||||
|
- you want a more compact transport form for JSON payload data;
|
||||||
|
- you want callback data to look opaque at a glance;
|
||||||
|
- you want the historical default used by the bot.
|
||||||
|
|
||||||
|
The important nuance is that the logical payload is the same structured callback data. Only the encoding changes.
|
||||||
|
|
||||||
|
Also note:
|
||||||
|
- the bot has a default payload type;
|
||||||
|
- an `InlineKeyboard` can override that default for one keyboard;
|
||||||
|
- strict payload mode can force decoding to accept only the configured default type.
|
||||||
|
|
||||||
|
See [[Inline-Keyboards-and-Payloads]] for the full callback model.
|
||||||
|
|
||||||
|
## Why can a keyboard override the bot payload type?
|
||||||
|
|
||||||
|
Because different UI surfaces sometimes have different needs.
|
||||||
|
|
||||||
|
Most bots want one default payload encoding across the project. But some keyboards are easier to inspect or integrate when encoded differently. Keyboard-local override keeps the global default simple while still allowing exceptions when they are actually useful.
|
||||||
|
|
||||||
|
## When should I use high-level `MsgContext` helpers instead of `tgapi` directly?
|
||||||
|
|
||||||
|
Use `MsgContext` helpers when you are working inside a handler and want the common reply/edit/delete path with the current chat, callback message, thread, and logger already wired in.
|
||||||
|
|
||||||
|
Use `tgapi` directly when:
|
||||||
|
- you need a Telegram method that has no `MsgContext` helper;
|
||||||
|
- you are working outside a handler context;
|
||||||
|
- you need lower-level request control;
|
||||||
|
- you are integrating with uploader or file APIs directly.
|
||||||
|
|
||||||
|
In practice:
|
||||||
|
- `MsgContext` is the ergonomic default for handler code;
|
||||||
|
- `tgapi` is the escape hatch and infrastructure layer.
|
||||||
|
|
||||||
|
See [[tgapi-Overview]] for the lower-level API surface.
|
||||||
|
|
||||||
|
## Why does `RunWithContext(...)` still require `Close()` afterward?
|
||||||
|
|
||||||
|
Because stopping the run loop and releasing owned resources are treated as two separate responsibilities.
|
||||||
|
|
||||||
|
`RunWithContext(...)` handles:
|
||||||
|
- polling lifecycle;
|
||||||
|
- graceful stop on `ctx.Done()`;
|
||||||
|
- draining queued work;
|
||||||
|
- waiting for runners to finish.
|
||||||
|
|
||||||
|
`Close()` handles:
|
||||||
|
- plugin shutdown hooks;
|
||||||
|
- uploader shutdown;
|
||||||
|
- API client cleanup;
|
||||||
|
- logger cleanup.
|
||||||
|
|
||||||
|
This separation keeps shutdown explicit and makes it easier to control lifetime in tests, services, and embedded applications.
|
||||||
|
|
||||||
|
## Why does plugin configuration need to be finished before `AddPlugins(...)`?
|
||||||
|
|
||||||
|
Because `AddPlugins(...)` snapshots plugin state into the bot. The bot should not depend on later mutation of the original `*Plugin`.
|
||||||
|
|
||||||
|
That avoids a whole class of surprising bugs where one part of the program mutates a plugin after another part already started relying on it.
|
||||||
|
|
||||||
|
Configure these before registration:
|
||||||
|
- commands;
|
||||||
|
- payloads;
|
||||||
|
- update handlers;
|
||||||
|
- plugin middleware;
|
||||||
|
- logger choice;
|
||||||
|
- `OnClose` callback.
|
||||||
|
|
||||||
|
## Why does async middleware ignore `false`?
|
||||||
|
|
||||||
|
Because async middleware is designed for side effects, not flow control.
|
||||||
|
|
||||||
|
Once middleware runs in its own goroutine, it cannot reliably stop the handler path that is already continuing. Laniakea makes that explicit: async middleware always lets execution continue and receives a copied `MsgContext`.
|
||||||
|
|
||||||
|
If you need to block or reject execution, use synchronous middleware.
|
||||||
|
|
||||||
|
See [[Middleware]] for details.
|
||||||
|
|||||||
+18
-12
@@ -4,27 +4,33 @@ Laniakea is a Go framework and Telegram Bot API wrapper built around plugins, ty
|
|||||||
|
|
||||||
Use this wiki as the structured companion to the README: start with setup, then move through commands, context, keyboards, and lower-level API usage.
|
Use this wiki as the structured companion to the README: start with setup, then move through commands, context, keyboards, and lower-level API usage.
|
||||||
|
|
||||||
Current fill plan: [[Page-Priority]]
|
|
||||||
|
|
||||||
## Start here
|
## Start here
|
||||||
- [[Getting-Started]]
|
- [[Getting-Started]]
|
||||||
|
- [[Bot-Options-and-Configuration]]
|
||||||
- [[Commands-and-Plugins]]
|
- [[Commands-and-Plugins]]
|
||||||
- [[MsgContext]]
|
- [[MsgContext]]
|
||||||
|
|
||||||
## Core API
|
## Bot Runtime
|
||||||
- [[Inline-Keyboards-and-Payloads]]
|
|
||||||
- [[tgapi-Overview]]
|
|
||||||
- [[Bot-Lifecycle]]
|
- [[Bot-Lifecycle]]
|
||||||
- [[Middleware]]
|
- [[Middleware]]
|
||||||
|
- [[Runners]]
|
||||||
|
- [[Error-Handling]]
|
||||||
|
- [[Logging]]
|
||||||
|
|
||||||
## Changes and troubleshooting
|
## Telegram API and Interaction
|
||||||
|
- [[Inline-Keyboards-and-Payloads]]
|
||||||
|
- [[tgapi-Overview]]
|
||||||
|
- [[Auto-Generated-Commands]]
|
||||||
|
- [[Rate-Limiting]]
|
||||||
|
- [[Drafts]]
|
||||||
|
|
||||||
|
## Practical Guides
|
||||||
|
- [[Recipes]]
|
||||||
|
- [[Localization]]
|
||||||
|
- [[Testing-Bots-with-Laniakea]]
|
||||||
|
|
||||||
|
## Migration and Maintenance
|
||||||
- [[Migration]]
|
- [[Migration]]
|
||||||
- [[FAQ]]
|
- [[FAQ]]
|
||||||
|
|
||||||
## Additional topics
|
|
||||||
- [[Drafts]]
|
|
||||||
- [[Localization]]
|
|
||||||
- [[Rate-Limiting]]
|
|
||||||
- [[Recipes]]
|
|
||||||
- [[Semver-and-Releases]]
|
- [[Semver-and-Releases]]
|
||||||
- [[Page-Priority]]
|
- [[Page-Priority]]
|
||||||
|
|||||||
+274
-7
@@ -1,10 +1,277 @@
|
|||||||
# Inline Keyboards and Payloads
|
# Inline Keyboards and Payloads
|
||||||
|
|
||||||
This page documents how Laniakea builds inline keyboards and encodes callback payloads.
|
Inline keyboards in Laniakea are built explicitly: you choose a row width, add URL or callback buttons, and decide how callback payloads are encoded. The high-level goal is simple: keep button construction ergonomic while keeping payload routing predictable in handlers.
|
||||||
|
|
||||||
## This page should cover
|
## The important model first
|
||||||
- `InlineKeyboard` builders and row layout;
|
|
||||||
- JSON vs Base64 callback payloads;
|
- `InlineKeyboard` builds Telegram inline keyboard markup row by row.
|
||||||
- bot default payload type vs keyboard-local override;
|
- callback buttons store a `CallbackData` payload with a command name and string arguments.
|
||||||
- strict payload mode and tolerant fallback behavior;
|
- payloads can be encoded as JSON or Base64.
|
||||||
- payload size and compatibility guidance.
|
- the bot has a default payload type, but each keyboard can override it locally.
|
||||||
|
- payload decoding can be tolerant or strict depending on bot configuration.
|
||||||
|
- callback payload handlers are normal plugin payload handlers, not a separate routing subsystem.
|
||||||
|
|
||||||
|
## Building a keyboard
|
||||||
|
|
||||||
|
The most direct constructors are:
|
||||||
|
- `laniakea.NewInlineKeyboardJson(maxRow)`
|
||||||
|
- `laniakea.NewInlineKeyboardBase64(maxRow)`
|
||||||
|
- `laniakea.NewInlineKeyboard(payloadType, maxRow)`
|
||||||
|
|
||||||
|
`maxRow` controls how many buttons fit into one row before the builder automatically starts the next row.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```go
|
||||||
|
kb := laniakea.NewInlineKeyboardJson(2).
|
||||||
|
AddCallbackButton("Open", "open", 42).
|
||||||
|
AddCallbackButton("Delete", "delete", 42).
|
||||||
|
AddUrlButton("Docs", "https://example.com/docs")
|
||||||
|
```
|
||||||
|
|
||||||
|
In that example:
|
||||||
|
- the first two buttons share the first row;
|
||||||
|
- the URL button starts the second row automatically.
|
||||||
|
|
||||||
|
Use `AddLine()` when you want to end the current row manually.
|
||||||
|
|
||||||
|
When the keyboard is ready, Laniakea usually converts it for you through helpers such as:
|
||||||
|
- `ctx.Keyboard(...)`
|
||||||
|
- `ctx.KeyboardLong(...)`
|
||||||
|
- `ctx.EditCallback(...)`
|
||||||
|
|
||||||
|
If you need the raw Telegram markup yourself, call `kb.Get()`.
|
||||||
|
|
||||||
|
## Row behavior
|
||||||
|
|
||||||
|
`maxRow` controls automatic wrapping.
|
||||||
|
|
||||||
|
Behavior:
|
||||||
|
- buttons are appended to the current row until it reaches `maxRow`;
|
||||||
|
- once full, the next button starts a new row automatically;
|
||||||
|
- `AddLine()` forces a row break early;
|
||||||
|
- `Get()` flushes the current unfinished row automatically.
|
||||||
|
|
||||||
|
This makes the builder easy to use in loops without manually managing row slices.
|
||||||
|
|
||||||
|
## Callback payload structure
|
||||||
|
|
||||||
|
Callback buttons carry a `CallbackData` value:
|
||||||
|
|
||||||
|
```go
|
||||||
|
type CallbackData struct {
|
||||||
|
Command string `json:"cmd"`
|
||||||
|
Args []string `json:"args"`
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
You normally do not build JSON by hand. Use:
|
||||||
|
- `AddCallbackButton(text, cmd, args...)`
|
||||||
|
- `AddCallbackButtonStyle(text, style, cmd, args...)`
|
||||||
|
- `NewCallbackData(cmd, args...)`
|
||||||
|
|
||||||
|
All payload arguments are converted with `fmt.Sprint`, so handlers receive strings regardless of whether you originally passed `int`, `bool`, or another basic value.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```go
|
||||||
|
kb := laniakea.NewInlineKeyboardJson(1).
|
||||||
|
AddCallbackButton("Ban", "ban_user", 12345, "spam")
|
||||||
|
```
|
||||||
|
|
||||||
|
That produces a payload equivalent to:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"cmd":"ban_user","args":["12345","spam"]}
|
||||||
|
```
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
## JSON vs Base64 payloads
|
||||||
|
|
||||||
|
Laniakea supports two payload encodings:
|
||||||
|
|
||||||
|
- `BotPayloadJson`
|
||||||
|
- `BotPayloadBase64`
|
||||||
|
|
||||||
|
JSON is easier to inspect in logs and tests.
|
||||||
|
Base64 is useful when you want a denser transport representation of the same JSON payload.
|
||||||
|
|
||||||
|
The encoding choice affects how callback data is serialized into the Telegram button. It does not change the logical payload shape seen by your handler after decoding.
|
||||||
|
|
||||||
|
## Callback payload size strategy
|
||||||
|
|
||||||
|
Even though Laniakea makes encoding ergonomic, callback data should still stay compact.
|
||||||
|
|
||||||
|
Good payloads usually contain:
|
||||||
|
- an action name;
|
||||||
|
- a small identifier;
|
||||||
|
- one or two short arguments.
|
||||||
|
|
||||||
|
Avoid putting large serialized state into callback data. A better pattern is:
|
||||||
|
- store compact identifiers in the payload;
|
||||||
|
- look up the full state on the server side.
|
||||||
|
|
||||||
|
## Bot default vs keyboard-local override
|
||||||
|
|
||||||
|
The bot has a default callback encoding:
|
||||||
|
|
||||||
|
```go
|
||||||
|
bot.SetPayloadType(laniakea.BotPayloadBase64)
|
||||||
|
```
|
||||||
|
|
||||||
|
That default is copied into `MsgContext`, so `ctx.NewInlineKeyboard(...)` starts with the bot’s current payload type.
|
||||||
|
|
||||||
|
For a single keyboard, you can override it locally:
|
||||||
|
|
||||||
|
```go
|
||||||
|
kb := ctx.NewInlineKeyboard(2).
|
||||||
|
SetPayloadType(laniakea.BotPayloadJson).
|
||||||
|
AddCallbackButton("Inspect", "inspect", 7)
|
||||||
|
```
|
||||||
|
|
||||||
|
Use a keyboard-local override when:
|
||||||
|
- one flow benefits from human-readable JSON during debugging;
|
||||||
|
- an older button set still needs a different encoding during migration;
|
||||||
|
- you want explicit control instead of relying on the bot-wide default.
|
||||||
|
|
||||||
|
## Strict vs tolerant decoding
|
||||||
|
|
||||||
|
When a callback arrives, the bot first tries to decode it using the bot’s configured payload type.
|
||||||
|
|
||||||
|
Default behavior is tolerant:
|
||||||
|
- if the configured type is Base64 and Base64 decoding fails, Laniakea falls back to JSON;
|
||||||
|
- if the configured type is JSON and JSON decoding fails, Laniakea falls back to Base64.
|
||||||
|
|
||||||
|
This is useful when:
|
||||||
|
- old buttons are still in chats after a payload-format change;
|
||||||
|
- different keyboards were generated with different local overrides;
|
||||||
|
- you want compatibility during rollout.
|
||||||
|
|
||||||
|
Strict mode disables that fallback:
|
||||||
|
|
||||||
|
```go
|
||||||
|
opts := (&laniakea.BotOpts{}).SetStrictPayloadType(true)
|
||||||
|
```
|
||||||
|
|
||||||
|
or:
|
||||||
|
|
||||||
|
```go
|
||||||
|
bot.SetStrictPayloadType(true)
|
||||||
|
```
|
||||||
|
|
||||||
|
In strict mode, a mismatched payload format returns `ErrPayloadTypeMismatch` instead of silently trying the other decoder.
|
||||||
|
|
||||||
|
Use strict mode when payload-format drift should be treated as a real bug rather than a migration convenience.
|
||||||
|
|
||||||
|
## Debug logging and payload decoding
|
||||||
|
|
||||||
|
When the bot is in debug mode and a callback is decoded from Base64, Laniakea logs the decoded JSON form for inspection.
|
||||||
|
|
||||||
|
That is useful when:
|
||||||
|
- you want readable payload traces in logs;
|
||||||
|
- the bot default is Base64 but you still want observability during development.
|
||||||
|
|
||||||
|
## Choosing an encoding
|
||||||
|
|
||||||
|
Use JSON when:
|
||||||
|
- you want debuggable callback payloads;
|
||||||
|
- payloads are already short;
|
||||||
|
- you are writing tests and want readable expectations.
|
||||||
|
|
||||||
|
Use Base64 when:
|
||||||
|
- you want the bot-wide default to stay compact and opaque;
|
||||||
|
- you prefer one stable encoded transport form everywhere;
|
||||||
|
- you are migrating from earlier Base64-based usage and want consistency.
|
||||||
|
|
||||||
|
In both cases, keep payloads short and intentional. Telegram callback data is limited, so do not treat buttons as a place to serialize large state blobs. Prefer storing compact identifiers in the payload and looking up the rest on the server side.
|
||||||
|
|
||||||
|
## Button builder for advanced cases
|
||||||
|
|
||||||
|
`InlineKbButtonBuilder` is the flexible path when you want button-specific styling or custom emoji icons.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```go
|
||||||
|
button := laniakea.NewInlineKbButton("Confirm").
|
||||||
|
SetStyle(laniakea.ButtonStyleSuccess).
|
||||||
|
SetCallbackDataJson("confirm_order", 99)
|
||||||
|
|
||||||
|
kb := laniakea.NewInlineKeyboardJson(2).AddButton(button)
|
||||||
|
```
|
||||||
|
|
||||||
|
The builder supports:
|
||||||
|
- `SetStyle(...)`
|
||||||
|
- `SetUrl(...)`
|
||||||
|
- `SetIconCustomEmojiId(...)`
|
||||||
|
- `SetCallbackDataJson(...)`
|
||||||
|
- `SetCallbackDataBase64(...)`
|
||||||
|
|
||||||
|
Use it when `AddCallbackButton(...)` is not expressive enough.
|
||||||
|
|
||||||
|
## Styled and URL buttons
|
||||||
|
|
||||||
|
Laniakea exposes three convenience style constants:
|
||||||
|
- `ButtonStylePrimary`
|
||||||
|
- `ButtonStyleSuccess`
|
||||||
|
- `ButtonStyleDanger`
|
||||||
|
|
||||||
|
You can use them with:
|
||||||
|
- `AddUrlButtonStyle(...)`
|
||||||
|
- `AddCallbackButtonStyle(...)`
|
||||||
|
- `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.
|
||||||
|
|
||||||
|
## Long replies with keyboards
|
||||||
|
|
||||||
|
If your plain-text reply may exceed Telegram’s message length limit, use:
|
||||||
|
- `ctx.AnswerLong(...)`
|
||||||
|
- `ctx.AnswerLongf(...)`
|
||||||
|
- `ctx.KeyboardLong(...)`
|
||||||
|
|
||||||
|
`KeyboardLong(...)` attaches the inline keyboard only to the final message chunk. That keeps multi-part replies readable and avoids duplicating the same keyboard on every chunk.
|
||||||
|
|
||||||
|
## Routing payloads to handlers
|
||||||
|
|
||||||
|
Payloads are registered on plugins with:
|
||||||
|
- `Plugin.NewPayload(...)`
|
||||||
|
- `Plugin.AddPayload(...)`
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```go
|
||||||
|
plugin.NewPayload(func(ctx *laniakea.MsgContext, db *App) error {
|
||||||
|
id := ctx.Args[0]
|
||||||
|
ctx.AnswerCbQueryText("Handled " + id)
|
||||||
|
return nil
|
||||||
|
}, "inspect")
|
||||||
|
```
|
||||||
|
|
||||||
|
When a callback arrives:
|
||||||
|
- the bot decodes the payload;
|
||||||
|
- finds the plugin payload handler by command name;
|
||||||
|
- copies decoded arguments into `ctx.Args`;
|
||||||
|
- runs plugin middleware;
|
||||||
|
- runs command-specific payload middleware if configured;
|
||||||
|
- executes the payload handler.
|
||||||
|
|
||||||
|
Related page:
|
||||||
|
- [[Commands-and-Plugins]]
|
||||||
|
|
||||||
|
## Practical recommendations
|
||||||
|
|
||||||
|
- Use `ctx.NewInlineKeyboard(...)` when you want the keyboard to inherit the bot’s current payload policy.
|
||||||
|
- Use keyboard-local payload overrides sparingly and intentionally.
|
||||||
|
- Prefer JSON payloads when debugging and Base64 when you want one opaque default across the project.
|
||||||
|
- Keep callback arguments compact and reconstruct richer state on the server side.
|
||||||
|
|
||||||
|
## Related pages
|
||||||
|
|
||||||
|
- [[Commands-and-Plugins]] for payload handler registration and routing.
|
||||||
|
- [[MsgContext]] for reply, edit, and callback helpers.
|
||||||
|
- [[tgapi-Overview]] for lower-level Telegram method access when keyboard helpers are not enough.
|
||||||
|
|||||||
+149
-6
@@ -1,9 +1,152 @@
|
|||||||
# Localization
|
# Localization
|
||||||
|
|
||||||
Localization in Laniakea is centered around `L10n` and key-based translation lookup.
|
Localization in Laniakea is centered around `L10n`, a small key-based translation store with fallback behavior. It is designed to keep handler code simple: handlers ask for keys, and the bot resolves the best available translation for the current user.
|
||||||
|
|
||||||
## This page should cover
|
## Overview
|
||||||
- fallback language behavior;
|
|
||||||
- attaching localization to the bot;
|
The localization flow has three main parts:
|
||||||
- `MsgContext.Translate`;
|
- create an `L10n` store with a fallback language;
|
||||||
- organizing dictionaries and keeping them maintainable.
|
- add translation entries keyed by stable identifiers;
|
||||||
|
- attach the store to the bot with `AddL10n(...)`.
|
||||||
|
|
||||||
|
Inside handlers, the most convenient lookup is `ctx.Translate(key)`.
|
||||||
|
|
||||||
|
## Creating a localization store
|
||||||
|
|
||||||
|
Create the store with `NewL10n(fallbackLanguage)`.
|
||||||
|
|
||||||
|
```go
|
||||||
|
l10n := laniakea.NewL10n("en")
|
||||||
|
```
|
||||||
|
|
||||||
|
The fallback language is used when:
|
||||||
|
- the user's language has no translation for the key;
|
||||||
|
- the user language is unknown but a fallback translation exists.
|
||||||
|
|
||||||
|
## Adding translations
|
||||||
|
|
||||||
|
Translations are stored per key with `AddDictEntry(...)`.
|
||||||
|
|
||||||
|
```go
|
||||||
|
l10n.AddDictEntry("greeting", laniakea.DictEntry{
|
||||||
|
"en": "Hello",
|
||||||
|
"ru": "Privet",
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Each key should represent meaning, not a specific language string. Good keys are stable names such as:
|
||||||
|
- `greeting`
|
||||||
|
- `errors.not_allowed`
|
||||||
|
- `menu.settings`
|
||||||
|
|
||||||
|
Avoid using the source-language sentence itself as the key. Stable keys are easier to refactor and reuse.
|
||||||
|
|
||||||
|
## Fallback behavior
|
||||||
|
|
||||||
|
`L10n.Translate(lang, key)` uses this order:
|
||||||
|
|
||||||
|
1. exact match for `lang`;
|
||||||
|
2. fallback language configured in `NewL10n(...)`;
|
||||||
|
3. the key itself.
|
||||||
|
|
||||||
|
That means a missing translation degrades predictably instead of returning an empty string.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```go
|
||||||
|
l10n := laniakea.NewL10n("en").
|
||||||
|
AddDictEntry("greeting", laniakea.DictEntry{
|
||||||
|
"en": "Hello",
|
||||||
|
"ru": "Privet",
|
||||||
|
})
|
||||||
|
|
||||||
|
l10n.Translate("ru", "greeting") // "Privet"
|
||||||
|
l10n.Translate("es", "greeting") // "Hello"
|
||||||
|
l10n.Translate("es", "unknown") // "unknown"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Attaching localization to the bot
|
||||||
|
|
||||||
|
After building the store, attach it with `Bot.AddL10n(...)`.
|
||||||
|
|
||||||
|
```go
|
||||||
|
bot.AddL10n(l10n)
|
||||||
|
```
|
||||||
|
|
||||||
|
From then on:
|
||||||
|
- `bot.L10n(lang, key)` is available for manual lookups;
|
||||||
|
- `MsgContext.Translate(key)` becomes the ergonomic handler-level helper.
|
||||||
|
|
||||||
|
If `AddL10n(nil)` is called, the bot logs a warning and keeps localization disabled.
|
||||||
|
|
||||||
|
## `MsgContext.Translate`
|
||||||
|
|
||||||
|
`ctx.Translate(key)` is the usual choice inside handlers.
|
||||||
|
|
||||||
|
It:
|
||||||
|
- reads the current user's language code from `ctx.From`;
|
||||||
|
- falls back to the bot's configured fallback language when needed;
|
||||||
|
- returns the key itself when no translation is available.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func start(ctx *laniakea.MsgContext, db *App) error {
|
||||||
|
ctx.Answer(ctx.Translate("greeting"))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
One subtle point:
|
||||||
|
- if `ctx.From` is `nil`, `ctx.Translate(key)` returns the key directly.
|
||||||
|
|
||||||
|
That can happen for some update shapes where there is no user object in context.
|
||||||
|
|
||||||
|
## Direct lookups with `bot.L10n(...)`
|
||||||
|
|
||||||
|
Use `bot.L10n(lang, key)` when you need a translation outside handler flow or when the language choice is explicit.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
- preparing background-runner messages;
|
||||||
|
- rendering text for stored user preferences;
|
||||||
|
- translating outside `MsgContext`.
|
||||||
|
|
||||||
|
## Concurrency and mutation safety
|
||||||
|
|
||||||
|
`L10n` is safe for concurrent use.
|
||||||
|
|
||||||
|
The implementation protects access with a mutex, so concurrent translation reads and dictionary updates do not require external synchronization.
|
||||||
|
|
||||||
|
Also, `AddDictEntry(...)` copies the provided `DictEntry` map before storing it. That means later external mutation of your original map does not silently rewrite the localization store.
|
||||||
|
|
||||||
|
## Organizing dictionaries
|
||||||
|
|
||||||
|
For small bots, a single `l10n.go` or `translations.go` file is often enough.
|
||||||
|
|
||||||
|
For larger bots, a good pattern is:
|
||||||
|
- keep one section or file per domain, such as `auth`, `menu`, `errors`, `admin`;
|
||||||
|
- keep keys stable and descriptive;
|
||||||
|
- group related translations together;
|
||||||
|
- use the same key names across all languages.
|
||||||
|
|
||||||
|
Example structure:
|
||||||
|
|
||||||
|
```go
|
||||||
|
l10n.
|
||||||
|
AddDictEntry("menu.start", laniakea.DictEntry{"en": "Start", "ru": "Старт"}).
|
||||||
|
AddDictEntry("menu.help", laniakea.DictEntry{"en": "Help", "ru": "Помощь"}).
|
||||||
|
AddDictEntry("errors.denied", laniakea.DictEntry{"en": "Access denied", "ru": "Доступ запрещен"})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Recommendations
|
||||||
|
|
||||||
|
- Pick one fallback language and keep it complete.
|
||||||
|
- Prefer stable semantic keys over sentence-as-key schemes.
|
||||||
|
- Use `ctx.Translate(...)` in handlers unless you have a strong reason to do manual lookup.
|
||||||
|
- Treat missing translations as a documentation problem and keep the fallback language authoritative.
|
||||||
|
|
||||||
|
## Related pages
|
||||||
|
|
||||||
|
- [[Getting-Started]] for basic bot setup
|
||||||
|
- [[MsgContext]] for handler helpers
|
||||||
|
- [[Recipes]] for localized handler examples
|
||||||
|
|||||||
+187
@@ -0,0 +1,187 @@
|
|||||||
|
# Logging
|
||||||
|
|
||||||
|
Laniakea has several logger layers: the main bot logger, an optional request logger, per-plugin loggers, and internal API or uploader loggers. This page explains how they are created, how they relate to each other, and where to customize them.
|
||||||
|
|
||||||
|
## Logger layers
|
||||||
|
|
||||||
|
The bot runtime can involve these loggers:
|
||||||
|
- the main bot logger;
|
||||||
|
- the optional request logger;
|
||||||
|
- plugin loggers;
|
||||||
|
- internal `tgapi.API` and `tgapi.Uploader` loggers.
|
||||||
|
|
||||||
|
These layers exist for different reasons:
|
||||||
|
- the bot logger covers lifecycle and high-level routing behavior;
|
||||||
|
- the request logger captures raw update payloads;
|
||||||
|
- plugin loggers let one module emit context-specific logs;
|
||||||
|
- internal API and uploader loggers help trace Telegram traffic and low-level behavior.
|
||||||
|
|
||||||
|
## Main bot logger
|
||||||
|
|
||||||
|
The main bot logger is created during bot construction.
|
||||||
|
|
||||||
|
It is used for:
|
||||||
|
- startup and shutdown messages;
|
||||||
|
- middleware and runner warnings;
|
||||||
|
- bot-level operational logs;
|
||||||
|
- fallback logging when no plugin-specific logger is available.
|
||||||
|
|
||||||
|
You can access it with:
|
||||||
|
|
||||||
|
```go
|
||||||
|
logger := bot.GetLogger()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Request logger
|
||||||
|
|
||||||
|
The request logger is optional and enabled through `BotOpts.UseRequestLogger`.
|
||||||
|
|
||||||
|
It is used for raw update logging after `getUpdates` succeeds. When enabled, the bot serializes each update and logs it through `RequestLogger`.
|
||||||
|
|
||||||
|
This is useful for:
|
||||||
|
- debugging update shapes;
|
||||||
|
- investigating routing issues;
|
||||||
|
- capturing real payload samples for tests.
|
||||||
|
|
||||||
|
Because it can be noisy, it is usually best enabled selectively.
|
||||||
|
|
||||||
|
## Plugin loggers
|
||||||
|
|
||||||
|
Each registered plugin can have its own logger.
|
||||||
|
|
||||||
|
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:
|
||||||
|
- the matched plugin logger when one exists;
|
||||||
|
- otherwise the bot logger.
|
||||||
|
|
||||||
|
That means handler-local logs naturally follow plugin boundaries when possible.
|
||||||
|
|
||||||
|
## `MsgContext.Logger`
|
||||||
|
|
||||||
|
Inside handlers and middleware, the easiest logger to use is `ctx.Logger`.
|
||||||
|
|
||||||
|
It already reflects the current execution context:
|
||||||
|
- plugin logger for matched plugin routes;
|
||||||
|
- bot logger as fallback.
|
||||||
|
|
||||||
|
This makes it the recommended logger for most application code inside handlers.
|
||||||
|
|
||||||
|
## Debug mode
|
||||||
|
|
||||||
|
Use `Bot.Debug(true)` or `BotOpts.Debug` to enable debug-level logging.
|
||||||
|
|
||||||
|
`Debug(true)` updates:
|
||||||
|
- the bot logger;
|
||||||
|
- the request logger, if enabled;
|
||||||
|
- already registered plugin loggers.
|
||||||
|
|
||||||
|
This is helpful when you want to increase verbosity after bot creation.
|
||||||
|
|
||||||
|
## File logging
|
||||||
|
|
||||||
|
Laniakea can write logs to files when:
|
||||||
|
- `WriteToFile` is enabled;
|
||||||
|
- `LoggerBasePath` is configured or defaults to `./`.
|
||||||
|
|
||||||
|
By default, file logging creates:
|
||||||
|
- `main.log` for the bot logger;
|
||||||
|
- `requests.log` for the request logger.
|
||||||
|
|
||||||
|
If file writer creation fails, the logger falls back to stdout logging rather than crashing the bot.
|
||||||
|
|
||||||
|
## Shared logger policy
|
||||||
|
|
||||||
|
The helper constructors in `utils` apply a shared logging policy:
|
||||||
|
- JSON stdout output by default;
|
||||||
|
- a prefix such as `BOT`, `REQUESTS`, `API`, or `UPLOADER`;
|
||||||
|
- the chosen log level.
|
||||||
|
|
||||||
|
This gives the library a consistent baseline logging style across components.
|
||||||
|
|
||||||
|
## Database logger writers
|
||||||
|
|
||||||
|
`AddDatabaseLoggerWriter(...)` lets you attach a writer derived from your database or shared dependency context to multiple loggers at once.
|
||||||
|
|
||||||
|
When it succeeds, the writer is attached to:
|
||||||
|
- the main bot logger;
|
||||||
|
- the request logger if present;
|
||||||
|
- API and uploader loggers;
|
||||||
|
- already registered plugin loggers.
|
||||||
|
|
||||||
|
Important nuance:
|
||||||
|
- call it after `AddPlugins(...)` if plugin loggers should receive the writer;
|
||||||
|
- plugins registered later do not automatically inherit previously attached database writers.
|
||||||
|
|
||||||
|
If the database context is unset or nil, the method logs a warning and skips the writer.
|
||||||
|
|
||||||
|
## Plugin logger customization
|
||||||
|
|
||||||
|
Plugin logger APIs:
|
||||||
|
- `SetLogger(...)`
|
||||||
|
- `RemoveLogger()`
|
||||||
|
|
||||||
|
These should be called before `AddPlugins(...)`, because plugin registration is a snapshot point for configuration.
|
||||||
|
|
||||||
|
If you mutate the original plugin logger after registration, the bot's internal copy is not guaranteed to reflect that change.
|
||||||
|
|
||||||
|
## Logging and errors
|
||||||
|
|
||||||
|
When handler errors flow through `ctx.error(...)`, the error is logged with the current context logger after the user-facing response is sent.
|
||||||
|
|
||||||
|
This means:
|
||||||
|
- returned command errors are logged under the plugin logger when one exists;
|
||||||
|
- otherwise they are logged under the main bot logger.
|
||||||
|
|
||||||
|
Related page:
|
||||||
|
- [[Error-Handling]]
|
||||||
|
|
||||||
|
## Practical patterns
|
||||||
|
|
||||||
|
### Turn on debug logging
|
||||||
|
|
||||||
|
```go
|
||||||
|
opts := (&laniakea.BotOpts{}).
|
||||||
|
SetToken("TOKEN").
|
||||||
|
SetDebug(true)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Enable request logging
|
||||||
|
|
||||||
|
```go
|
||||||
|
opts := (&laniakea.BotOpts{}).
|
||||||
|
SetToken("TOKEN").
|
||||||
|
SetUseRequestLogger(true)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Provide a custom plugin logger
|
||||||
|
|
||||||
|
```go
|
||||||
|
plugin := laniakea.NewPlugin[*App]("admin")
|
||||||
|
plugin.SetLogger(customLogger)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Attach database-backed logging
|
||||||
|
|
||||||
|
```go
|
||||||
|
bot.DatabaseContext(db)
|
||||||
|
bot.AddPlugins(plugin)
|
||||||
|
bot.AddDatabaseLoggerWriter(func(db *sql.DB) slog.LoggerWriter {
|
||||||
|
return newDBWriter(db)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Recommendations
|
||||||
|
|
||||||
|
- Use `ctx.Logger` inside handlers instead of reaching for global loggers directly.
|
||||||
|
- Enable request logging when debugging update routing, then turn it back down.
|
||||||
|
- Treat plugin loggers as a useful boundary for large bots with multiple modules.
|
||||||
|
- Call `AddDatabaseLoggerWriter(...)` only after the bot has a valid database context.
|
||||||
|
|
||||||
|
## Related pages
|
||||||
|
|
||||||
|
- [[Bot-Options-and-Configuration]]
|
||||||
|
- [[Error-Handling]]
|
||||||
|
- [[Bot-Lifecycle]]
|
||||||
+222
-7
@@ -1,10 +1,225 @@
|
|||||||
# Middleware
|
# Middleware
|
||||||
|
|
||||||
Middleware lets you run logic before commands, payloads, and update handlers.
|
Middleware lets you run logic before commands, payloads, and non-command update handlers. It is the main place for cross-cutting concerns such as access checks, request logging, feature flags, and lightweight context preparation.
|
||||||
|
|
||||||
## This page should cover
|
## What middleware can do
|
||||||
- synchronous vs asynchronous middleware;
|
|
||||||
- stop/continue behavior;
|
Middleware is useful when the same check or side effect should apply in more than one handler.
|
||||||
- ordering expectations;
|
|
||||||
- race and mutation caveats for async middleware;
|
Typical uses:
|
||||||
- where middleware fits relative to plugins and handlers.
|
- reject updates from unauthorized users;
|
||||||
|
- log incoming commands and callback payloads;
|
||||||
|
- attach derived values to `MsgContext`;
|
||||||
|
- 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]].
|
||||||
|
|
||||||
|
## Middleware levels
|
||||||
|
|
||||||
|
Laniakea has three middleware layers:
|
||||||
|
|
||||||
|
- bot-level middleware, added with `Bot.AddMiddleware(...)`;
|
||||||
|
- plugin-level middleware, added with `Plugin.AddMiddleware(...)`;
|
||||||
|
- command or payload middleware, added with `Command.Use(...)`.
|
||||||
|
|
||||||
|
Each level is useful for a different scope:
|
||||||
|
- bot-level middleware applies to every update the bot processes;
|
||||||
|
- plugin-level middleware applies to every handler in a single plugin;
|
||||||
|
- command-level middleware applies only to one command or payload.
|
||||||
|
|
||||||
|
## Execution order
|
||||||
|
|
||||||
|
The effective order is:
|
||||||
|
|
||||||
|
1. bot-level middleware;
|
||||||
|
2. plugin-level middleware for the matched plugin;
|
||||||
|
3. command-level middleware for the matched command or payload;
|
||||||
|
4. the final handler.
|
||||||
|
|
||||||
|
For non-command update handlers registered through `AddUpdateHandler(...)`, the flow is:
|
||||||
|
|
||||||
|
1. bot-level middleware;
|
||||||
|
2. plugin-level middleware for each plugin that handles that update type;
|
||||||
|
3. the update handler itself.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
## Synchronous middleware
|
||||||
|
|
||||||
|
By default, middleware is synchronous.
|
||||||
|
|
||||||
|
The executor signature is:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func(ctx *laniakea.MsgContext, db T) bool
|
||||||
|
```
|
||||||
|
|
||||||
|
Return values mean:
|
||||||
|
- `true`: continue processing;
|
||||||
|
- `false`: stop the current chain immediately.
|
||||||
|
|
||||||
|
This makes synchronous middleware the right choice for:
|
||||||
|
- authorization;
|
||||||
|
- validation;
|
||||||
|
- rate limiting gates;
|
||||||
|
- any logic that must block handler execution on failure.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```go
|
||||||
|
auth := laniakea.NewMiddleware("auth", func(ctx *laniakea.MsgContext, db *App) bool {
|
||||||
|
if ctx.From == nil || !db.Allowed(ctx.From.ID) {
|
||||||
|
ctx.Answer("Access denied")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Asynchronous middleware
|
||||||
|
|
||||||
|
Middleware can also run asynchronously with `SetAsync(true)`.
|
||||||
|
|
||||||
|
```go
|
||||||
|
audit := laniakea.NewMiddleware("audit", func(ctx *laniakea.MsgContext, db *App) bool {
|
||||||
|
db.Audit(ctx.Update.UpdateID, ctx.Text)
|
||||||
|
return true
|
||||||
|
}).SetAsync(true)
|
||||||
|
```
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
That means async middleware is appropriate for:
|
||||||
|
- fire-and-forget logging;
|
||||||
|
- metrics;
|
||||||
|
- telemetry;
|
||||||
|
- best-effort notifications.
|
||||||
|
|
||||||
|
It is not appropriate for:
|
||||||
|
- access control;
|
||||||
|
- required validation;
|
||||||
|
- mutating context values that the handler must read;
|
||||||
|
- any logic that must deterministically stop execution.
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
Practical consequence:
|
||||||
|
- changes you make to the copied `ctx` inside async middleware are local to that goroutine;
|
||||||
|
- handlers and later middleware will not see those changes.
|
||||||
|
|
||||||
|
So this pattern does not work:
|
||||||
|
|
||||||
|
```go
|
||||||
|
bad := laniakea.NewMiddleware("bad", func(ctx *laniakea.MsgContext, db *App) bool {
|
||||||
|
ctx.Text = "rewritten"
|
||||||
|
return false
|
||||||
|
}).SetAsync(true)
|
||||||
|
```
|
||||||
|
|
||||||
|
The handler chain will still continue, and the rewritten text will not become the canonical handler context.
|
||||||
|
|
||||||
|
## Bot-level ordering
|
||||||
|
|
||||||
|
Bot-level middleware is the only layer that has explicit sorting support.
|
||||||
|
|
||||||
|
`Bot.AddMiddleware(...)` sorts middleware by:
|
||||||
|
- `order` ascending;
|
||||||
|
- then by `name` lexicographically when orders are equal.
|
||||||
|
|
||||||
|
You can set the order with `SetOrder(...)`:
|
||||||
|
|
||||||
|
```go
|
||||||
|
logMW := laniakea.NewMiddleware("log", logFn).SetOrder(10)
|
||||||
|
authMW := laniakea.NewMiddleware("auth", authFn).SetOrder(20)
|
||||||
|
|
||||||
|
bot.AddMiddleware(authMW, logMW)
|
||||||
|
```
|
||||||
|
|
||||||
|
Even though `authMW` is added first here, `logMW` runs first because its order is lower.
|
||||||
|
|
||||||
|
## Plugin and command ordering
|
||||||
|
|
||||||
|
Plugin-level and command-level middleware keep insertion order.
|
||||||
|
|
||||||
|
That means:
|
||||||
|
- `Plugin.AddMiddleware(a).AddMiddleware(b)` runs `a`, then `b`;
|
||||||
|
- `cmd.Use(a).Use(b)` runs `a`, then `b`.
|
||||||
|
|
||||||
|
If you need precise phase control across the whole bot, prefer putting those checks into bot-level middleware where ordering is explicit.
|
||||||
|
|
||||||
|
## Where middleware fits in routing
|
||||||
|
|
||||||
|
The routing behavior matters when deciding where to attach middleware:
|
||||||
|
|
||||||
|
- bot-level middleware sees every update, even updates that never match a command or plugin;
|
||||||
|
- plugin-level middleware runs only after the bot has already matched the target plugin;
|
||||||
|
- command-level middleware runs only after the command or payload was resolved and arguments were parsed for that command structure.
|
||||||
|
|
||||||
|
This usually means:
|
||||||
|
- use bot-level middleware for global gates and observability;
|
||||||
|
- use plugin-level middleware for module-local policy;
|
||||||
|
- use command-level middleware for one handler's special preconditions.
|
||||||
|
|
||||||
|
## Common patterns
|
||||||
|
|
||||||
|
### Global authorization gate
|
||||||
|
|
||||||
|
```go
|
||||||
|
bot.AddMiddleware(
|
||||||
|
laniakea.NewMiddleware("private-only", func(ctx *laniakea.MsgContext, db *App) bool {
|
||||||
|
if ctx.Chat == nil || ctx.Chat.Type != "private" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Plugin-wide admin policy
|
||||||
|
|
||||||
|
```go
|
||||||
|
admin := laniakea.NewPlugin[*App]("admin")
|
||||||
|
admin.AddMiddleware(
|
||||||
|
laniakea.NewMiddleware("admin-only", func(ctx *laniakea.MsgContext, db *App) bool {
|
||||||
|
return ctx.From != nil && db.IsAdmin(ctx.From.ID)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Command-specific validation
|
||||||
|
|
||||||
|
```go
|
||||||
|
ban := admin.NewCommand(banUser, "ban")
|
||||||
|
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")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}))
|
||||||
|
```
|
||||||
|
|
||||||
|
## Caveats
|
||||||
|
|
||||||
|
- Middleware with an empty name is skipped by `Bot.AddMiddleware(...)`.
|
||||||
|
- Async middleware cannot block execution.
|
||||||
|
- Async middleware should avoid depending on mutable shared state unless you provide your own synchronization.
|
||||||
|
- Plugin middleware and command middleware are snapshotted when the plugin is registered with `Bot.AddPlugins(...)`, so finish configuring them before registration.
|
||||||
|
- For non-command update handlers, plugin chains are isolated by cloned contexts; for matched commands and payloads, processing stops after the first matching plugin handles the update.
|
||||||
|
|
||||||
|
## Recommendations
|
||||||
|
|
||||||
|
- Default to synchronous middleware unless you specifically want fire-and-forget behavior.
|
||||||
|
- Keep middleware small and single-purpose.
|
||||||
|
- Put denial responses close to the gate that makes the decision.
|
||||||
|
- Prefer bot-level middleware for global concerns and explicit ordering.
|
||||||
|
- Use async middleware only for side effects that are safe to lose or reorder.
|
||||||
|
|||||||
+183
-6
@@ -1,9 +1,186 @@
|
|||||||
# Migration
|
# Migration
|
||||||
|
|
||||||
Use this page to track version-to-version changes that affect existing bots.
|
Use this page when upgrading an existing bot between Laniakea release candidates. It focuses on migration-impacting API and behavior changes, especially the larger RC transitions that require code edits instead of just a rebuild.
|
||||||
|
|
||||||
## This page should cover
|
## How to use this page
|
||||||
- breaking changes by release;
|
|
||||||
- migration notes for major release-candidate milestones;
|
For the full release history, read the repository `CHANGELOG.md`. This page is narrower: it groups the most important upgrade work by milestone and calls out what usually needs to change in real bots.
|
||||||
- changed defaults and behavior contracts;
|
|
||||||
- links to [[Semver-and-Releases]] and `CHANGELOG.md`.
|
Also see:
|
||||||
|
- [[Bot-Lifecycle]] for the current startup and shutdown model;
|
||||||
|
- [[Commands-and-Plugins]] for handler registration patterns;
|
||||||
|
- [[Inline-Keyboards-and-Payloads]] for payload-type behavior;
|
||||||
|
- [[Semver-and-Releases]] for the project's versioning intent.
|
||||||
|
|
||||||
|
## Recommended upgrade strategy
|
||||||
|
|
||||||
|
When jumping across multiple RC versions:
|
||||||
|
|
||||||
|
1. Update to the latest version in `go.mod`.
|
||||||
|
2. Fix compile errors first.
|
||||||
|
3. Revisit startup and shutdown code.
|
||||||
|
4. Revisit handler signatures.
|
||||||
|
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`.
|
||||||
|
|
||||||
|
## `v1.0.0-rc.12`
|
||||||
|
|
||||||
|
`rc.12` is mainly a handler and validation release. The biggest breaking change is that handlers now return `error`.
|
||||||
|
|
||||||
|
### 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.
|
||||||
|
- 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`.
|
||||||
|
|
||||||
|
### What to migrate
|
||||||
|
|
||||||
|
Update every handler to return `error`, even if it normally succeeds:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func ping(ctx *laniakea.MsgContext, db *App) error {
|
||||||
|
ctx.Answer("pong")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If your old handlers did their own error reporting inline, you can now choose between:
|
||||||
|
- still replying manually and returning `nil`;
|
||||||
|
- or returning an error and letting the bot's centralized error handling format the user-visible response.
|
||||||
|
|
||||||
|
If you previously used `Answer(...)` for text that could exceed Telegram's single-message limit, consider moving to `AnswerLong(...)` or `KeyboardLong(...)` instead of changing the behavior of existing calls.
|
||||||
|
|
||||||
|
If your callback payloads relied on tolerant decoding, be aware that enabling strict payload mode will reject payloads encoded in a different format than the bot default.
|
||||||
|
|
||||||
|
## `v1.0.0-rc.10`
|
||||||
|
|
||||||
|
`rc.10` is the largest migration step in the current codebase. It changed construction, run semantics, handler dependency typing, plugin registration behavior, and update handling.
|
||||||
|
|
||||||
|
### What changed
|
||||||
|
|
||||||
|
- `NewBot[T](opts)` now returns `(*Bot[T], error)`.
|
||||||
|
- `Run()` and `RunWithContext(ctx)` now return `error`.
|
||||||
|
- `Bot` instances became explicitly single-use.
|
||||||
|
- Handler dependency typing changed from forced `*T` usage to consistent `T`.
|
||||||
|
- `DatabaseContext(...)`, `GetDBContext()`, and `DbLogger[T]` were updated to that `T`-based model.
|
||||||
|
- `Plugin.AddUpdateHandler(...)` was introduced for non-command update routing.
|
||||||
|
- Builder helpers such as `NewMiddleware(...)` now return values instead of pointers.
|
||||||
|
- Plugin registration now snapshots plugin state at `AddPlugins(...)`.
|
||||||
|
- `BaseMenuButton` was renamed to `MenuButton`.
|
||||||
|
|
||||||
|
### What to migrate
|
||||||
|
|
||||||
|
Construction now needs explicit error handling:
|
||||||
|
|
||||||
|
```go
|
||||||
|
bot, err := laniakea.NewBot[*sql.DB](opts)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer bot.Close()
|
||||||
|
```
|
||||||
|
|
||||||
|
Startup now also returns errors:
|
||||||
|
|
||||||
|
```go
|
||||||
|
if err := bot.RunWithContext(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If your bot type used `Bot[MyDB]` while handlers expected `*MyDB`, update the generic parameter to match what you actually want to inject. Shared dependencies should usually use pointer types:
|
||||||
|
|
||||||
|
```go
|
||||||
|
bot, err := laniakea.NewBot[*sql.DB](opts)
|
||||||
|
bot.DatabaseContext(db)
|
||||||
|
```
|
||||||
|
|
||||||
|
If you mutated plugins after `AddPlugins(...)`, stop doing that. Register commands, payloads, middleware, logger configuration, and `OnClose` hooks before handing the plugin to the bot.
|
||||||
|
|
||||||
|
If you previously routed every update through commands and payloads, consider moving non-command update types to `AddUpdateHandler(...)` instead of overloading command logic.
|
||||||
|
|
||||||
|
### Typical `rc.10` fixes
|
||||||
|
|
||||||
|
- add `err` handling after `NewBot(...)`;
|
||||||
|
- add `err` handling after `Run()` or `RunWithContext(...)`;
|
||||||
|
- replace `Bot[MyDB]` with `Bot[*MyDB]` where shared mutable dependencies are intended;
|
||||||
|
- replace `*NewMiddleware(...)`-style assumptions with direct values;
|
||||||
|
- rename `BaseMenuButton` usages to `MenuButton`.
|
||||||
|
|
||||||
|
## `v1.0.0-rc.7`
|
||||||
|
|
||||||
|
`rc.7` focused on shutdown and logging model cleanup.
|
||||||
|
|
||||||
|
### What changed
|
||||||
|
|
||||||
|
- `Bot.Close(ctx)` became `Bot.Close()`.
|
||||||
|
- remote Telegram session shutdown moved to `Bot.CloseRemote(ctx)`.
|
||||||
|
- `tgapi.API.CloseApi()` was renamed to `tgapi.API.Close()`.
|
||||||
|
- `tgapi.API.Close()` was renamed to `tgapi.API.CloseRemote()`.
|
||||||
|
- `tgapi.API.CloseWithContext()` was renamed to `tgapi.API.CloseRemoteWithContext(ctx)`.
|
||||||
|
- plugin lifecycle APIs such as `SetLogger`, `RemoveLogger`, `SetOnClose`, and `Plugin.Close()` were added.
|
||||||
|
|
||||||
|
### What to migrate
|
||||||
|
|
||||||
|
Replace local shutdown calls:
|
||||||
|
|
||||||
|
```go
|
||||||
|
defer bot.Close()
|
||||||
|
```
|
||||||
|
|
||||||
|
If you actually need Telegram Bot API remote close semantics, call them explicitly:
|
||||||
|
|
||||||
|
```go
|
||||||
|
if err := bot.CloseRemote(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
And update direct `tgapi` calls to the renamed close methods.
|
||||||
|
|
||||||
|
## `v1.0.0-rc.4`
|
||||||
|
|
||||||
|
`rc.4` mainly affects callers using lower-level `tgapi` webhook APIs.
|
||||||
|
|
||||||
|
### What changed
|
||||||
|
|
||||||
|
- `WithContext` variants were added across more `tgapi` methods.
|
||||||
|
- Webhook certificate upload moved away from the JSON `SetWebhookP.Certificate` path.
|
||||||
|
- Certificate upload now goes through uploader-based webhook APIs.
|
||||||
|
|
||||||
|
### What to migrate
|
||||||
|
|
||||||
|
If you were sending webhook certificates through `SetWebhookP.Certificate`, switch to `Uploader.SetWebhook(...)` or `Uploader.SetWebhookWithContext(...)`.
|
||||||
|
|
||||||
|
If you maintain infrastructure code around deadlines or cancellation, prefer the newer `WithContext` variants consistently instead of wrapping only some calls.
|
||||||
|
|
||||||
|
## Behavior changes worth noticing
|
||||||
|
|
||||||
|
Not every important change is a compile-time break.
|
||||||
|
|
||||||
|
Pay attention to these behavior changes after upgrading:
|
||||||
|
- polling retry now uses exponential backoff instead of busy looping;
|
||||||
|
- `RunWithContext` does not close resources automatically; callers still need `Close()`;
|
||||||
|
- callback payload decoding can now be strict or tolerant depending on configuration;
|
||||||
|
- message and caption validation now fails earlier, before Telegram requests are made;
|
||||||
|
- long plain-text replies now have dedicated helpers instead of implicit splitting.
|
||||||
|
|
||||||
|
## After upgrading
|
||||||
|
|
||||||
|
After a version jump, it is worth rechecking:
|
||||||
|
- startup and shutdown flows;
|
||||||
|
- command and payload handlers;
|
||||||
|
- callback payload decoding;
|
||||||
|
- any direct `tgapi` usage;
|
||||||
|
- tests that use update fixtures or callback payload samples.
|
||||||
|
|
||||||
|
## When in doubt
|
||||||
|
|
||||||
|
If an upgrade feels ambiguous, compare:
|
||||||
|
- the relevant section in `CHANGELOG.md`;
|
||||||
|
- current examples in `README.md`;
|
||||||
|
- the focused wiki pages linked above.
|
||||||
|
|||||||
+14
-9
@@ -1,9 +1,10 @@
|
|||||||
# Page Priority
|
# Page Priority
|
||||||
|
|
||||||
This page tracks the recommended fill order for the wiki while documentation is still being built out.
|
This page tracks maintenance priority for the wiki now that the core page set is in place. Use it to decide where future edits, expansions, and API-alignment work should land first.
|
||||||
|
|
||||||
## Priority 1
|
## Priority 1
|
||||||
- [[Getting-Started]]
|
- [[Getting-Started]]
|
||||||
|
- [[Bot-Options-and-Configuration]]
|
||||||
- [[Commands-and-Plugins]]
|
- [[Commands-and-Plugins]]
|
||||||
- [[MsgContext]]
|
- [[MsgContext]]
|
||||||
- [[Inline-Keyboards-and-Payloads]]
|
- [[Inline-Keyboards-and-Payloads]]
|
||||||
@@ -12,19 +13,23 @@ This page tracks the recommended fill order for the wiki while documentation is
|
|||||||
## Priority 2
|
## Priority 2
|
||||||
- [[Bot-Lifecycle]]
|
- [[Bot-Lifecycle]]
|
||||||
- [[Middleware]]
|
- [[Middleware]]
|
||||||
|
- [[Auto-Generated-Commands]]
|
||||||
|
- [[Rate-Limiting]]
|
||||||
|
- [[Drafts]]
|
||||||
|
- [[Localization]]
|
||||||
|
- [[Recipes]]
|
||||||
|
- [[Runners]]
|
||||||
|
- [[Error-Handling]]
|
||||||
|
- [[Logging]]
|
||||||
|
- [[Testing-Bots-with-Laniakea]]
|
||||||
- [[Migration]]
|
- [[Migration]]
|
||||||
- [[FAQ]]
|
- [[FAQ]]
|
||||||
|
|
||||||
## Priority 3
|
## Priority 3
|
||||||
- [[Drafts]]
|
|
||||||
- [[Localization]]
|
|
||||||
- [[Rate-Limiting]]
|
|
||||||
- [[Recipes]]
|
|
||||||
|
|
||||||
## Priority 4
|
|
||||||
- [[Semver-and-Releases]]
|
- [[Semver-and-Releases]]
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
- Keep `Home.md` ordered the same way.
|
- Keep `Home.md` aligned with the general information architecture, not necessarily the exact priority order.
|
||||||
- Within each page, explain the most necessary and most frequently used APIs first.
|
- Within each page, explain the most necessary and most frequently used APIs first.
|
||||||
- Add new pages into this list before writing large amounts of content for them.
|
- Add new pages here before writing large amounts of content for them.
|
||||||
|
- Revisit Priority 1 pages first when public APIs or README examples change.
|
||||||
|
|||||||
+171
-6
@@ -1,9 +1,174 @@
|
|||||||
# Rate Limiting
|
# Rate Limiting
|
||||||
|
|
||||||
This page explains how Laniakea applies request throttling and reacts to Telegram rate-limit responses.
|
This page explains how Laniakea throttles outgoing Telegram API requests and how it reacts when Telegram answers with `429 Too Many Requests`. The built-in limiter is designed to protect both the global bot throughput and hot chats that might otherwise overwhelm the API.
|
||||||
|
|
||||||
## This page should cover
|
## Overview
|
||||||
- configured limiter behavior;
|
|
||||||
- drop mode vs waiting;
|
Laniakea wires a `RateLimiter` into the internal `tgapi.API` client during `NewBot(...)`.
|
||||||
- `retry_after` handling;
|
|
||||||
- practical tuning guidelines for different bot sizes.
|
That limiter combines:
|
||||||
|
- a global token bucket;
|
||||||
|
- per-chat token buckets;
|
||||||
|
- global cooldown locks;
|
||||||
|
- per-chat cooldown locks.
|
||||||
|
|
||||||
|
In other words, the library handles both steady-state throttling and reactive backoff after Telegram explicitly says to wait.
|
||||||
|
|
||||||
|
## Default behavior
|
||||||
|
|
||||||
|
The built-in limiter starts with these defaults:
|
||||||
|
- global limit: `30` requests per second, burst `30`;
|
||||||
|
- per-chat limit: `1` request per second, burst `1`.
|
||||||
|
|
||||||
|
When a bot is created, `BotOpts.RateLimit` can override the global rate. The per-chat limiter remains `1 req/s` per chat in the current implementation.
|
||||||
|
|
||||||
|
## Where the limiter applies
|
||||||
|
|
||||||
|
The limiter is used by:
|
||||||
|
- normal JSON Telegram API requests through `tgapi.API`;
|
||||||
|
- multipart uploader requests through `tgapi.Uploader`.
|
||||||
|
|
||||||
|
That matters because rate limiting is not only about `SendMessage(...)`. Upload-heavy bots still benefit from the same cooldown handling and retry behavior.
|
||||||
|
|
||||||
|
## Two operating modes
|
||||||
|
|
||||||
|
The limiter supports two modes:
|
||||||
|
- wait mode;
|
||||||
|
- drop mode.
|
||||||
|
|
||||||
|
The mode is controlled by `BotOpts.DropRLOverflow` and passed into `tgapi.API` as the limiter's overflow behavior.
|
||||||
|
|
||||||
|
## Wait mode
|
||||||
|
|
||||||
|
Wait mode is the default and usually the safest option.
|
||||||
|
|
||||||
|
In wait mode:
|
||||||
|
- if capacity is available, the request proceeds immediately;
|
||||||
|
- if a limiter bucket is empty, the request waits;
|
||||||
|
- if a global or chat cooldown lock is active, the request waits until that lock expires;
|
||||||
|
- if the context is canceled while waiting, the request returns the context error.
|
||||||
|
|
||||||
|
This mode favors reliability and delivery over latency.
|
||||||
|
|
||||||
|
It is usually the right choice for:
|
||||||
|
- bots where losing messages is unacceptable;
|
||||||
|
- admin or workflow bots;
|
||||||
|
- bots that send important transactional responses.
|
||||||
|
|
||||||
|
## Drop mode
|
||||||
|
|
||||||
|
Drop mode rejects requests immediately when a limiter would otherwise block.
|
||||||
|
|
||||||
|
In drop mode:
|
||||||
|
- requests do not wait for limiter capacity;
|
||||||
|
- requests do not wait for cooldown locks to expire;
|
||||||
|
- the limiter returns `ErrDropOverflow` instead.
|
||||||
|
|
||||||
|
This mode favors responsiveness over guaranteed delivery.
|
||||||
|
|
||||||
|
It can make sense for:
|
||||||
|
- noisy bots with low-value updates;
|
||||||
|
- bots where stale replies are worse than skipped replies;
|
||||||
|
- telemetry or best-effort notification workloads.
|
||||||
|
|
||||||
|
Be careful with it in user-facing command flows, because it can turn load spikes into visible dropped messages.
|
||||||
|
|
||||||
|
## Global limit versus per-chat limit
|
||||||
|
|
||||||
|
The limiter checks both global and per-chat constraints.
|
||||||
|
|
||||||
|
Global limit protects the bot as a whole:
|
||||||
|
- too many concurrent requests across all chats will hit the global bucket first.
|
||||||
|
|
||||||
|
Per-chat limit protects one chat from becoming too noisy:
|
||||||
|
- a flood in one chat does not automatically consume the entire chat-level budget of another chat;
|
||||||
|
- chat cooldowns are scoped to the affected chat.
|
||||||
|
|
||||||
|
This is especially helpful for bots used in large groups and private chats at the same time.
|
||||||
|
|
||||||
|
## How `retry_after` is handled
|
||||||
|
|
||||||
|
When Telegram replies with error `429` and a `retry_after` value:
|
||||||
|
|
||||||
|
- Laniakea logs the cooldown;
|
||||||
|
- the limiter stores a cooldown lock;
|
||||||
|
- the lock is scoped to the chat if the request had a chat ID;
|
||||||
|
- otherwise the lock becomes global;
|
||||||
|
- the client waits for the specified time and retries the request automatically.
|
||||||
|
|
||||||
|
This behavior exists in both the normal API client and the uploader path.
|
||||||
|
|
||||||
|
That means Telegram's own feedback actively reshapes future request pacing instead of being treated as a plain error.
|
||||||
|
|
||||||
|
## Chat-scoped versus global cooldowns
|
||||||
|
|
||||||
|
If the request is associated with a concrete `chatID`, Laniakea applies `retry_after` as a chat-specific lock.
|
||||||
|
|
||||||
|
If the request has no chat context, Laniakea applies it as a global lock.
|
||||||
|
|
||||||
|
Examples of global-scope requests:
|
||||||
|
- requests that are not tied to one chat;
|
||||||
|
- some infrastructure or metadata calls;
|
||||||
|
- requests created without an associated chat ID in the low-level API.
|
||||||
|
|
||||||
|
This distinction is important because it prevents one noisy chat from unnecessarily freezing the entire bot when Telegram's limit is actually chat-local.
|
||||||
|
|
||||||
|
## Context cancellation behavior
|
||||||
|
|
||||||
|
Waiting is always context-aware.
|
||||||
|
|
||||||
|
If the bot or request context is canceled while the limiter is waiting:
|
||||||
|
- the wait stops immediately;
|
||||||
|
- the request returns the context error instead of hanging until the cooldown finishes.
|
||||||
|
|
||||||
|
This matters for graceful shutdown, because rate-limited requests should not keep the process alive longer than the caller intends.
|
||||||
|
|
||||||
|
## Practical tuning
|
||||||
|
|
||||||
|
### Small and medium bots
|
||||||
|
|
||||||
|
Start with the default global rate or a conservative custom value. The defaults are usually good enough unless you already know your workload characteristics.
|
||||||
|
|
||||||
|
### Bots with bursts across many chats
|
||||||
|
|
||||||
|
Increase `BotOpts.RateLimit` carefully if:
|
||||||
|
- handlers are fast;
|
||||||
|
- your infrastructure can absorb the parallelism;
|
||||||
|
- you are not already seeing Telegram `429` responses.
|
||||||
|
|
||||||
|
Do not assume that raising the global limit alone solves everything. Per-chat pressure can still trigger chat-local cooldowns.
|
||||||
|
|
||||||
|
### Bots with heavy uploads
|
||||||
|
|
||||||
|
Remember that uploader requests also participate in rate limiting. If your bot sends media aggressively, watch for `retry_after` behavior there too, not only in message sends.
|
||||||
|
|
||||||
|
### Low-value, high-volume bots
|
||||||
|
|
||||||
|
Consider drop mode only when skipped messages are acceptable. It is a policy choice, not a performance upgrade in all cases.
|
||||||
|
|
||||||
|
## Configuration points
|
||||||
|
|
||||||
|
The main knobs exposed through `BotOpts` are:
|
||||||
|
- `SetRateLimit(limit)` for the global request-per-second limit;
|
||||||
|
- `SetDropRLOverflow(drop)` to choose drop mode instead of waiting.
|
||||||
|
|
||||||
|
There is no high-level bot option today for changing the per-chat `1 req/s` limiter. If you need a different per-chat policy, that would currently require working with the lower-level limiter implementation directly.
|
||||||
|
|
||||||
|
## Caveats
|
||||||
|
|
||||||
|
- `RateLimit <= 0` does not replace the built-in global limiter; it leaves the current limiter settings in place.
|
||||||
|
- Drop mode returns `ErrDropOverflow`, so callers that care about delivery should surface or log that explicitly.
|
||||||
|
- `retry_after` auto-retry still depends on context lifetime; cancellation wins over waiting.
|
||||||
|
- Low-level requests created without chat IDs can only benefit from global cooldown scoping, not chat-local scoping.
|
||||||
|
|
||||||
|
## Recommendations
|
||||||
|
|
||||||
|
- Default to wait mode unless you have a strong reason to prefer dropping.
|
||||||
|
- Tune the global limit gradually and based on observed traffic.
|
||||||
|
- Expect `retry_after` to happen occasionally and design handlers to tolerate delayed delivery.
|
||||||
|
- For chatty bots, monitor which flows are producing the most requests before raising limits.
|
||||||
|
|
||||||
|
## Related pages
|
||||||
|
|
||||||
|
- [[Bot-Lifecycle]] for request execution during startup and shutdown
|
||||||
|
- [[tgapi-Overview]] for the lower-level client layer
|
||||||
|
|||||||
+154
-8
@@ -1,11 +1,157 @@
|
|||||||
# Recipes
|
# Recipes
|
||||||
|
|
||||||
This page should collect short, task-focused examples for common bot patterns.
|
This page collects short, task-focused examples for common bot patterns. Each recipe is intentionally small and copy-friendly, and you can combine them with the deeper pages such as [[Commands-and-Plugins]], [[Middleware]], and [[Inline-Keyboards-and-Payloads]].
|
||||||
|
|
||||||
## This page should cover
|
## Admin-only command
|
||||||
- admin-only commands;
|
|
||||||
- callback button flows;
|
Use plugin middleware when several commands share the same access rule.
|
||||||
- long replies;
|
|
||||||
- file uploads;
|
```go
|
||||||
- localized commands;
|
type App struct{}
|
||||||
- custom update handlers.
|
|
||||||
|
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 {
|
||||||
|
return ctx.From != nil && app.IsAdmin(ctx.From.ID)
|
||||||
|
}))
|
||||||
|
|
||||||
|
admin.NewCommand(func(ctx *laniakea.MsgContext, app *App) error {
|
||||||
|
ctx.Answer("Admin command executed")
|
||||||
|
return nil
|
||||||
|
}, "reload")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Callback button flow
|
||||||
|
|
||||||
|
Use a payload handler for inline keyboard callbacks.
|
||||||
|
|
||||||
|
```go
|
||||||
|
menu := laniakea.NewPlugin[laniakea.NoDB]("menu")
|
||||||
|
|
||||||
|
menu.NewCommand(func(ctx *laniakea.MsgContext, db laniakea.NoDB) error {
|
||||||
|
kb := ctx.NewInlineKeyboard(1)
|
||||||
|
kb.NewDataButton("Open settings", laniakea.NewCallbackData("settings"))
|
||||||
|
ctx.Keyboard("Choose an action", kb)
|
||||||
|
return nil
|
||||||
|
}, "menu")
|
||||||
|
|
||||||
|
menu.NewPayload(func(ctx *laniakea.MsgContext, db laniakea.NoDB) error {
|
||||||
|
ctx.EditCallback("Settings screen", nil)
|
||||||
|
return nil
|
||||||
|
}, "settings")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Long plain-text reply
|
||||||
|
|
||||||
|
Use `AnswerLong(...)` when you want explicit splitting into multiple safe Telegram messages.
|
||||||
|
|
||||||
|
```go
|
||||||
|
plugin.NewCommand(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(...)`.
|
||||||
|
|
||||||
|
## Localized command
|
||||||
|
|
||||||
|
Attach an `L10n` store to the bot and use `ctx.Translate(...)` inside handlers.
|
||||||
|
|
||||||
|
```go
|
||||||
|
l10n := laniakea.NewL10n("en").
|
||||||
|
AddDictEntry("greeting", laniakea.DictEntry{
|
||||||
|
"en": "Hello",
|
||||||
|
"ru": "Privet",
|
||||||
|
})
|
||||||
|
|
||||||
|
bot.AddL10n(l10n)
|
||||||
|
|
||||||
|
plugin.NewCommand(func(ctx *laniakea.MsgContext, db *App) error {
|
||||||
|
ctx.Answer(ctx.Translate("greeting"))
|
||||||
|
return nil
|
||||||
|
}, "start")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Non-command update handler
|
||||||
|
|
||||||
|
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 {
|
||||||
|
if ctx.From != nil {
|
||||||
|
ctx.Logger.Infoln("inline query from", ctx.From.ID)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
This is usually cleaner than forcing non-command traffic through a command parser.
|
||||||
|
|
||||||
|
## Draft-based staged reply
|
||||||
|
|
||||||
|
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 {
|
||||||
|
draft := ctx.NewDraft()
|
||||||
|
if draft == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := draft.Push("Collecting data...\n"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := draft.Push("Formatting output...\n"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return draft.Flush()
|
||||||
|
}, "build")
|
||||||
|
```
|
||||||
|
|
||||||
|
## File upload with `tgapi`
|
||||||
|
|
||||||
|
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)
|
||||||
|
defer uploader.Close()
|
||||||
|
|
||||||
|
_, err := uploader.SendPhoto(tgapi.UploadPhotoP{
|
||||||
|
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`.
|
||||||
|
|
||||||
|
## Strict payload mode
|
||||||
|
|
||||||
|
Enable strict payload decoding if you want callback payload formats to be enforced consistently.
|
||||||
|
|
||||||
|
```go
|
||||||
|
opts := (&laniakea.BotOpts{}).
|
||||||
|
SetToken("TOKEN").
|
||||||
|
SetStrictPayloadType(true)
|
||||||
|
```
|
||||||
|
|
||||||
|
Or after bot creation:
|
||||||
|
|
||||||
|
```go
|
||||||
|
bot.SetStrictPayloadType(true)
|
||||||
|
```
|
||||||
|
|
||||||
|
This is most useful when you want payload-type mismatches to fail loudly instead of being decoded tolerantly.
|
||||||
|
|
||||||
|
## Related pages
|
||||||
|
|
||||||
|
- [[Commands-and-Plugins]]
|
||||||
|
- [[Middleware]]
|
||||||
|
- [[Inline-Keyboards-and-Payloads]]
|
||||||
|
- [[Drafts]]
|
||||||
|
- [[Localization]]
|
||||||
|
|||||||
+187
@@ -0,0 +1,187 @@
|
|||||||
|
# Runners
|
||||||
|
|
||||||
|
Runners are background or one-time tasks that start with the bot and live alongside update processing. They are useful for periodic cleanup, maintenance jobs, health checks, and startup work that belongs to the bot process but not to any single update handler.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Runners are registered with `Bot.AddRunner(...)` and executed from `RunWithContext(...)`.
|
||||||
|
|
||||||
|
Each runner is built from:
|
||||||
|
- a name;
|
||||||
|
- a function `func(*Bot[T]) error`;
|
||||||
|
- execution flags configured through builder methods.
|
||||||
|
|
||||||
|
Main builder methods:
|
||||||
|
- `Onetime(bool)`
|
||||||
|
- `Async(bool)`
|
||||||
|
- `Timeout(duration)`
|
||||||
|
|
||||||
|
## Creating a runner
|
||||||
|
|
||||||
|
Use `NewRunner(name, fn)` to create a runner.
|
||||||
|
|
||||||
|
```go
|
||||||
|
cleanup := laniakea.NewRunner("cleanup", func(bot *laniakea.Bot[*App]) error {
|
||||||
|
return cleanupExpiredState(bot.GetDBContext())
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
By default, a new runner is:
|
||||||
|
- asynchronous;
|
||||||
|
- not one-time;
|
||||||
|
- configured with zero timeout.
|
||||||
|
|
||||||
|
That default means you almost always want to finish configuration before adding it to the bot.
|
||||||
|
|
||||||
|
## Runner execution modes
|
||||||
|
|
||||||
|
There are three meaningful configurations.
|
||||||
|
|
||||||
|
### One-time synchronous
|
||||||
|
|
||||||
|
```go
|
||||||
|
runner := laniakea.NewRunner("warmup", fn).
|
||||||
|
Onetime(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).
|
||||||
|
Onetime(true)
|
||||||
|
```
|
||||||
|
|
||||||
|
Behavior:
|
||||||
|
- runs once;
|
||||||
|
- starts in a goroutine;
|
||||||
|
- does not block bot startup.
|
||||||
|
|
||||||
|
Use this for fire-and-forget startup work that is useful but not required before handling updates.
|
||||||
|
|
||||||
|
### Repeating asynchronous
|
||||||
|
|
||||||
|
```go
|
||||||
|
runner := laniakea.NewRunner("cleanup", fn).
|
||||||
|
Timeout(time.Minute)
|
||||||
|
```
|
||||||
|
|
||||||
|
Behavior:
|
||||||
|
- runs on a ticker;
|
||||||
|
- keeps running until `ctx.Done()` from `RunWithContext(...)`;
|
||||||
|
- is awaited during graceful shutdown.
|
||||||
|
|
||||||
|
Use this for recurring background jobs.
|
||||||
|
|
||||||
|
## Invalid configuration
|
||||||
|
|
||||||
|
One configuration is intentionally treated as invalid:
|
||||||
|
|
||||||
|
- `Onetime(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.
|
||||||
|
|
||||||
|
## Registration
|
||||||
|
|
||||||
|
Add runners with `Bot.AddRunner(...)`.
|
||||||
|
|
||||||
|
```go
|
||||||
|
bot.AddRunner(cleanupRunner)
|
||||||
|
```
|
||||||
|
|
||||||
|
Runners with an empty name are skipped with a warning, so always give them a stable, readable name.
|
||||||
|
|
||||||
|
## Lifecycle
|
||||||
|
|
||||||
|
Runners are not started by `NewBot(...)`. They start from `RunWithContext(...)`, right before the bot begins its polling loop.
|
||||||
|
|
||||||
|
That means runner execution belongs to the bot's runtime lifecycle, not to its configuration phase.
|
||||||
|
|
||||||
|
High-level order:
|
||||||
|
1. validate bot startup;
|
||||||
|
2. start runners;
|
||||||
|
3. begin update polling;
|
||||||
|
4. process updates concurrently;
|
||||||
|
5. cancel context to stop polling and let runners exit.
|
||||||
|
|
||||||
|
For the broader bot runtime model, see [[Bot-Lifecycle]].
|
||||||
|
|
||||||
|
## Error handling
|
||||||
|
|
||||||
|
Runner functions return `error`.
|
||||||
|
|
||||||
|
When a runner returns a non-nil error:
|
||||||
|
- the bot logs a warning;
|
||||||
|
- the process continues;
|
||||||
|
- the bot does not crash automatically.
|
||||||
|
|
||||||
|
This is useful for periodic jobs where failure should be visible but not fatal.
|
||||||
|
|
||||||
|
If a runner must be fatal for startup, make it one-time synchronous and return an error that your process architecture handles outside the runner itself.
|
||||||
|
|
||||||
|
## Shutdown behavior
|
||||||
|
|
||||||
|
`RunWithContext(...)` waits for runner completion in two groups:
|
||||||
|
- one-time async runners;
|
||||||
|
- background repeating runners.
|
||||||
|
|
||||||
|
Repeating runners observe `ctx.Done()` and exit when the bot context is canceled.
|
||||||
|
|
||||||
|
This means graceful shutdown includes runner shutdown, but only if your runner function itself returns promptly and does not ignore cancellation indirectly.
|
||||||
|
|
||||||
|
## Practical patterns
|
||||||
|
|
||||||
|
### Periodic cleanup
|
||||||
|
|
||||||
|
```go
|
||||||
|
cleanup := laniakea.NewRunner("cleanup", func(bot *laniakea.Bot[*App]) error {
|
||||||
|
return bot.GetDBContext().CleanupExpired()
|
||||||
|
}).Timeout(5 * time.Minute)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Startup warmup
|
||||||
|
|
||||||
|
```go
|
||||||
|
warmup := laniakea.NewRunner("warmup", func(bot *laniakea.Bot[*App]) error {
|
||||||
|
return bot.GetDBContext().WarmCaches()
|
||||||
|
}).Onetime(true).Async(false)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Background metrics push
|
||||||
|
|
||||||
|
```go
|
||||||
|
metrics := laniakea.NewRunner("metrics", func(bot *laniakea.Bot[*App]) error {
|
||||||
|
return pushMetrics(bot.GetDBContext())
|
||||||
|
}).Timeout(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.
|
||||||
|
- Keep runner bodies small and delegate complex work to regular application services.
|
||||||
|
- Treat runner names as operational identifiers that should make sense in logs.
|
||||||
|
|
||||||
|
## Caveats
|
||||||
|
|
||||||
|
- Runners do not receive `context.Context` directly; they receive `*Bot[T]`.
|
||||||
|
- Repeating sync runners are skipped.
|
||||||
|
- Repeating async runners without timeout are skipped.
|
||||||
|
- Slow one-time sync runners delay bot startup.
|
||||||
|
|
||||||
|
## Related pages
|
||||||
|
|
||||||
|
- [[Bot-Lifecycle]]
|
||||||
|
- [[Testing-Bots-with-Laniakea]]
|
||||||
+136
-6
@@ -1,9 +1,139 @@
|
|||||||
# Semver and Releases
|
# Semver and Releases
|
||||||
|
|
||||||
This page is for maintainers and contributors who need the project’s release rules in one place.
|
This page is for maintainers and contributors who need the project's release rules in one place. It complements `SEMVER.md`, the changelog, and the repository workflow rules in `AGENTS.md`.
|
||||||
|
|
||||||
## This page should cover
|
## Sources of truth
|
||||||
- what counts as public API;
|
|
||||||
- what is considered a breaking change;
|
For release and compatibility work, keep these files aligned:
|
||||||
- how version numbers are chosen;
|
- `SEMVER.md` for the project's semantic-versioning policy;
|
||||||
- how changelog sections map to published tags.
|
- `CHANGELOG.md` for user-visible changes grouped by target version;
|
||||||
|
- `utils/version.go` for the version constants currently declared in code;
|
||||||
|
- `AGENTS.md` for repository workflow rules around changelog updates and breaking changes.
|
||||||
|
|
||||||
|
## What counts as public API
|
||||||
|
|
||||||
|
The public API includes:
|
||||||
|
- exported identifiers in package `laniakea`;
|
||||||
|
- exported identifiers in package `tgapi`;
|
||||||
|
- documented behavior in `README.md`, `README_RU.md`, and package godoc.
|
||||||
|
|
||||||
|
That means compatibility is not only about function names. Behavioral guarantees documented for callers also count.
|
||||||
|
|
||||||
|
Examples of public API surface:
|
||||||
|
- handler signatures;
|
||||||
|
- exported helper methods such as `AnswerLong(...)`;
|
||||||
|
- exported request and response DTOs in `tgapi`;
|
||||||
|
- callback payload behavior when documented as stable.
|
||||||
|
|
||||||
|
## What is a breaking change
|
||||||
|
|
||||||
|
A change requires a major version bump when it breaks existing callers or documented expectations.
|
||||||
|
|
||||||
|
Typical breaking changes include:
|
||||||
|
- renaming or removing exported identifiers;
|
||||||
|
- changing exported function or method signatures;
|
||||||
|
- changing struct field names or JSON wire compatibility in `tgapi`;
|
||||||
|
- changing documented behavior in a way that breaks existing bots.
|
||||||
|
|
||||||
|
Examples from the current release-candidate history:
|
||||||
|
- changing `CommandExecutor` to return `error`;
|
||||||
|
- changing `NewBot(...)` to return `(*Bot[T], error)`;
|
||||||
|
- renaming `BaseMenuButton` to `MenuButton`;
|
||||||
|
- changing shutdown methods such as `Bot.Close(ctx)` to `Bot.Close()`.
|
||||||
|
|
||||||
|
## How version numbers are chosen
|
||||||
|
|
||||||
|
The project follows semantic versioning with prerelease builds.
|
||||||
|
|
||||||
|
In broad terms:
|
||||||
|
- major version: required for breaking public API changes;
|
||||||
|
- minor version: backward-compatible additions;
|
||||||
|
- 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):
|
||||||
|
- `VersionString`
|
||||||
|
- `VersionMajor`
|
||||||
|
- `VersionMinor`
|
||||||
|
- `VersionPatch`
|
||||||
|
- `VersionBeta`
|
||||||
|
|
||||||
|
## Release candidates
|
||||||
|
|
||||||
|
The project is currently in the `1.0.0-rc.N` phase.
|
||||||
|
|
||||||
|
This means:
|
||||||
|
- API adjustments can still happen before `v1.0.0`;
|
||||||
|
- changelog sections should still be written carefully and explicitly;
|
||||||
|
- once `v1.0.0` is released, breaking changes should require a new major version under the documented policy.
|
||||||
|
|
||||||
|
Even during RCs, treating compatibility seriously is still useful because users may already be building real bots against these versions.
|
||||||
|
|
||||||
|
## Changelog mapping
|
||||||
|
|
||||||
|
Each user-visible code or documentation change in the main repository should be recorded in `CHANGELOG.md` under the next target version section.
|
||||||
|
|
||||||
|
Repository rules currently require:
|
||||||
|
- changes in the main repository update `CHANGELOG.md`;
|
||||||
|
- wiki-only changes in `.wiki/` do not require main changelog updates;
|
||||||
|
- changelog entries must go into the section for the next version after the latest published tag;
|
||||||
|
- the target changelog version must match `utils/version.go`.
|
||||||
|
|
||||||
|
This keeps three things synchronized:
|
||||||
|
- the last published version in git tags;
|
||||||
|
- the declared next version in `CHANGELOG.md`;
|
||||||
|
- the version constants in code.
|
||||||
|
|
||||||
|
## Latest published tag versus next version
|
||||||
|
|
||||||
|
The repository workflow distinguishes between:
|
||||||
|
- the latest published git tag;
|
||||||
|
- the next unreleased version section in `CHANGELOG.md`;
|
||||||
|
- the version currently declared in `utils/version.go`.
|
||||||
|
|
||||||
|
The intended rule is:
|
||||||
|
- if the latest published tag is `vX.Y.Z`, new work should land in the next version section, not the already published one;
|
||||||
|
- `utils/version.go` should declare that same unreleased target version.
|
||||||
|
|
||||||
|
If these drift apart, the version story becomes ambiguous for users and maintainers.
|
||||||
|
|
||||||
|
## Breaking-change policy during normal development
|
||||||
|
|
||||||
|
Repository workflow currently forbids unapproved breaking changes unless the target version is a new major version.
|
||||||
|
|
||||||
|
In practice, when a breaking change is requested, the preferred options are:
|
||||||
|
1. avoid the breaking change;
|
||||||
|
2. add a backward-compatible alternative;
|
||||||
|
3. bump the major version first, then make the break.
|
||||||
|
|
||||||
|
This is especially relevant for:
|
||||||
|
- handler signatures;
|
||||||
|
- bot and plugin lifecycle APIs;
|
||||||
|
- `tgapi` DTO and wire-format compatibility;
|
||||||
|
- callback payload defaults and semantics.
|
||||||
|
|
||||||
|
## What usually belongs in a changelog entry
|
||||||
|
|
||||||
|
Good changelog entries describe user-visible effects such as:
|
||||||
|
- new helpers or methods;
|
||||||
|
- fixed runtime behavior;
|
||||||
|
- changed defaults;
|
||||||
|
- renamed or removed APIs;
|
||||||
|
- newly enforced validation or decoding behavior.
|
||||||
|
|
||||||
|
Low-level internal refactors without user-visible effects usually do not need prominent changelog language unless they affect documented guarantees.
|
||||||
|
|
||||||
|
## Maintainer checklist before release-related edits
|
||||||
|
|
||||||
|
Before editing version or changelog data:
|
||||||
|
|
||||||
|
1. inspect the latest published git tag;
|
||||||
|
2. inspect the target section in `CHANGELOG.md`;
|
||||||
|
3. inspect `utils/version.go`;
|
||||||
|
4. decide whether the change is additive, fixing, or breaking;
|
||||||
|
5. confirm that the chosen version level matches that impact.
|
||||||
|
|
||||||
|
## Related pages
|
||||||
|
|
||||||
|
- [[Migration]] for upgrade guidance across release candidates
|
||||||
|
- [[FAQ]] for rationale behind some API decisions
|
||||||
|
|||||||
@@ -0,0 +1,185 @@
|
|||||||
|
# Testing Bots with Laniakea
|
||||||
|
|
||||||
|
Laniakea is very testable with ordinary Go tests. The repository itself already uses unit-style tests for handlers, context helpers, argument validation, long replies, runners, and request-shape assertions. This page collects the most useful testing patterns.
|
||||||
|
|
||||||
|
## What to test
|
||||||
|
|
||||||
|
Good test targets include:
|
||||||
|
- handler return behavior;
|
||||||
|
- command argument validation;
|
||||||
|
- middleware decisions;
|
||||||
|
- long-message splitting;
|
||||||
|
- callback payload behavior;
|
||||||
|
- update routing;
|
||||||
|
- runner shutdown behavior;
|
||||||
|
- request bodies sent to Telegram methods.
|
||||||
|
|
||||||
|
## General testing strategy
|
||||||
|
|
||||||
|
The most practical approach is:
|
||||||
|
|
||||||
|
1. isolate one behavior;
|
||||||
|
2. create a small bot, plugin, or `MsgContext`;
|
||||||
|
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
|
||||||
|
|
||||||
|
Many helper methods can be tested by constructing a `MsgContext` directly.
|
||||||
|
|
||||||
|
Common ingredients:
|
||||||
|
- a fake `tgapi.API` with a custom `http.Client`;
|
||||||
|
- a synthetic `tgapi.Message` with `Chat.ID`;
|
||||||
|
- a logger created with `slog.CreateLogger()`.
|
||||||
|
|
||||||
|
Example pattern:
|
||||||
|
|
||||||
|
```go
|
||||||
|
ctx := &laniakea.MsgContext{
|
||||||
|
Api: api,
|
||||||
|
Msg: &tgapi.Message{Chat: &tgapi.Chat{ID: 42, Type: string(tgapi.ChatTypePrivate)}},
|
||||||
|
Logger: slog.CreateLogger(),
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This is enough to test helpers such as:
|
||||||
|
- `Answer(...)`
|
||||||
|
- `AnswerLong(...)`
|
||||||
|
- `AnswerPhoto(...)`
|
||||||
|
- `KeyboardLong(...)`
|
||||||
|
- `NewDraft()`
|
||||||
|
|
||||||
|
## Testing outgoing request bodies
|
||||||
|
|
||||||
|
When you care about exact Telegram request shape, use a fake `http.Client` transport and inspect `req.Body`.
|
||||||
|
|
||||||
|
This is especially useful for:
|
||||||
|
- optional fields;
|
||||||
|
- parse modes;
|
||||||
|
- direct message topic IDs;
|
||||||
|
- split long-message requests;
|
||||||
|
- keyboard attachment behavior.
|
||||||
|
|
||||||
|
The repository already uses this pattern extensively for `MsgContext` helper tests.
|
||||||
|
|
||||||
|
## Testing long replies
|
||||||
|
|
||||||
|
Long plain-text helpers are a good candidate for regression tests.
|
||||||
|
|
||||||
|
Things worth checking:
|
||||||
|
- the text is split into Telegram-safe parts;
|
||||||
|
- joining the parts reconstructs the original text;
|
||||||
|
- keyboards only appear on the final chunk in `KeyboardLong(...)`;
|
||||||
|
- no request is sent when validation fails before sending.
|
||||||
|
|
||||||
|
## Testing command argument validation
|
||||||
|
|
||||||
|
Command argument validation can often be tested without a full bot instance.
|
||||||
|
|
||||||
|
Create a command and call its validation path indirectly or through higher-level behavior.
|
||||||
|
|
||||||
|
Useful cases:
|
||||||
|
- required argument positions;
|
||||||
|
- integer and boolean regex validation;
|
||||||
|
- extra arguments beyond the declared set.
|
||||||
|
|
||||||
|
## Testing update routing
|
||||||
|
|
||||||
|
For routing tests, create a small bot with:
|
||||||
|
- a logger;
|
||||||
|
- one or more registered plugins;
|
||||||
|
- synthetic `tgapi.Update` values.
|
||||||
|
|
||||||
|
Then call the bot's handling path in a focused test and assert:
|
||||||
|
- which handler was called;
|
||||||
|
- what `MsgContext` fields were populated;
|
||||||
|
- whether context mutations leaked across plugins.
|
||||||
|
|
||||||
|
This is particularly useful for:
|
||||||
|
- non-command update handlers;
|
||||||
|
- callback flows;
|
||||||
|
- channel post handling;
|
||||||
|
- update-type isolation.
|
||||||
|
|
||||||
|
## Testing runners
|
||||||
|
|
||||||
|
Runners are easy to test without starting a full bot process.
|
||||||
|
|
||||||
|
Useful assertions:
|
||||||
|
- one-time synchronous runners run exactly once;
|
||||||
|
- background runners stop after context cancellation;
|
||||||
|
- misconfigured runners are skipped.
|
||||||
|
|
||||||
|
A typical pattern is:
|
||||||
|
- create a context with cancel;
|
||||||
|
- register a runner that increments an atomic counter;
|
||||||
|
- call `ExecRunners(ctx)`;
|
||||||
|
- cancel the context and wait for runner wait groups.
|
||||||
|
|
||||||
|
## Testing error handling
|
||||||
|
|
||||||
|
When testing error behavior, decide which layer you are verifying:
|
||||||
|
- handler returns an error;
|
||||||
|
- middleware replies manually and stops;
|
||||||
|
- helper validation prevents sending.
|
||||||
|
|
||||||
|
For centralized error flow, assert both:
|
||||||
|
- the user-facing response shape if relevant;
|
||||||
|
- the logical error path taken by the handler or helper.
|
||||||
|
|
||||||
|
## Testing with fake Telegram APIs
|
||||||
|
|
||||||
|
You do not need a real Telegram bot token for most unit tests.
|
||||||
|
|
||||||
|
A fake `http.Client` with a custom transport is enough to:
|
||||||
|
- return canned Telegram responses;
|
||||||
|
- inspect raw JSON request bodies;
|
||||||
|
- verify request counts;
|
||||||
|
- simulate failures.
|
||||||
|
|
||||||
|
This approach is used throughout the repository for API, context, and command-generation tests.
|
||||||
|
|
||||||
|
## Recommended test shapes
|
||||||
|
|
||||||
|
### Small unit tests
|
||||||
|
|
||||||
|
Best for:
|
||||||
|
- helper validation;
|
||||||
|
- argument parsing;
|
||||||
|
- payload encoding;
|
||||||
|
- simple middleware behavior.
|
||||||
|
|
||||||
|
### Focused integration-style tests
|
||||||
|
|
||||||
|
Best for:
|
||||||
|
- command routing;
|
||||||
|
- callback flows;
|
||||||
|
- request serialization;
|
||||||
|
- long-reply behavior;
|
||||||
|
- runner lifecycle behavior.
|
||||||
|
|
||||||
|
## Regression-test ideas
|
||||||
|
|
||||||
|
When you fix a bug, consider adding a test for:
|
||||||
|
- nil or missing context fields;
|
||||||
|
- oversized message text or caption;
|
||||||
|
- update types with unusual payload shape;
|
||||||
|
- strict versus tolerant payload decoding;
|
||||||
|
- shutdown and cancellation timing;
|
||||||
|
- plugin snapshot behavior after registration.
|
||||||
|
|
||||||
|
## Recommendations
|
||||||
|
|
||||||
|
- Prefer fast, deterministic tests over large end-to-end setups.
|
||||||
|
- Use fake HTTP transports to test Telegram-facing behavior.
|
||||||
|
- Keep update fixtures small and purpose-built.
|
||||||
|
- Add regression tests for every bug you fix in routing, validation, or payload handling.
|
||||||
|
|
||||||
|
## Related pages
|
||||||
|
|
||||||
|
- [[Commands-and-Plugins]]
|
||||||
|
- [[Error-Handling]]
|
||||||
|
- [[Runners]]
|
||||||
|
- [[MsgContext]]
|
||||||
+255
-7
@@ -1,10 +1,258 @@
|
|||||||
# tgapi Overview
|
# tgapi Overview
|
||||||
|
|
||||||
`tgapi` is the lower-level Telegram API layer used by Laniakea.
|
`tgapi` is the low-level Telegram Bot API layer used under Laniakea’s 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.
|
||||||
|
|
||||||
## This page should cover
|
## The important split first
|
||||||
- the difference between `API` and `Uploader`;
|
|
||||||
- when to use typed helpers vs low-level request builders;
|
There are two main clients:
|
||||||
- context-aware methods;
|
|
||||||
- request/response modeling and Telegram wire compatibility;
|
- `tgapi.API` for JSON requests
|
||||||
- where `tgapi` ends and higher-level bot behavior begins.
|
- `tgapi.Uploader` for multipart file uploads
|
||||||
|
|
||||||
|
That split is intentional:
|
||||||
|
- JSON-only calls such as `SendMessage`, `GetMe`, `GetUpdates`, or `EditMessageText` go through `API`;
|
||||||
|
- binary uploads such as `SendPhoto`, `SendDocument`, `SendVideo`, or webhook certificates go through `Uploader`.
|
||||||
|
|
||||||
|
If you stay on the typed method surface, you usually do not need to think about raw HTTP details at all.
|
||||||
|
|
||||||
|
## The normal layering
|
||||||
|
|
||||||
|
In practice, Laniakea has three levels:
|
||||||
|
- high-level handler helpers on `MsgContext`;
|
||||||
|
- runtime structure on `Bot`, plugins, and middleware;
|
||||||
|
- low-level Telegram access in `tgapi`.
|
||||||
|
|
||||||
|
`tgapi` is not a separate product inside the repository. It is the lower-level layer that the high-level bot runtime is already using under the hood.
|
||||||
|
|
||||||
|
## When to use `tgapi` directly
|
||||||
|
|
||||||
|
Use `tgapi` directly when:
|
||||||
|
- a `MsgContext` 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.
|
||||||
|
|
||||||
|
Stay on high-level Laniakea helpers when:
|
||||||
|
- you only need normal replies or callback answers;
|
||||||
|
- the work belongs inside command or payload handlers;
|
||||||
|
- keyboard, draft, localization, and context helpers already cover the use case.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
## Typed methods first
|
||||||
|
|
||||||
|
The normal `tgapi` workflow is method-specific and typed.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```go
|
||||||
|
api := tgapi.NewAPI(tgapi.NewAPIOpts(token))
|
||||||
|
defer api.Close()
|
||||||
|
|
||||||
|
msg, err := api.SendMessage(tgapi.SendMessageP{
|
||||||
|
ChatID: chatID,
|
||||||
|
Text: "Hello",
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
This is preferred over building raw requests manually because:
|
||||||
|
- the method name is fixed correctly;
|
||||||
|
- the parameter type matches the Telegram method;
|
||||||
|
- the result type is explicit;
|
||||||
|
- per-chat rate limiting can be wired by the helper.
|
||||||
|
|
||||||
|
Most methods also have a context-aware variant:
|
||||||
|
|
||||||
|
```go
|
||||||
|
msg, err := api.SendMessageWithContext(ctx, params)
|
||||||
|
```
|
||||||
|
|
||||||
|
Use the context-aware variants when cancellation, deadlines, or graceful shutdown behavior matters.
|
||||||
|
|
||||||
|
## `APIOpts`
|
||||||
|
|
||||||
|
`API` is configured through `NewAPIOpts(token)`.
|
||||||
|
|
||||||
|
Useful options include:
|
||||||
|
- `SetHTTPClient(...)`
|
||||||
|
- `UseTestServer(...)`
|
||||||
|
- `SetAPIUrl(...)`
|
||||||
|
- `SetLimiter(...)`
|
||||||
|
- `SetLimiterDrop(...)`
|
||||||
|
|
||||||
|
These options are also what `Bot` wires internally during construction, so understanding them helps even if you mostly use the high-level bot runtime.
|
||||||
|
|
||||||
|
## `API` behavior and responsibilities
|
||||||
|
|
||||||
|
`tgapi.API` handles:
|
||||||
|
- JSON request encoding;
|
||||||
|
- HTTP execution;
|
||||||
|
- internal worker-pool scheduling;
|
||||||
|
- optional rate limiting;
|
||||||
|
- Telegram `429 retry_after` backoff and retry;
|
||||||
|
- response decoding into typed results.
|
||||||
|
|
||||||
|
A few practical points matter:
|
||||||
|
|
||||||
|
- `NewAPIOpts(token)` is the normal constructor entry point.
|
||||||
|
- if you do not provide an HTTP client, `API` creates one with a 45-second timeout.
|
||||||
|
- `SetLimiter(...)` connects a `utils.RateLimiter`.
|
||||||
|
- `SetLimiterDrop(true)` switches from waiting mode to immediate `ErrDropOverflow` behavior when the limiter is full.
|
||||||
|
- `Close()` must be called to stop the worker pool and close idle connections.
|
||||||
|
|
||||||
|
## Worker pool behavior
|
||||||
|
|
||||||
|
`tgapi.API` executes requests through an internal worker pool.
|
||||||
|
|
||||||
|
That means:
|
||||||
|
- even typed API calls go through a managed execution layer;
|
||||||
|
- `Close()` is important because it stops that pool cleanly;
|
||||||
|
- context-aware variants are the right choice when cancellation should interrupt waiting work.
|
||||||
|
|
||||||
|
This is one reason `Close()` is not optional in long-lived code.
|
||||||
|
|
||||||
|
## `Uploader` behavior and responsibilities
|
||||||
|
|
||||||
|
`tgapi.Uploader` is the multipart companion to `API`.
|
||||||
|
|
||||||
|
Use it when Telegram expects an uploaded file body rather than a `file_id` or URL. The uploader reuses the underlying API client, including its HTTP client and limiter behavior.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```go
|
||||||
|
api := tgapi.NewAPI(tgapi.NewAPIOpts(token))
|
||||||
|
defer api.Close()
|
||||||
|
|
||||||
|
uploader := tgapi.NewUploader(api)
|
||||||
|
defer uploader.Close()
|
||||||
|
|
||||||
|
photo := tgapi.NewUploaderFile("cat.jpg", data)
|
||||||
|
msg, err := uploader.SendPhoto(tgapi.UploadPhotoP{
|
||||||
|
ChatID: chatID,
|
||||||
|
Caption: "Cat",
|
||||||
|
}, photo)
|
||||||
|
```
|
||||||
|
|
||||||
|
`NewUploaderFile(name, data)` auto-detects a Telegram upload field from the file extension. If needed, override it with `SetType(...)`.
|
||||||
|
|
||||||
|
Call `Uploader.Close()` when you own a standalone uploader instance. When you use the uploader created by `laniakea.Bot`, `bot.Close()` handles that lifecycle for you.
|
||||||
|
|
||||||
|
## Choosing between `API` and `Uploader`
|
||||||
|
|
||||||
|
Use `API` when:
|
||||||
|
- Telegram accepts JSON-only parameters;
|
||||||
|
- you are sending `file_id` or URL references instead of new binary content.
|
||||||
|
|
||||||
|
Use `Uploader` when:
|
||||||
|
- Telegram expects multipart file upload;
|
||||||
|
- you are sending new binary content from memory or disk;
|
||||||
|
- the method has an `Upload*P` parameter type.
|
||||||
|
|
||||||
|
## File downloads
|
||||||
|
|
||||||
|
`tgapi` also covers downloads from Telegram’s file server.
|
||||||
|
|
||||||
|
The usual flow is:
|
||||||
|
1. call `GetFile(...)` to obtain file metadata and `FilePath`;
|
||||||
|
2. download via one of the file-link helpers.
|
||||||
|
|
||||||
|
Use:
|
||||||
|
- `GetFileByLink(...)` when you want the full file in memory as `[]byte`;
|
||||||
|
- `OpenFileByLink(...)` when you want a streaming `io.ReadCloser`.
|
||||||
|
|
||||||
|
Prefer the streaming helpers for large files so you do not buffer everything into memory at once.
|
||||||
|
|
||||||
|
## Low-level request builders
|
||||||
|
|
||||||
|
The raw request builders exist as escape hatches, not as the recommended day-to-day API.
|
||||||
|
|
||||||
|
Available helpers:
|
||||||
|
- `NewRequest(...)`
|
||||||
|
- `NewRequestWithChatID(...)`
|
||||||
|
- `NewUploaderRequest(...)`
|
||||||
|
- `NewUploaderRequestWithChatID(...)`
|
||||||
|
|
||||||
|
The `WithChatID` variants matter because chat ID is used for per-chat rate limiting.
|
||||||
|
|
||||||
|
## Low-level escape hatches
|
||||||
|
|
||||||
|
For advanced cases, `tgapi` keeps raw request builders public:
|
||||||
|
|
||||||
|
- `tgapi.NewRequest(...)`
|
||||||
|
- `tgapi.NewRequestWithChatID(...)`
|
||||||
|
- `tgapi.NewUploaderRequest(...)`
|
||||||
|
- `tgapi.NewUploaderRequestWithChatID(...)`
|
||||||
|
|
||||||
|
These are intentionally lower-level than the typed helpers.
|
||||||
|
|
||||||
|
Use them only when:
|
||||||
|
- the project has not added a typed wrapper for a Telegram method yet;
|
||||||
|
- you need a one-off method quickly and are comfortable supplying the exact method name;
|
||||||
|
- you can guarantee that the request and response types actually match Telegram’s schema.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```go
|
||||||
|
req := tgapi.NewRequest[bool]("deleteWebhook", tgapi.DeleteWebhookP{
|
||||||
|
DropPendingUpdates: true,
|
||||||
|
})
|
||||||
|
ok, err := req.Do(api)
|
||||||
|
```
|
||||||
|
|
||||||
|
The `WithChatID` variants matter when the request should participate in per-chat rate limiting.
|
||||||
|
|
||||||
|
## Error model
|
||||||
|
|
||||||
|
At the `tgapi` layer, most failures come back as ordinary Go errors.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
- HTTP transport failures;
|
||||||
|
- JSON parsing failures;
|
||||||
|
- Telegram API errors formatted as `"[code] description"`;
|
||||||
|
- limiter-related failures such as drop-overflow mode;
|
||||||
|
- context cancellation and deadline errors from `WithContext` variants.
|
||||||
|
|
||||||
|
Telegram `429 retry_after` is handled specially:
|
||||||
|
- the limiter lock is updated;
|
||||||
|
- the client waits;
|
||||||
|
- the request is retried automatically unless the context is canceled.
|
||||||
|
|
||||||
|
Related page:
|
||||||
|
- [[Rate-Limiting]]
|
||||||
|
|
||||||
|
## Testability
|
||||||
|
|
||||||
|
`tgapi` is easy to test with a fake `http.Client` transport.
|
||||||
|
|
||||||
|
This is a strong pattern for:
|
||||||
|
- asserting JSON request bodies;
|
||||||
|
- simulating Telegram API responses;
|
||||||
|
- testing method wrappers without real network calls.
|
||||||
|
|
||||||
|
The repository already uses that style for both `API` and uploader-related tests.
|
||||||
|
|
||||||
|
Related page:
|
||||||
|
- [[Testing-Bots-with-Laniakea]]
|
||||||
|
|
||||||
|
## `tgapi` vs high-level Laniakea
|
||||||
|
|
||||||
|
As a rule of thumb:
|
||||||
|
|
||||||
|
- use `MsgContext` 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.
|
||||||
|
|
||||||
|
That boundary keeps normal bot code ergonomic without hiding Telegram-specific capabilities from advanced users.
|
||||||
|
|
||||||
|
## Related pages
|
||||||
|
|
||||||
|
- [[Getting-Started]] for the normal high-level bot setup path.
|
||||||
|
- [[MsgContext]] 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.
|
||||||
|
|||||||
Reference in New Issue
Block a user