diff --git a/CHANGELOG.md b/CHANGELOG.md index ede2df1..3769cae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,8 +10,15 @@ - Renamed final public APIs to idiomatic names before the stable release: `RunWebhookWithContext(...)`, `RunWebhook(...)`, `CloseWebhook()`, `BotWebhookOpts`, `NewBotWebhookOpts()`, `SetWebhookLogger(...)`, and `GetWebhookLogger()`. - Renamed plugin builder helpers from `NewCommand(...)` and `NewScene(...)` to `Command(...)` and `Scene(...)`; the surviving `NewCommand(...)` takes the command string before the executor. - Renamed command argument value constants to `CommandValueString`, `CommandValueInt`, `CommandValueBool`, and `CommandValueAny`; `NewCommandArg(...)` now defaults to unvalidated `CommandValueAny`. -- Renamed runner builders from `Onetime(...)` and `Timeout(...)` to `Once(...)` and `Every(...)`. +- Renamed runner builders from `Onetime(...)` and `Timeout(...)` to `Every(...)` and `Async(...)`; `Runner.Once()` is removed. Use the default configuration (every=0, async=true) for a fire-and-forget goroutine, or `Async(false)` for a synchronous blocking one-shot. - Renamed remaining public acronym/casing outliers including `AnswerCallback...`, `ParseMarkdownV2`, `ParseMarkdown`, `GetChatMemberCount`, `DropRateLimitOverflow`, `SetDropRateLimitOverflow`, and inline keyboard builder APIs. +- Renamed `Observer` event delivery methods `OnReceiveUpdate` → `OnUpdateReceived` and `OnHandledUpdate` → `OnUpdateHandled` to match the `UpdateReceivedEvent`/`UpdateHandledEvent` names and the `OnX` pattern of all other observer methods. +- `Scene.PluginName` is now unexported; it is assigned by the framework during plugin registration and must not be set by callers. +- `SceneSession.Data` is now unexported; use the `Set`/`Get`/`HasData`/`ClearData`/`BindData`/`SaveData` helpers instead. +- `BotPayloadType*` sentinels are now `const` instead of `var`; code that assigned to them will no longer compile. + +### Bot API 10.0 +- Added full support for Telegram Bot API 10.0 types, methods, and update kinds. ### Added - Added `MessageContext.IsCallback()` and `MessageContext.HasPhoto()` helpers for callback-aware handler code. @@ -24,6 +31,9 @@ - Added `RateLimiter.Cleanup(idleThreshold)` to evict per-chat limiter state and expired chat cooldowns; the limiter now tracks per-chat last-seen time so long-running bots can bound memory through a periodic runner. - Added cached bot identity (`Bot.userID`) populated at `NewBot` so chat-admin policies and similar lookups reuse it instead of issuing a fresh `GetMe` request. - Added `tgapi.ResponseError` so Telegram API error codes, descriptions, and response parameters remain inspectable through returned errors. +- Added nine exported webhook error sentinels — `ErrSetWebhookFailed`, `ErrBotAPINil`, `ErrBotWebhookOptsEmptyPath`, `ErrBotWebhookOptsPathNoSlash`, `ErrBotWebhookOptsPathHasQueryOrFragment`, `ErrBotWebhookOptsPathCollidesStatus`, `ErrBotWebhookTLSFilesIncomplete`, `ErrBotWebhookTLSFilesTooMany`, and `ErrStatusPathSecretRequired` — replacing the previous inline `errors.New(...)` calls so callers can match webhook startup errors with `errors.Is`. +- Added `ErrInvalidPayload` for compact payload decoding failures so callers can distinguish malformed payload bytes from other decode errors. +- Panics inside `Bot.handle` and the polling goroutine now emit an `ErrorEvent` through the observer so instrumentation sees runtime panics in addition to normal handler errors. ### Changed - Version metadata now reports the stable `v1.0.0` release instead of `v1.0.0-rc.16`. @@ -47,6 +57,10 @@ - Fixed webhook startup so empty-secret warnings are logged only after the webhook logger is initialized. - Fixed webhook startup so a logger configured through `SetWebhookLogger(...)` is preserved. - Fixed long-polling 429 handling so `getUpdates` retries use Telegram `retry_after` directly and do not inflate later transient-error backoff. +- Fixed `BotOptsFileJSON` silently dropping `PollTimeout` on round-trip; the field is now encoded and decoded correctly. +- Fixed the `tgapi.Uploader` returning an ad-hoc error string on Telegram API failures; it now returns `*tgapi.ResponseError` matching the JSON API client, so `errors.As(err, &respErr)` works consistently for both upload and JSON paths. +- Fixed webhook secret validation to use `subtle.ConstantTimeCompare` instead of a plain string equality check, removing the timing side-channel. +- Fixed the `/status` handler returning HTTP 403 for a wrong secret, which disclosed endpoint existence; it now returns 404 uniformly for any unauthenticated request. ### Tests - Added regression coverage proving bot-level middleware blocks still complete the observer update lifecycle. @@ -61,6 +75,11 @@ - Added regression coverage for `Draft.Push` preserving the existing message when validation rejects the candidate. - Added regression coverage for `RateLimiter.Cleanup` evicting idle chat limiters and expired chat locks while leaving active state in place. - Updated `MessageContext.Error` tests so unclassified errors stay internal-only and only `AsUserError` reaches the user. +- Added regression coverage for `BotOptsFileJSON` `PollTimeout` round-trip. +- Added regression coverage proving the `tgapi.Uploader` surfaces `*tgapi.ResponseError` for Telegram 4xx responses. +- Added regression coverage proving a panic inside `Bot.handle` emits an `ErrorEvent` through the observer. +- Added regression coverage for the webhook `/status` endpoint rejecting wrong and same-length-but-different secrets with HTTP 404, and accepting the correct secret. +- Added table-driven regression coverage for `parseCommand` with `/cmd@botname` stripping, bare commands, commands with arguments, and empty input. ## v1.0.0-rc.16 diff --git a/README.md b/README.md index 91cf0c1..286a26e 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ import ( // It receives two parameters: // - ctx: the message context (contains info about the message, sender, chat, etc.) // - data: your shared application data (here we use NoData, a placeholder for no shared data) -func echo(ctx *laniakea.MsgContext, data laniakea.NoData) error { +func echo(ctx *laniakea.MessageContext, data laniakea.NoData) error { // Answer the user with the text they sent, without any command prefix. // ctx.Text contains the user's message with the command part stripped off. ctx.Answer(ctx.Text) // User input WITHOUT command @@ -85,7 +85,7 @@ func main() { // 5. Add another command using an anonymous function (closure). // This command simply replies "Pong" when the user sends "/ping". - p.Command("ping", func(ctx *laniakea.MsgContext, data laniakea.NoData) error { + p.Command("ping", func(ctx *laniakea.MessageContext, data laniakea.NoData) error { ctx.Answer("Pong") return nil }) @@ -112,8 +112,8 @@ func main() { 1. `BotOpts`: Holds configuration like the API token. 2. `NewBot[T]`: Creates a bot instance. The type parameter T allows you to pass custom shared application data (for example, *sql.DB or a service container) that will be available in all handlers. Use laniakea.NoData if you don't need it. 3. `NewPlugin`: Creates a logical group for commands and middlewares. -4. `Command`: Creates and registers a command. The first argument is the command name without the slash, the second is the handler function (`func(*MsgContext, T) error`). -5. **Handler Functions**: Receive *MsgContext (message details, methods like Answer) and your custom application data T, and return an error for centralized error handling. +4. `Command`: Creates and registers a command. The first argument is the command name without the slash, the second is the handler function (`func(*MessageContext, T) error`). +5. **Handler Functions**: Receive *MessageContext (message details, methods like Answer) and your custom application data T, and return an error for centralized error handling. 6. `SetErrorTemplate`: Sets a template for error messages. The %s placeholder is replaced by the actual error. 7. `AutoGenerateCommands`: Registers plugin-defined commands with Telegram across the supported scopes. 8. `Run()`: Starts the bot's update polling loop and returns an error if startup or polling fails. @@ -181,14 +181,14 @@ bot.AddPlugins(plugin) A command is a function that handles a specific bot command (e.g., /start). ```go -func myHandler(ctx *laniakea.MsgContext, db *MyDB) error { +func myHandler(ctx *laniakea.MessageContext, db *MyDB) error { // Access command arguments via ctx.Args ([]string) // Reply to the user: ctx.Answer("some text") return nil } ``` -### MsgContext +### MessageContext Provides access to the incoming message and useful reply methods: @@ -200,8 +200,8 @@ Provides access to the incoming message and useful reply methods: - `KeyboardMarkdown(text string, keyboard *InlineKeyboard) *AnswerMessage`: Sends a message formatted with MarkdownV2 (you handle escaping) and inline keyboard. - `AnswerPhoto(photoID, text string) *AnswerMessage`: Sends a message with photo with parse_mode none. - `AnswerPhotoMarkdown(photoID, text string) *AnswerMessage`: Sends a photo with MarkdownV2 caption (you handle escaping). -- `EditCallback(text string)`: Edits message with parse_mode none after clicking inline button. -- `EditCallbackMarkdown(text string)`: Edits a message formatted with MarkdownV2 (you handle escaping) after clicking inline button. +- `EditCallback(text string, keyboard *InlineKeyboard) *AnswerMessage`: Edits message with parse_mode none after clicking inline button. +- `EditCallbackMarkdown(text string, keyboard *InlineKeyboard) *AnswerMessage`: Edits a message formatted with MarkdownV2 (you handle escaping) after clicking inline button. - `SendAction(action tgapi.ChatActionType)`: Sends a “typing”, “uploading photo”, etc., action. - Fields: `Text`, `Args`, `From`, `FromID`, `Msg`, `InlineMsgID`, `CallbackQueryID`, etc. - And more methods and fields! @@ -268,6 +268,39 @@ plugin.Scene("signup"). - Use `SceneContext.SaveData(...)` and `SceneContext.BindData(...)` for JSON session state. - Use `SceneScopeUser`, `SceneScopeChat`, or `SceneScopeUserChat` depending on how widely a conversation should be shared. +## ⏱️ Runners + +Runners are background tasks that execute alongside the bot runtime. They are registered before the bot starts and launched automatically when the bot starts. + +```go +import "time" + +// One-shot runner — fires once in a goroutine when the bot starts (default). +bot.AddRunner( + laniakea.NewRunner("seed-cache", func(b *laniakea.Bot[*MyDB]) error { + return b.GetAppData().SeedCache() + }), +) + +// Periodic runner — fires every 10 minutes in a goroutine. +bot.AddRunner( + laniakea.NewRunner("refresh-stats", func(b *laniakea.Bot[*MyDB]) error { + return b.GetAppData().RefreshStats() + }).Every(10 * time.Minute), +) + +// Synchronous one-shot — blocks runtime startup until it completes. +bot.AddRunner( + laniakea.NewRunner("migrate", func(b *laniakea.Bot[*MyDB]) error { + return b.GetAppData().Migrate() + }).Async(false), +) +``` + +Builder methods: +- `Async(bool) *Runner[T]` — if `true` (default), runs in a goroutine; if `false`, blocks runtime startup. +- `Every(time.Duration) *Runner[T]` — sets the repeat interval. Zero (default) means run once; positive value repeats. Periodic runners require `Async(true)`. + ## 🧩 Middleware Middleware are functions that run before a command handler. They are perfect for cross-cutting concerns like logging, access control, rate limiting, or modifying the context. @@ -275,7 +308,7 @@ Middleware are functions that run before a command handler. They are perfect for A middleware function has the same signature as a command handler, but it must return a bool: ```go -func(ctx *MsgContext, db T) bool +func(ctx *MessageContext, db T) bool ``` - If it returns true, the next middleware (or the command) will be executed. @@ -295,7 +328,7 @@ plugin.Command("ban", banUser) 1. Logging Middleware – logs every command execution. ```go -func loggingMiddleware(ctx *laniakea.MsgContext, db *MyDB) bool { +func loggingMiddleware(ctx *laniakea.MessageContext, db *MyDB) bool { log.Printf("User %d executed command: %s", ctx.FromID, ctx.Msg.Text) return true // continue to next middleware/command } @@ -303,7 +336,7 @@ func loggingMiddleware(ctx *laniakea.MsgContext, db *MyDB) bool { 2. Admin-Only Middleware – restricts access to users with a specific role. ```go -func adminOnlyMiddleware(ctx *laniakea.MsgContext, db *MyDB) bool { +func adminOnlyMiddleware(ctx *laniakea.MessageContext, db *MyDB) bool { if !db.IsAdmin(ctx.FromID) { // assume db has IsAdmin method ctx.Answer("⛔ Access denied. Admins only.") return false // stop execution @@ -313,7 +346,7 @@ func adminOnlyMiddleware(ctx *laniakea.MsgContext, db *MyDB) bool { ``` ### Important Notes -- Middleware can modify the MsgContext (e.g., add custom fields) before the command runs. +- Middleware can modify the MessageContext (e.g., add custom fields) before the command runs. ## ⚙️ Advanced Configuration - **Inline Keyboards**: Build keyboards using `laniakea.NewInlineKeyboardJSON`, `laniakea.NewInlineKeyboardBase64`, or `laniakea.NewInlineKeyboard`. `Bot.SetPayloadType(...)` defines the default payload format, and `InlineKeyboard.SetPayloadType(...)` overrides it for one keyboard. diff --git a/README_RU.md b/README_RU.md index 8e22937..1f91bc7 100644 --- a/README_RU.md +++ b/README_RU.md @@ -56,7 +56,7 @@ import ( // Она получает два параметра: // - ctx: контекст сообщения (содержит информацию о сообщении, отправителе, чате и т.д.) // - data: ваши общие данные приложения (здесь мы используем NoData — заглушку без общих зависимостей) -func echo(ctx *laniakea.MsgContext, data laniakea.NoData) error { +func echo(ctx *laniakea.MessageContext, data laniakea.NoData) error { // Отвечаем пользователю текстом, который он прислал, без префикса команды. // ctx.Text содержит сообщение пользователя, из которого удалена часть с командой. ctx.Answer(ctx.Text) // Ввод пользователя БЕЗ команды @@ -86,7 +86,7 @@ func main() { // 5. Добавляем ещё одну команду, используя анонимную функцию (замыкание). // Эта команда просто отвечает "Pong", когда пользователь отправляет "/ping". - p.Command("ping", func(ctx *laniakea.MsgContext, data laniakea.NoData) error { + p.Command("ping", func(ctx *laniakea.MessageContext, data laniakea.NoData) error { ctx.Answer("Pong") return nil }) @@ -113,8 +113,8 @@ func main() { 1. `BotOpts`: Содержит конфигурацию, например, токен API. 2. `NewBot[T]`: Создаёт экземпляр бота. Параметр типа T позволяет передать общие данные приложения (например, *sql.DB или контейнер сервисов), которые будут доступны во всех обработчиках. Используйте laniakea.NoData, если они не нужны. 3. `NewPlugin`: Создаёт логическую группу для команд и Middleware. -4. `Command`: Создаёт и регистрирует команду. Первый аргумент — имя команды без слеша, второй — функция-обработчик (`func(*MsgContext, T) error`). -5. **Функции-обработчики**: Получают *MsgContext (детали сообщения, методы типа Answer) и ваши данные приложения типа T, а ошибку возвращают для централизованной обработки. +4. `Command`: Создаёт и регистрирует команду. Первый аргумент — имя команды без слеша, второй — функция-обработчик (`func(*MessageContext, T) error`). +5. **Функции-обработчики**: Получают *MessageContext (детали сообщения, методы типа Answer) и ваши данные приложения типа T, а ошибку возвращают для централизованной обработки. 6. `SetErrorTemplate`: Устанавливает шаблон для сообщений об ошибках. Плейсхолдер %s заменяется на текст ошибки. 7. `AutoGenerateCommands`: Регистрирует команды из плагинов в Telegram для поддерживаемых scope. 8. `Run()`: Запускает цикл опроса обновлений бота и возвращает ошибку, если старт или polling завершился неуспешно. @@ -182,14 +182,14 @@ bot.AddPlugins(plugin) Команда — это функция, которая обрабатывает конкретную команду бота (например, /start). ```go -func myHandler(ctx *laniakea.MsgContext, db *MyDB) error { +func myHandler(ctx *laniakea.MessageContext, db *MyDB) error { // Доступ к аргументам команды через ctx.Args ([]string) // Ответ пользователю: ctx.Answer("какой-то текст") return nil } ``` -### Контекст сообщения (MsgContext) +### Контекст сообщения (MessageContext) Предоставляет доступ к входящему сообщению и полезные методы для ответа: - `Answer(text string)`: Отправляет сообщение с parse_mode none. @@ -200,8 +200,8 @@ func myHandler(ctx *laniakea.MsgContext, db *MyDB) error { - `KeyboardMarkdown(text string, keyboard *InlineKeyboard) *AnswerMessage`: Отправляет сообщение, отформатированное MarkdownV2 (экранирование на вашей стороне), и Inline клавиатурой. - `AnswerPhoto(photoID, text string) *AnswerMessage`: Отправляет фотографию с подписью и parse_mode none. - `AnswerPhotoMarkdown(photoID, text string) *AnswerMessage`: Отправляет фотографию с подписью, отформатированной MarkdownV2 (экранирование на вашей стороне). -- `EditCallback(text string)`: Редактирует сообщение с `parse_mode` none после нажатия inline-кнопки. -- `EditCallbackMarkdown(text string)`: Редактирует сообщение в формате MarkdownV2 (экранирование на вашей стороне) после нажатия inline-кнопки. +- `EditCallback(text string, keyboard *InlineKeyboard) *AnswerMessage`: Редактирует сообщение с `parse_mode` none после нажатия inline-кнопки. +- `EditCallbackMarkdown(text string, keyboard *InlineKeyboard) *AnswerMessage`: Редактирует сообщение в формате MarkdownV2 (экранирование на вашей стороне) после нажатия inline-кнопки. - `SendAction(action tgapi.ChatActionType)`: Отправляет действие "печатает", "загружает фото" и т.д. - Поля: `Text`, `Args`, `From`, `FromID`, `Msg`, `InlineMsgID`, `CallbackQueryID` и другие. - И много других методов и полей! @@ -256,6 +256,39 @@ plugin.Scene("signup"). - Для JSON-состояния сцены используйте `SceneContext.SaveData(...)` и `SceneContext.BindData(...)`. - Выбирайте `SceneScopeUser`, `SceneScopeChat` или `SceneScopeUserChat` в зависимости от того, насколько широко должен разделяться диалог. +## ⏱️ Раннеры (Runners) + +Раннеры — фоновые задачи, которые выполняются вместе с bot runtime. Они регистрируются до запуска бота и автоматически запускаются при старте. + +```go +import "time" + +// Одноразовый раннер — запускается один раз в горутине при старте (по умолчанию). +bot.AddRunner( + laniakea.NewRunner("seed-cache", func(b *laniakea.Bot[*MyDB]) error { + return b.GetAppData().SeedCache() + }), +) + +// Периодический раннер — запускается каждые 10 минут в горутине. +bot.AddRunner( + laniakea.NewRunner("refresh-stats", func(b *laniakea.Bot[*MyDB]) error { + return b.GetAppData().RefreshStats() + }).Every(10 * time.Minute), +) + +// Синхронный одноразовый — блокирует запуск runtime до завершения. +bot.AddRunner( + laniakea.NewRunner("migrate", func(b *laniakea.Bot[*MyDB]) error { + return b.GetAppData().Migrate() + }).Async(false), +) +``` + +Методы builder: +- `Async(bool) *Runner[T]` — если `true` (по умолчанию), запускается в горутине; если `false`, блокирует запуск runtime. +- `Every(time.Duration) *Runner[T]` — задаёт интервал повторного запуска. Ноль (по умолчанию) означает одноразовый запуск; положительное значение — периодический. Периодические раннеры требуют `Async(true)`. + ### tgapi: API и Uploader В `tgapi` есть два клиента: @@ -272,7 +305,7 @@ Middleware — это функции, которые выполняются пе Функция middleware имеет ту же сигнатуру, что и обработчик команды, но должна возвращать bool: ```go -func(ctx *MsgContext, db T) bool +func(ctx *MessageContext, db T) bool ``` - Если возвращается true, выполняется следующий middleware (или сама команда). @@ -292,7 +325,7 @@ plugin.Command("ban", banUser) 1. Логирующий middleware – логирует каждое выполнение команды. ```go -func loggingMiddleware(ctx *laniakea.MsgContext, db *MyDB) bool { +func loggingMiddleware(ctx *laniakea.MessageContext, db *MyDB) bool { log.Printf("Пользователь %d выполнил команду: %s", ctx.FromID, ctx.Msg.Text) return true // продолжаем к следующему middleware/команде } @@ -300,7 +333,7 @@ func loggingMiddleware(ctx *laniakea.MsgContext, db *MyDB) bool { 2. Middleware только для администраторов – ограничивает доступ пользователям с определённой ролью. ```go -func adminOnlyMiddleware(ctx *laniakea.MsgContext, db *MyDB) bool { +func adminOnlyMiddleware(ctx *laniakea.MessageContext, db *MyDB) bool { if !db.IsAdmin(ctx.FromID) { // предполагается, что db имеет метод IsAdmin ctx.Answer("⛔ Доступ запрещён. Только для администраторов.") return false // останавливаем выполнение @@ -310,7 +343,7 @@ func adminOnlyMiddleware(ctx *laniakea.MsgContext, db *MyDB) bool { ``` ### Важные замечания -- Middleware может изменять MsgContext (например, добавлять пользовательские поля) перед запуском команды. +- Middleware может изменять MessageContext (например, добавлять пользовательские поля) перед запуском команды. ## ⚙️ Расширенная настройка - **Инлайн-клавиатуры**: Создавайте клавиатуры с помощью `laniakea.NewInlineKeyboardJSON`, `laniakea.NewInlineKeyboardBase64` или `laniakea.NewInlineKeyboard`. `Bot.SetPayloadType(...)` задаёт payload format по умолчанию, а `InlineKeyboard.SetPayloadType(...)` переопределяет его для конкретной клавиатуры. diff --git a/TODO.md b/TODO.md index 1e67ab4..4c422a1 100644 --- a/TODO.md +++ b/TODO.md @@ -1,52 +1,5 @@ # TODO -## v1.0.0 pre-release review - -Findings from the full-repo review against `AGENTS.md` priorities. Build, vet, tests, and lint are clean; items below are public-API and godoc hygiene before the stable tag. - -### Major — close before 1.0.0 tag - -- [X] **M1. `BotPayloadType*` are `var`, must be `const`** — `bot.go:50-59`. Public sentinels are user-mutable globals. `KeyboardButtonStyle*` in `keyboard.go:10-17` already uses `const`; match the pattern. -- [X] **M2. `Observer` method naming asymmetry** — `observer.go:147-157`. `OnReceiveUpdate` → `OnUpdateReceived`; `OnHandledUpdate` → `OnUpdateHandled` to match `UpdateReceivedEvent` / `UpdateHandledEvent` and the rest of the `OnX` pattern. Breaking after 1.0. -- [X] **M3. Uploader returns ad-hoc error string instead of `*ResponseError`** — `tgapi/uploader_api.go:183`. `tgapi/api.go:258-292` returns `*ResponseError`; uploader must do the same so `errors.As(err, &tgapi.ResponseError{})` works for upload paths too. -- [X] **M4. `BotOptsFileJSON` is missing `PollTimeout`** — `bot_opts_loader.go:35-46`, plus `FromBytes`/`ToBytes` mapping. File round-trip silently drops `PollTimeout`. -- [X] **M5. Stale `Bot.Updates` godoc** — `methods.go:11-44`. Claims "30-second timeout" and "empty slice if none"; in reality timeout is `bot.pollTimeout` and the function returns `nil` on error. -- [X] **M6. Self-contradicting `NewRandomDraftProvider` godoc** — `drafts.go:50-59`. Says "cryptographically secure random numbers" but uses `math/rand/v2` (the underlying generator type correctly notes it is not crypto-secure). -- [X] **M7. `Draft.Delete` godoc says "internal method"** — `drafts.go:190-201`. Method is exported; either rewrite the godoc with a public-intent description or unexport. -- [X] **M8. Russian comments in production code** - - `msg_handler.go:28` — "Ищем команду по точному совпадению" - - `tgapi/uploader_api.go:181` — "Повторяем запрос" -- [X] **M9. `MessageContext.Error` godoc references unexported helper** — `msg_context.go:540`. "Error is an alias for error()" — rewrite to describe the centralized handler error path and `IsUserError` gating. -- [X] **M10. `Scene` and `SceneSession` mix exported fields with setters** - - `Scene` exports `Name/Scope/Entry/PluginName` and also has `SetScope/SetEntry`; `PluginName` is framework-assigned but publicly mutable. - - `SceneSession` exports `Data []byte` and also has `Set/Get/HasData/ClearData/BindData/SaveData`. - - Pick one model per type before 1.0.0. -- [X] **M11. Constant-time compare for webhook secret** — `bot_webhook.go:296` (update handler) and `bot_webhook.go:341` (`/status`). Use `subtle.ConstantTimeCompare`. - -### Minor — can slip to 1.0.x - -- [X] Strip `// Internal helper …` godoc from unexported funcs (~23 occurrences in repo); `AGENTS.md` explicitly forbids godoc-style comments on unexported declarations without a strong reason. -- [X] `Plugin.AddCommand` godoc references unexported field `.command` — `plugins.go:48-49`. -- [X] `Runner` builder naming: `runner.Once(true)`, `runner.Async(true)` read awkwardly; consider `SetOnce`/`SetAsync` to match `Set*` on other types, or zero-arg `Once()` + paired `Repeat(every)`. -- [X] Typo in webhook error string: `bot_webhook.go:143` — "MaxConnections must between 1 and 100" (missing `be`). -- [X] `RunWebhookWithContext` uses inline `errors.New(...)` instead of `Err*` sentinels (`bot_webhook.go:131-156`); rest of the package uses sentinels from `errors.go`. -- [X] `tgapi.UpdateTypeManagedBot` (`tgapi/types.go:61`) has no godoc. -- [X] `Bot.GetAPI`, `Bot.GetUploader`, `InlineKeyboard.GetMaxRow` have no godoc. -- [X] `Bot.L10n` godoc says "Returns empty string if translation not found"; actually returns the key (`l10n.go:48-59`). -- [X] `Bot.handle` panic recovery only logs — emit `ErrorEvent` so observers see panics (`handler.go:18-23`). -- [X] `handleCallback` vs `handleMessage` differ in plugin-logger assignment: callback assigns unconditionally then falls back to bot logger (`msg_handler.go:209-212`); message only assigns if non-nil (`msg_handler.go:35-37`). Align. -- [X] `SetCallbackData` godoc says "default payload type is JSON" — actually the zero `BotPayloadType` falls through to the `default` branch (which happens to be JSON). Either document the zero-value behavior explicitly or initialize the builder with the bot's default (`keyboard.go:106-122`). -- [X] `commands.go:62-66` — empty `case CommandValueAny:` next to `default: regex = nil` looks like an incomplete switch. Merge or add a one-line comment. -- [X] `Bot.SetDebug` does not call `configMutable` unlike sibling setters; if intentional, note it in godoc. - -### Tests to add after the fixes - -- [X] `BotOptsFileJSON` round-trip for `PollTimeout` (after M4). -- [X] Uploader 4xx/429 surfaces `*tgapi.ResponseError` (after M3). -- [X] `Bot.handle` panic → observer receives `ErrorEvent` (after panic-recovery fix). -- [X] Webhook `/status` with wrong `SecretToken` returns 403 / `403`-equivalent (after M11), incl. a constant-time-compare smoke. -- [X] Table-driven `parseCommand` cases for `/cmd@botname` and stripping behavior. - --- The framework backlog has moved to the wiki. diff --git a/plugins.go b/plugins.go index 2cfc800..122a137 100644 --- a/plugins.go +++ b/plugins.go @@ -107,7 +107,7 @@ func (p *Plugin[T]) AddScene(scene *Scene[T]) *Plugin[T] { } scene.pluginName = p.name if _, exists := p.scenes[scene.name]; exists && p.logger != nil { - p.logger.Warnf("scene '%s'да already registered in plugin '%s'; overwriting", scene.name, p.name) + p.logger.Warnf("scene '%s' already registered in plugin '%s'; overwriting", scene.name, p.name) } p.scenes[scene.name] = scene return p