REPOSITORY / ScuroNeko/Laniakea

Compare commits

DIFF REPOSITORY

Compare commits

..
24 Commits
Author SHA1 Message Date
ScuroNekoandClaude Sonnet 4.6 38309e74f6 (fix): polling loop exits on HTTP client timeout, not only on context cancel
Golang lint / lint (push) Successful in 1m40s
(fix): HTTP client timeout too close to poll timeout

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 13:44:22 +03:00
ScuroNeko c2f6406819 Merge branch 'dev' of scuroneko.dev:ScuroNeko/Laniakea into dev
Golang lint / lint (pull_request) Successful in 2m59s
Golang lint / lint (push) Successful in 2m56s
2026-06-11 13:34:48 +03:00
ScuroNekoandClaude Sonnet 4.6 9e3450df31 (security): auto-generate webhook secret when unset, redact token in tgapi standalone logs (fix): webhookLogger nil panic, dead warning block (tests): update secret-path test for new auto-gen behaviour (doc): CHANGELOG v1.0.1
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 13:33:34 +03:00
ScuroNeko effd26bd9a Merge branch 'main' into dev
Golang lint / lint (pull_request) Successful in 2m42s
Golang lint / lint (push) Successful in 2m44s
2026-05-20 13:43:20 +03:00
ScuroNeko 5514665625 (doc): CHANGELOG v1.0.0, README MessageContext + Runners, TODO wiki done
Golang lint / lint (pull_request) Successful in 2m42s
Golang lint / lint (push) Successful in 1m20s
(fix): typo in scene overwrite warning
2026-05-20 13:23:31 +03:00
ScuroNeko 950ce6b88c (tests): PollTimeout round-trip, uploader ResponseError, panic ErrorEvent, status secret compare, parseCommand + botname
Golang lint / lint (push) Successful in 2m42s
Golang lint / lint (pull_request) Successful in 2m42s
(doc): TODO test checklist closed
2026-05-20 12:42:36 +03:00
ScuroNeko 8a3f2cedf2 (new): webhook + payload error sentinels
Golang lint / lint (push) Successful in 2m46s
Golang lint / lint (pull_request) Successful in 2m47s
(fix): polling panic ErrorEvent, nil-logger guard
  (refactor): inline webhook errors → sentinels, dead Runner branch
  (doc): Runner godoc, Error godoc, drafts cleanup
2026-05-20 12:30:44 +03:00
ScuroNeko 61d0b1ebb8 (fix): webhook secret constant-time compare
Golang lint / lint (pull_request) Successful in 2m53s
Golang lint / lint (push) Successful in 2m55s
(fix): observer rename, panic emits ErrorEvent
(refactor): Runner.Once removed, Scene.PluginName unexported
(doc): godoc hygiene pre-1.0.0
2026-05-20 12:06:59 +03:00
ScuroNeko 1e26d871b5 (new): Bot API 10.0
Golang lint / lint (push) Successful in 2m53s
Golang lint / lint (pull_request) Successful in 2m54s
(doc): Since: Bot API X.Y annotations across all tgapi types and methods
2026-05-19 13:42:10 +03:00
ScuroNeko affb802a7b (new): PollTimeout config, RateLimiter.Cleanup
Golang lint / lint (pull_request) Successful in 3m4s
Golang lint / lint (push) Successful in 3m5s
(fix): compact payload escape, Draft.push validation, plugin logger ownership, worker StopAndWait, getChatLimiter deadlock, runner ctx-after-tick
(refactor): remove NewPayload, buildSceneKey from sceneRuntime, unify ToJSON fallback
(tests): compact round-trip, Draft.push state, RateLimiter.Cleanup eviction
(doc): changelog v1.0.0 rewrite, NewCommand dual-use godoc
2026-05-18 17:27:14 +03:00
ScuroNeko 09fb9261df (new): compact payload type
Golang lint / lint (push) Successful in 2m55s
Golang lint / lint (pull_request) Successful in 2m55s
2026-05-12 16:23:30 +03:00
ScuroNeko 5959d69945 (fix): polling retry-after
Golang lint / lint (pull_request) Successful in 3m4s
Golang lint / lint (push) Successful in 3m8s
(tests): polling rate-limit

(doc): changelog update
2026-05-08 14:11:58 +03:00
ScuroNeko daa1b862ed (new): keyboard helpers
Golang lint / lint (push) Successful in 7m2s
Golang lint / lint (pull_request) Successful in 7m7s
(tests): payload encoding
(doc): changelog
2026-05-04 11:35:02 +03:00
ScuroNeko 7205b21fa2 (new): tgfmt package
Golang lint / lint (pull_request) Successful in 7m10s
Golang lint / lint (push) Successful in 7m13s
(fix): formatting helpers
(ci/cd): go checks
(tests): tgfmt coverage
2026-05-04 11:20:09 +03:00
ScuroNeko 6595265cb3 (ci/cd): added go test and gofmt
Golang lint / lint (push) Successful in 3m34s
2026-04-30 14:02:15 +03:00
ScuroNeko 071fc2375e (draft): telegram markdown v2 string builder
Golang lint / lint (push) Successful in 4m51s
2026-04-30 13:56:36 +03:00
ScuroNeko 269ccec007 (new): command groups
Golang lint / lint (push) Successful in 1m9s
(refactor): command cloning
(tests): command groups
(doc): changelog
2026-04-30 11:19:46 +03:00
ScuroNeko b123709f28 (fix): msg context upsert
Golang lint / lint (push) Successful in 1m8s
(tests): callback upsert coverage

(doc): changelog rules
2026-04-29 16:36:25 +03:00
ScuroNeko 4807dec6ae (fix): observer lifecycle
Golang lint / lint (push) Successful in 1m5s
(tests): webhook runtime

(doc): logger options
2026-04-29 13:11:54 +03:00
ScuroNeko a7c8d68925 Merge pull request 'v0.5.0' (#8) from dev into main
Reviewed-on: https://git.nix13.pw/ScuroNeko/Laniakea/pulls/8
2026-02-12 11:51:01 +03:00
ScuroNeko 5f17b88787 Merge pull request 'v0.3.10' (#7) from dev into main
Reviewed-on: https://git.nix13.pw/ScuroNeko/Laniakea/pulls/7
2026-02-04 17:33:51 +03:00
ScuroNeko 6d6f5738cd Merge pull request 'v0.3.2' (#3) from dev into main
Reviewed-on: https://git.nix13.pw/ScuroNeko/Laniakea/pulls/3
2026-01-29 11:47:29 +03:00
ScuroNeko fef718438a v0.3.0 2026-01-29 09:51:50 +03:00
ScuroNeko 7f248fff62 fix 2025-11-05 11:38:09 +03:00
95 changed files with 5077 additions and 1296 deletions
+17 -1
View File
@@ -1,6 +1,6 @@
name: Golang lint
run-name: Linting code
on: [push]
on: [push, pull_request]
jobs:
lint:
@@ -8,5 +8,21 @@ jobs:
steps:
- name: Checkout repository code
uses: actions/checkout@v6
- name: Verify formatting
run: |
files="$(gofmt -l .)"
if [ -n "$files" ]; then
echo "These files are not gofmt-formatted:"
echo "$files"
exit 1
fi
- name: Run go test
run: go test ./...
- name: Run go vet
run: go vet ./...
- name: Run golangci-lint
run: golangci-lint run
+2
View File
@@ -4,3 +4,5 @@
test/
.codex/
.codex
.agents/
.claude/
-1
View File
@@ -5,7 +5,6 @@ linters:
disable-all: true
enable:
- errcheck
- govet
- ineffassign
- staticcheck
- unused
+5 -2
View File
@@ -1,7 +1,7 @@
# AGENTS.md
## Purpose
This repository uses Codex for full-project Go code review, not diff-only review.
This repository uses AI coding agents for full-project Go code review, not diff-only review.
When asked to review code, inspect the entire repository and use repository-wide context. Do not limit analysis to the latest commit, pull request diff, or recently changed files.
@@ -96,6 +96,9 @@ Prefer the repositorys documented commands. If multiple choices exist, use th
- Changes made only in `AGENTS.md` must not be added to `CHANGELOG.md`.
- Add changes only to the section for the next version after the latest published git tag.
- The agent must check the latest published tag, `CHANGELOG.md`, and `utils/version.go` before editing the changelog.
- Before editing `CHANGELOG.md`, the agent must inspect the full diff between the latest published tag and the current worktree, for example `git diff --name-status <latest-tag> -- .` and targeted `git diff <latest-tag> -- <files>`.
- Changelog entries must be based on all user-visible changes present between the latest published tag and the current files, including earlier uncommitted or pre-existing worktree changes, not only changes made in the current turn.
- The agent must not add changelog entries for changes that are not present in the diff from the latest published tag, and must remove or rewrite stale entries that no longer match that diff.
- The agent must verify that the target changelog version matches the version declared in `utils/version.go`.
- If the latest published tag is, for example, `v1.0.0`, and `CHANGELOG.md` does not yet contain the next version section, the agent must stop and ask the user which version the change belongs to:
1. `v1.0.1`
@@ -103,7 +106,7 @@ Prefer the repositorys documented commands. If multiple choices exist, use th
3. `v2.0.0`
- The agent must not guess the next version when that section is missing.
- If the user-selected version does not match `utils/version.go`, the agent must warn about the mismatch and require the version file to be updated before proceeding.
- Changelog entries must describe all user-visible behavior changes made in the turn, including API additions, fixes, behavior changes, and breaking changes.
- Changelog entries must describe all user-visible behavior changes in the diff from the latest published tag, including API additions, fixes, behavior changes, and breaking changes.
- When a framework backlog item recorded in `TODO.md` is completed, the agent must also update the backlog status using the existing format:
1. move the completed item into the top of the `Done` section;
2. replace the numbered backlog label with a version tag, for example `1. Scene Model` becomes `[v2.0.0] Scene Model`;
+93
View File
@@ -1,5 +1,98 @@
# Changelog
## v1.0.2
### Fixed
- Fixed long-polling stopping permanently when the HTTP client's internal timeout fired. The polling loop was checking `errors.Is(err, context.DeadlineExceeded)`, which matched HTTP client timeout errors (`*url.Error` wraps `context.DeadlineExceeded`), causing the goroutine to exit as if the bot context was canceled. The check is now `ctx.Err() != nil` so only a real context cancellation stops polling.
- Fixed the HTTP client timeout (45 s) being too close to the long-poll `getUpdates` timeout (30 s default), leaving insufficient margin for connection setup and response transfer. The client timeout is now derived from the configured `PollTimeout` plus a 60-second buffer.
## v1.0.1
### Fixed
- Fixed webhook always accepting unauthenticated requests when `SecretToken` is not configured. A cryptographically random 32-byte token is now generated automatically when `SecretToken` is empty, so the webhook endpoint is always authenticated. The generated token is logged as a warning so the operator can record it.
- Fixed `tgapi.NewAPI` and `tgapi.NewUploader` not installing token redaction on their managed loggers. The bot token is now masked as `<TOKEN>` in debug output even when the `tgapi` package is used standalone without the `laniakea.Bot` wrapper.
## v1.0.0
### Breaking Changes
- Renamed `MsgContext` to `MessageContext` across the public API, including handler signatures (`CommandExecutor`, `MiddlewareExecutor`, scene handler types), all reply/edit/scene helpers, embedded fields on `SceneContext`, and documentation.
- Removed the `NewPayload(...)` constructor. `NewCommand(...)` builds the underlying `Command[T]` for both `/-`commands and callback payloads; registration via `Plugin.AddPayload`/`Plugin.Payload` decides routing.
- `MessageContext.Error(...)` no longer sends unclassified errors to the user. Only errors marked with `AsUserError(...)` are surfaced through the centralized reply path; everything else stays internal-only and is logged.
- `Plugin.Close()` no longer closes a logger supplied through `Plugin.SetLogger(...)`. Only loggers created by the bot during `AddPlugins` registration are owned and closed; caller-supplied loggers remain the caller's responsibility.
- 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 `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.
- Added `MessageContext.UpsertKeyboard(...)` and `MessageContext.UpsertKeyboardMarkdown(...)` helpers that edit callback messages, replace photo callback messages with a fresh chat message, and send a new chat message outside callback flow.
- Added `CommandGroup`, `NewCommandGroup(...)`, `Plugin.CommandGroup(...)`, and `Plugin.AddCommandGroup(...)` helpers for registering prefixed command groups with shared middleware.
- Added the `tgfmt` package with typed MarkdownV2, HTML, legacy Markdown formatting helpers, and a message entity builder.
- Added `InlineKeyboardButtonBuilder.SetPayloadType(...)`, `InlineKeyboardButtonBuilder.SetCallbackData(...)`, and `MessageContext.NewInlineKeyboardButton(...)` helpers for payload-aware button building.
- Added compact callback payload encoding through `BotPayloadCompact`, `BotPayloadCompactBase64`, compact inline keyboard builders, and matching `CallbackData` helpers.
- Added `BotOpts.PollTimeout`, `BotOpts.SetPollTimeout(...)`, and the `POLL_TIMEOUT` environment variable to configure the long-polling `getUpdates` timeout (default 30 seconds).
- 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`.
- Compact callback payload encoding now escapes `,`, `|`, and `\` in command and arg bytes so payloads containing those bytes round-trip without ambiguity. Note: the format coalesces "no args" with "single empty arg" — both encode as `cmd|` and decode to nil args.
- `CallbackData.ToJSON()`, `ToBase64()`, `ToCompact()`, and `ToCompactBase64()` now all return an empty string on serialization failure; the previous `ToJSON()` fallback `{"cmd":""}` has been removed so encoder bugs surface visibly instead of routing to no handler.
- Bot-level middleware blocks now emit a final `UpdateHandledEvent` with `Handled=false`, keeping observer update lifecycles balanced.
- Plugin registration now warns when `AddCommand`, `AddPayload`, or `AddScene` overwrites an existing entry with the same name instead of silently replacing it.
- `BotOpts`, `tgapi.APIOpts`, logger utilities, README, and wiki pages now document the final stable API names and configuration options consistently.
- CI now checks formatting, tests, vet, and lint on both pushes and pull requests.
### Fixed
- Fixed the update worker pool returning before in-flight handlers completed. `startUpdateWorkers` now calls `pool.StopAndWait()` so the bot waits for already-submitted tasks before runtime exit.
- Fixed `RateLimiter.getChatLimiter` upgrading a held read lock to a write lock, which could deadlock under contention. The lookup now releases the read lock before acquiring the write lock and re-checks the map.
- Fixed `RateLimiter` per-chat limiter and lock maps growing unbounded for the lifetime of long-running bots that serve many distinct chats.
- Fixed `Draft.Push` mutating `Message` before validating the candidate length, leaving the draft in a half-mutated state when the candidate would exceed Telegram's limit. The candidate is now validated first; on failure the draft remains unchanged.
- Fixed background runners running one extra iteration after context cancellation when both `ctx.Done()` and the ticker were ready in the same `select`.
- Fixed `Plugin.Close()` double-closing a logger supplied by the caller through `SetLogger(...)`.
- Fixed compact callback payload corruption for arguments containing `,` or `|` bytes.
- Fixed `LoadOptsFromEnv` calling `os.Getenv("MAX_WORKERS")` twice when parsing the worker count.
- Fixed `sceneRuntime` interface carrying a delegating `buildSceneKey` method that just forwarded to a package-level helper; `MessageContext` scene helpers now call the helper directly.
- 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.
- Added webhook runtime regression coverage for request enqueue through worker execution of a command handler.
- Added regression coverage for inline callback keyboard upserts and callback target detection.
- Added regression coverage for command group prefixing, middleware order, clone behavior, and plugin registration.
- Added formatting coverage for escaping, composition, link destinations, HTML attributes, and legacy Markdown code blocks.
- Added regression coverage for context-aware inline keyboard button payload encoding.
- Added regression coverage for compact and Base64-encoded compact callback payload decoding.
- Added regression coverage for long-polling `retry_after` handling on Telegram 429 responses.
- Added regression coverage for compact callback payload round-tripping through `,`, `|`, and `\` separator bytes and a missing-separator decode error.
- 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
### Breaking Changes
+61 -28
View File
@@ -23,7 +23,7 @@ A lightweight, easy-to-use, and performant Telegram Bot API wrapper for Go. It s
* **Built-in Rate Limiting:** Protect your bot from hitting Telegram API limits (supports `retry_after` handling).
* **Context-Aware:** Pass custom application data or state contexts to your handlers.
* **Configurable API:** Mix `Set...` and `Add...` helpers to configure bots clearly (for example, `bot.SetErrorTemplate(...).AddPlugins(...)`).
* **Polling and Webhook Runtime:** Run bots through long polling with `Run()` / `RunWithContext(...)` or through a bot-owned webhook server with `RunWebHookWithContext(...)`.
* **Polling and Webhook Runtime:** Run bots through long polling with `Run()` / `RunWithContext(...)` or through a bot-owned webhook server with `RunWebhookWithContext(...)`.
---
@@ -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
@@ -80,15 +80,15 @@ func main() {
p := laniakea.NewPlugin[laniakea.NoData]("ping")
// 4. Add a command to the plugin.
// p.NewCommand(echo, "echo") creates a command that triggers the 'echo' function on the "/echo" command.
p.AddCommand(p.NewCommand(echo, "echo"))
// p.Command("echo", echo) creates a command that triggers the 'echo' function on the "/echo" command.
p.Command("echo", echo)
// 5. Add another command using an anonymous function (closure).
// This command simply replies "Pong" when the user sends "/ping".
p.AddCommand(p.NewCommand(func(ctx *laniakea.MsgContext, data laniakea.NoData) error {
p.Command("ping", func(ctx *laniakea.MessageContext, data laniakea.NoData) error {
ctx.Answer("Pong")
return nil
}, "ping"))
})
// 6. Configure the bot with a custom error template and add the plugin.
// SetErrorTemplate sets a format string for errors (where %s will be replaced by the actual error).
@@ -112,13 +112,13 @@ 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. `AddCommand`: Registers a command. The first argument is the handler function (`func(*MsgContext, T) error`), the second is the command name (without the slash).
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.
9. `RunWebHookWithContext(...)`: Starts the bot-owned webhook runtime when Telegram should deliver updates over HTTP instead of long polling.
10. A `Bot` instance is single-use. After `Run()`, `RunWithContext()`, or `RunWebHookWithContext()` returns, create a new bot instance for the next session.
9. `RunWebhookWithContext(...)`: Starts the bot-owned webhook runtime when Telegram should deliver updates over HTTP instead of long polling.
10. A `Bot` instance is single-use. After `Run()`, `RunWithContext()`, or `RunWebhookWithContext()` returns, create a new bot instance for the next session.
## File-Based Config
@@ -151,7 +151,7 @@ See the full guide in the wiki: [Bot Options and Configuration](https://git.scur
## Webhook Runtime
Laniakea also supports a bot-owned webhook runtime through `RunWebHookWithContext(...)` and `RunWebHook(...)`.
Laniakea also supports a bot-owned webhook runtime through `RunWebhookWithContext(...)` and `RunWebhook(...)`.
Use it when:
- Telegram should push updates to your HTTP endpoint instead of your bot polling for them.
@@ -159,11 +159,11 @@ Use it when:
- You want Laniakea to register the webhook and own the local HTTP server.
Production notes:
- Set `BotWebHookOpts.SecretToken` for request authentication.
- `BotWebHookOpts.SecretToken` is required when `BotWebHookOpts.UseStatusPath` is enabled.
- Keep `BotWebHookOpts.Path` specific instead of serving webhook traffic on `/`.
- If you switch an existing deployment from webhook mode to long polling, delete the webhook first with `CloseWebHook()` or `tgapi.DeleteWebhook(...)`. Telegram keeps webhook delivery active until it is removed.
- Use `RunWebHookWithContext(...)` with a cancelable context, then call `Close()` after runtime shutdown.
- Set `BotWebhookOpts.SecretToken` for request authentication.
- `BotWebhookOpts.SecretToken` is required when `BotWebhookOpts.UseStatusPath` is enabled.
- Keep `BotWebhookOpts.Path` specific instead of serving webhook traffic on `/`.
- If you switch an existing deployment from webhook mode to long polling, delete the webhook first with `CloseWebhook()` or `tgapi.DeleteWebhook(...)`. Telegram keeps webhook delivery active until it is removed.
- Use `RunWebhookWithContext(...)` with a cancelable context, then call `Close()` after runtime shutdown.
See the full guide in the wiki: [Webhook Runtime](https://git.scuroneko.dev/ScuroNeko/Laniakea/wiki/Webhook-Runtime)
@@ -173,7 +173,7 @@ See the full guide in the wiki: [Webhook Runtime](https://git.scuroneko.dev/Scur
Plugins are the main way to organize code. A plugin can have multiple commands and middlewares.
```go
plugin := laniakea.NewPlugin[*MyDB]("admin")
plugin.AddCommand(plugin.NewCommand(banUser, "ban"))
plugin.Command("ban", banUser)
bot.AddPlugins(plugin)
```
@@ -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!
@@ -238,7 +238,7 @@ Scenes model multi-step conversations inside a plugin. Each active scene is stor
```go
plugin := laniakea.NewPlugin[MyDB]("signup")
plugin.NewScene("signup").
plugin.Scene("signup").
SetScope(laniakea.SceneScopeUserChat).
SetEntry("ask_name").
OnStep("ask_name", func(ctx *laniakea.SceneContext, db MyDB) (laniakea.SceneResult, error) {
@@ -268,6 +268,39 @@ plugin.NewScene("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.
@@ -288,14 +321,14 @@ Use `AddMiddleware` on a plugin to add one or more shared middleware functions.
plugin := laniakea.NewPlugin[*MyDB]("admin")
plugin.AddMiddleware(laniakea.NewMiddleware("logging", loggingMiddleware))
plugin.AddMiddleware(laniakea.NewMiddleware("admin-only", adminOnlyMiddleware))
plugin.AddCommand(plugin.NewCommand(banUser, "ban"))
plugin.Command("ban", banUser)
```
### Example Middlewares
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,14 +346,14 @@ 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.
- **Rate Limiting**: Pass a configured utils.RateLimiter via BotOpts to handle Telegram's rate limits gracefully.
- **Localization**: `L10n` is safe for concurrent use once attached to the bot.
- **Custom Update Handlers**: Use `plugin.AddUpdateHandler(...)` for Telegram update types that are not part of the command/payload flow.
- **Lifecycle**: `RunWithContext(...)` and `RunWebHookWithContext(...)` do not call `Close()` for you. Shut the bot down explicitly, and create a fresh `Bot` for the next run.
- **Lifecycle**: `RunWithContext(...)` and `RunWebhookWithContext(...)` do not call `Close()` for you. Shut the bot down explicitly, and create a fresh `Bot` for the next run.
## Telegram Update Handling
- Commands and payloads are handled through plugins.
+61 -28
View File
@@ -24,7 +24,7 @@
* **Встроенный ограничитель запросов (Rate Limiter):** Защитите бота от превышения лимитов Telegram API (с обработкой `retry_after`).
* **Контекст данных:** Передавайте общие данные приложения или state в обработчики.
* **Настраиваемый API:** Комбинируйте `Set...` и `Add...` helper-методы для понятной конфигурации, например `bot.SetErrorTemplate(...).AddPlugins(...)`.
* **Polling и Webhook Runtime:** Запускайте бота через long polling с `Run()` / `RunWithContext(...)` или через webhook server, которым владеет сам бот, с `RunWebHookWithContext(...)`.
* **Polling и Webhook Runtime:** Запускайте бота через long polling с `Run()` / `RunWithContext(...)` или через webhook server, которым владеет сам бот, с `RunWebhookWithContext(...)`.
---
@@ -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) // Ввод пользователя БЕЗ команды
@@ -81,15 +81,15 @@ func main() {
p := laniakea.NewPlugin[laniakea.NoData]("ping")
// 4. Добавляем команду в плагин.
// p.NewCommand(echo, "echo") создаёт команду, которая вызывает функцию 'echo' по команде "/echo".
p.AddCommand(p.NewCommand(echo, "echo"))
// p.Command("echo", echo) создаёт команду, которая вызывает функцию 'echo' по команде "/echo".
p.Command("echo", echo)
// 5. Добавляем ещё одну команду, используя анонимную функцию (замыкание).
// Эта команда просто отвечает "Pong", когда пользователь отправляет "/ping".
p.AddCommand(p.NewCommand(func(ctx *laniakea.MsgContext, data laniakea.NoData) error {
p.Command("ping", func(ctx *laniakea.MessageContext, data laniakea.NoData) error {
ctx.Answer("Pong")
return nil
}, "ping"))
})
// 6. Настраиваем бота: задаём шаблон ошибки и добавляем плагин.
// SetErrorTemplate устанавливает формат для сообщений об ошибках (где %s будет заменён на текст ошибки).
@@ -113,13 +113,13 @@ func main() {
1. `BotOpts`: Содержит конфигурацию, например, токен API.
2. `NewBot[T]`: Создаёт экземпляр бота. Параметр типа T позволяет передать общие данные приложения (например, *sql.DB или контейнер сервисов), которые будут доступны во всех обработчиках. Используйте laniakea.NoData, если они не нужны.
3. `NewPlugin`: Создаёт логическую группу для команд и Middleware.
4. `AddCommand`: Регистрирует команду. Первый аргумент — функция-обработчик (`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 завершился неуспешно.
9. `RunWebHookWithContext(...)`: Запускает bot-owned webhook runtime, когда Telegram должен доставлять update по HTTP вместо long polling.
10. Экземпляр `Bot` одноразовый. После завершения `Run()`, `RunWithContext()` или `RunWebHookWithContext()` для следующего запуска создавайте новый бот.
9. `RunWebhookWithContext(...)`: Запускает bot-owned webhook runtime, когда Telegram должен доставлять update по HTTP вместо long polling.
10. Экземпляр `Bot` одноразовый. После завершения `Run()`, `RunWithContext()` или `RunWebhookWithContext()` для следующего запуска создавайте новый бот.
## Конфиг из файла
@@ -152,7 +152,7 @@ if err != nil {
## Webhook Runtime
Laniakea также поддерживает bot-owned webhook runtime через `RunWebHookWithContext(...)` и `RunWebHook(...)`.
Laniakea также поддерживает bot-owned webhook runtime через `RunWebhookWithContext(...)` и `RunWebhook(...)`.
Используй его, когда:
- Telegram должен сам отправлять update на твой HTTP endpoint вместо polling.
@@ -160,11 +160,11 @@ Laniakea также поддерживает bot-owned webhook runtime чере
- Ты хочешь, чтобы Laniakea сама регистрировала webhook и владела локальным HTTP server.
Практические замечания:
- Задавай `BotWebHookOpts.SecretToken` для аутентификации запросов.
- Непустой `BotWebHookOpts.SecretToken` обязателен, если включён `BotWebHookOpts.UseStatusPath`.
- Используй явный `BotWebHookOpts.Path`, а не `/`.
- Если ты переводишь уже существующий deployment с webhook-режима на long polling, сначала удали webhook через `CloseWebHook()` или `tgapi.DeleteWebhook(...)`. Пока webhook не удалён, Telegram продолжает доставку через него.
- Запускай `RunWebHookWithContext(...)` с cancelable context и после остановки runtime всё равно вызывай `Close()`.
- Задавай `BotWebhookOpts.SecretToken` для аутентификации запросов.
- Непустой `BotWebhookOpts.SecretToken` обязателен, если включён `BotWebhookOpts.UseStatusPath`.
- Используй явный `BotWebhookOpts.Path`, а не `/`.
- Если ты переводишь уже существующий deployment с webhook-режима на long polling, сначала удали webhook через `CloseWebhook()` или `tgapi.DeleteWebhook(...)`. Пока webhook не удалён, Telegram продолжает доставку через него.
- Запускай `RunWebhookWithContext(...)` с cancelable context и после остановки runtime всё равно вызывай `Close()`.
Полное руководство есть в wiki: [Webhook Runtime](https://git.scuroneko.dev/ScuroNeko/Laniakea/wiki/Webhook-Runtime-RU)
@@ -174,7 +174,7 @@ Laniakea также поддерживает bot-owned webhook runtime чере
```go
plugin := laniakea.NewPlugin[*MyDB]("admin")
plugin.AddCommand(plugin.NewCommand(banUser, "ban"))
plugin.Command("ban", banUser)
bot.AddPlugins(plugin)
```
@@ -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` и другие.
- И много других методов и полей!
@@ -226,7 +226,7 @@ bot.SetAppData(db)
```go
plugin := laniakea.NewPlugin[MyDB]("signup")
plugin.NewScene("signup").
plugin.Scene("signup").
SetScope(laniakea.SceneScopeUserChat).
SetEntry("ask_name").
OnStep("ask_name", func(ctx *laniakea.SceneContext, db MyDB) (laniakea.SceneResult, error) {
@@ -256,6 +256,39 @@ plugin.NewScene("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 (или сама команда).
@@ -285,14 +318,14 @@ func(ctx *MsgContext, db T) bool
plugin := laniakea.NewPlugin[*MyDB]("admin")
plugin.AddMiddleware(laniakea.NewMiddleware("logging", loggingMiddleware))
plugin.AddMiddleware(laniakea.NewMiddleware("admin-only", adminOnlyMiddleware))
plugin.AddCommand(plugin.NewCommand(banUser, "ban"))
plugin.Command("ban", banUser)
```
### Примеры middleware
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,14 +343,14 @@ func adminOnlyMiddleware(ctx *laniakea.MsgContext, db *MyDB) bool {
```
### Важные замечания
- Middleware может изменять MsgContext (например, добавлять пользовательские поля) перед запуском команды.
- Middleware может изменять MessageContext (например, добавлять пользовательские поля) перед запуском команды.
## ⚙️ Расширенная настройка
- **Инлайн-клавиатуры**: Создавайте клавиатуры с помощью `laniakea.NewInlineKeyboardJSON`, `laniakea.NewInlineKeyboardBase64` или `laniakea.NewInlineKeyboard`. `Bot.SetPayloadType(...)` задаёт payload format по умолчанию, а `InlineKeyboard.SetPayloadType(...)` переопределяет его для конкретной клавиатуры.
- **Ограничение запросов**: Передайте настроенный `utils.RateLimiter` через `BotOpts` для корректной обработки лимитов Telegram.
- **Локализация**: `L10n` безопасен для конкурентного использования после подключения к боту.
- **Пользовательские update handlers**: Используйте `plugin.AddUpdateHandler(...)` для Telegram update types вне command/payload flow.
- **Жизненный цикл**: `RunWithContext(...)` и `RunWebHookWithContext(...)` не вызывают `Close()` автоматически. Завершайте бот явно и создавайте новый `Bot` для следующего запуска.
- **Жизненный цикл**: `RunWithContext(...)` и `RunWebhookWithContext(...)` не вызывают `Close()` автоматически. Завершайте бот явно и создавайте новый `Bot` для следующего запуска.
## Обработка Telegram Updates
- Команды и payload-ы обрабатываются через плагины.
+2
View File
@@ -1,5 +1,7 @@
# TODO
---
The framework backlog has moved to the wiki.
Primary page:
+75 -33
View File
@@ -4,6 +4,7 @@ import (
"context"
"errors"
"fmt"
"net/http"
"sync"
"time"
@@ -36,7 +37,7 @@ type AppData any
// data.
//
// Use Bot[NoData] to indicate no shared dependency injection is required.
type NoData struct{ AppData }
type NoData struct{}
// AppDataLogger builds a sneklog.LoggerWriter from injected application data.
//
@@ -47,11 +48,15 @@ type AppDataLogger[T AppData] func(data T) sneklog.LoggerWriter
// BotPayloadType defines the serialization format for callback data payloads.
type BotPayloadType string
var (
const (
// BotPayloadBase64 encodes callback data as a Base64 string.
BotPayloadBase64 BotPayloadType = "base64"
// BotPayloadJSON encodes callback data as a JSON string.
BotPayloadJSON BotPayloadType = "json"
// BotPayloadCompact encodes callback data as a compact delimited string.
BotPayloadCompact BotPayloadType = "compact"
// BotPayloadCompactBase64 encodes compact callback data as a Base64 string.
BotPayloadCompactBase64 BotPayloadType = "compact-base64"
)
var (
@@ -59,7 +64,7 @@ var (
ErrNoPrefixes = errors.New("no prefixes defined")
// ErrNoPlugins reports that the bot was started without any registered plugins.
ErrNoPlugins = errors.New("no plugins defined")
// ErrBotAlreadyRun reports that Run, RunWithContext, or RunWebHookWithContext was called more than once.
// ErrBotAlreadyRun reports that Run, RunWithContext, or RunWebhookWithContext was called more than once.
ErrBotAlreadyRun = errors.New("bot can only be run once")
// ErrTokenRequired reports that BotOpts.Token was empty.
@@ -78,24 +83,26 @@ var (
// - Localization and draft message support
//
// Runtime accessors are safe for concurrent use. Configure the bot before Run,
// RunWithContext, or RunWebHookWithContext.
// A Bot is single-use: after Run, RunWithContext, or RunWebHookWithContext returns,
// RunWithContext, or RunWebhookWithContext.
// A Bot is single-use: after Run, RunWithContext, or RunWebhookWithContext returns,
// create a new Bot for the next session.
type Bot[T AppData] struct {
token string
debug bool
errorTemplate string
userID int64
username string
payloadType BotPayloadType
strictPayloadType bool
maxWorkers int
pollTimeout int // Long-polling timeout in seconds for getUpdates
logFormat utils.LogFormat
logFormatter *sneklog.Formatter
logger *sneklog.Logger // Main bot logger (JSON stdout + optional file)
requestLogger *sneklog.Logger // Optional request-level API logging
useReqLogger bool
webHookLogger *sneklog.Logger // Webhook logger. Available only after Bot.RunWebHookWithContext.
webhookLogger *sneklog.Logger // Webhook logger. Available only after Bot.RunWebhookWithContext.
extraLoggers extypes.Slice[*sneklog.Logger] // API, Uploader, and custom loggers
plugins []Plugin[T] // Command/event handlers
@@ -160,13 +167,27 @@ func NewBot[T any](opts *BotOpts) (*Bot[T], error) {
limiter := utils.NewRateLimiter()
limiter.SetGlobalRate(opts.RateLimit)
workers := 32
if opts.MaxWorkers > 0 {
workers = opts.MaxWorkers
}
pollTimeout := 30
if opts.PollTimeout > 0 {
pollTimeout = opts.PollTimeout
}
// HTTP client timeout must exceed pollTimeout to avoid spurious deadline
// errors that the polling loop would misinterpret as context cancellation.
httpTimeout := time.Duration(pollTimeout)*time.Second + 60*time.Second
apiOpts := tgapi.NewAPIOpts(opts.Token).
SetAPIURL(opts.APIURL).
UseTestServer(opts.UseTestServer).
SetLimiter(limiter).
SetLimiterDrop(opts.DropRLOverflow).
SetDropRateLimitOverflow(opts.DropRateLimitOverflow).
SetLogFormat(opts.LogFormat).
SetLogFormatter(opts.LogFormatter)
SetLogFormatter(opts.LogFormatter).
SetHTTPClient(&http.Client{Timeout: httpTimeout})
api := tgapi.NewAPI(apiOpts)
uploader := tgapi.NewUploader(api)
@@ -175,17 +196,13 @@ func NewBot[T any](opts *BotOpts) (*Bot[T], error) {
prefixes = []string{"/"}
}
workers := 32
if opts.MaxWorkers > 0 {
workers = opts.MaxWorkers
}
bot := &Bot[T]{
updateOffset: 0,
errorTemplate: "%s",
payloadType: BotPayloadBase64,
strictPayloadType: opts.StrictPayloadType,
maxWorkers: workers,
pollTimeout: pollTimeout,
updateQueue: updateQueue,
api: api,
uploader: uploader,
@@ -235,6 +252,7 @@ func NewBot[T any](opts *BotOpts) (*Bot[T], error) {
return nil, err
}
bot.username = Val(u.Username, "")
bot.userID = u.ID
if bot.username == "" {
bot.logger.Warn("Can't get bot username. Named command handlers won't work!")
}
@@ -256,12 +274,18 @@ func (bot *Bot[T]) SetRequestLogger(l *sneklog.Logger) *Bot[T] {
return bot
}
// SetWebHookLogger replaces the webhook logger.
func (bot *Bot[T]) SetWebHookLogger(l *sneklog.Logger) *Bot[T] {
bot.webHookLogger = l
// SetWebhookLogger replaces the webhook logger.
func (bot *Bot[T]) SetWebhookLogger(l *sneklog.Logger) *Bot[T] {
bot.webhookLogger = l
return bot
}
// GetAPI returns the underlying Telegram Bot API client.
func (bot *Bot[T]) GetAPI() *tgapi.API { return bot.api }
// GetUploader returns the underlying file uploader client.
func (bot *Bot[T]) GetUploader() *tgapi.Uploader { return bot.uploader }
// Close gracefully shuts down bot-owned resources.
//
// Close shuts down, in order:
@@ -272,7 +296,7 @@ func (bot *Bot[T]) SetWebHookLogger(l *sneklog.Logger) *Bot[T] {
// - RequestLogger (if enabled)
// - Main logger
//
// RunWithContext and RunWebHookWithContext do not call Close automatically.
// RunWithContext and RunWebhookWithContext do not call Close automatically.
// The caller is responsible for invoking Close after runtime returns to release
// these resources.
//
@@ -294,11 +318,11 @@ func (bot *Bot[T]) Close() error {
e = append(e, err)
}
}
if bot.webHookLogger != nil {
if err := bot.webHookLogger.Close(); err != nil {
if bot.webhookLogger != nil {
if err := bot.webhookLogger.Close(); err != nil {
logCloseErr(err)
}
bot.webHookLogger = nil
bot.webhookLogger = nil
}
if bot.uploader != nil {
if err := bot.uploader.Close(); err != nil {
@@ -354,8 +378,8 @@ func (bot *Bot[T]) GetLogger() *sneklog.Logger { return bot.logger }
// GetRequestLogger returns the request-level logger, if configured.
func (bot *Bot[T]) GetRequestLogger() *sneklog.Logger { return bot.requestLogger }
// GetWebHookLogger returns the webhook logger, if configured.
func (bot *Bot[T]) GetWebHookLogger() *sneklog.Logger { return bot.webHookLogger }
// GetWebhookLogger returns the webhook logger, if configured.
func (bot *Bot[T]) GetWebhookLogger() *sneklog.Logger { return bot.webhookLogger }
// GetLoggerLevel returns the effective log level derived from the bot's debug
// flag.
@@ -368,7 +392,7 @@ func (bot *Bot[T]) GetLoggerLevel() sneklog.LogLevel {
}
// L10n translates a key in the given language.
// Returns empty string if translation not found.
// Returns key if translation not found.
func (bot *Bot[T]) L10n(lang, key string) string {
return bot.l10n.Translate(lang, key)
}
@@ -387,7 +411,7 @@ func (bot *Bot[T]) L10n(lang, key string) string {
// - Waits for registered runners to exit
//
// If you are switching an existing deployment from webhook delivery to polling,
// delete the current webhook first with CloseWebHook or tgapi.DeleteWebhook.
// delete the current webhook first with CloseWebhook or tgapi.DeleteWebhook.
// Telegram keeps webhook delivery active until the webhook is removed.
//
// RunWithContext does not close API, uploader, or logger resources on return.
@@ -414,13 +438,13 @@ func (bot *Bot[T]) RunWithContext(ctx context.Context) error {
}
bot.requestLogger = nil
}
if bot.webHookLogger != nil {
bot.logger.Warnln("Bot#webHookLogger present. You shouldn't set this, if ran in Long Polling mode!")
err := bot.webHookLogger.Close()
if bot.webhookLogger != nil {
bot.logger.Warnln("Bot#webhookLogger present. You shouldn't set this, if ran in Long Polling mode!")
err := bot.webhookLogger.Close()
if err != nil {
bot.logger.Errorln(err)
}
bot.webHookLogger = nil
bot.webhookLogger = nil
}
bot.ExecRunners(ctx)
@@ -430,10 +454,21 @@ func (bot *Bot[T]) RunWithContext(ctx context.Context) error {
defer func() {
if r := recover(); r != nil {
bot.logger.Errorln(fmt.Sprintf("panic in update polling: %v", r))
err, ok := r.(error)
if !ok {
err = fmt.Errorf("%v", r)
}
bot.safeEmitEvent(ctx, ErrorEvent{
Plugin: "bot",
HandlerKind: HandlerPollingKind,
HandlerName: "getUpdates",
Err: err,
UserFacing: false,
})
}
close(bot.updateQueue)
}()
retryDelay := time.Duration(0)
backoffDelay := time.Duration(0)
retryCount := 0
for {
select {
@@ -442,11 +477,18 @@ func (bot *Bot[T]) RunWithContext(ctx context.Context) error {
default:
updates, err := bot.Updates(ctx)
if err != nil {
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
if ctx.Err() != nil {
return
}
bot.logger.Errorln("failed to fetch updates:", err)
retryDelay = nextPollRetryDelay(retryDelay)
retryDelay, ok := pollRetryAfterDelay(err)
if ok {
bot.logger.Warnln("getUpdates rate limited; retrying after", retryDelay)
backoffDelay = 0
} else {
bot.logger.Errorln("failed to fetch updates:", err)
backoffDelay = nextPollRetryDelay(backoffDelay)
retryDelay = backoffDelay
}
retryCount++
bot.safeEmitEvent(ctx, PollingRetryEvent{
Attempt: retryCount,
@@ -471,7 +513,7 @@ func (bot *Bot[T]) RunWithContext(ctx context.Context) error {
}
continue
}
retryDelay = 0
backoffDelay = 0
retryCount = 0
for _, update := range updates {
+7 -4
View File
@@ -60,7 +60,7 @@ func (bot *Bot[T]) SetSessionStore(store SessionStore) *Bot[T] {
return bot
}
if store == nil {
bot.logger.Warn("SetSessionStore called with nil store; using default MemorySessionStore")
bot.logger.Warn("SetSessionStore called with nil store; nothing changed")
return bot
}
bot.sessionStore = store
@@ -125,7 +125,7 @@ func (bot *Bot[T]) GetAppData() T { return bot.appData }
// SetUpdateTypes sets the list of update types the bot will request from Telegram.
// Overwrites any previously set types.
func (bot *Bot[T]) SetUpdateTypes(t ...tgapi.UpdateType) *Bot[T] {
if !bot.configMutable("UpdateTypes") {
if !bot.configMutable("SetUpdateTypes") {
return bot
}
bot.updateTypes = make([]tgapi.UpdateType, 0)
@@ -177,7 +177,7 @@ func (bot *Bot[T]) SetStrictPayloadType(strict bool) *Bot[T] {
// Use "%s" to insert the error message.
// Example: "❌ Error: %s" → "❌ Error: Command not found".
func (bot *Bot[T]) SetErrorTemplate(s string) *Bot[T] {
if !bot.configMutable("ErrorTemplate") {
if !bot.configMutable("SetErrorTemplate") {
return bot
}
bot.errorTemplate = s
@@ -186,6 +186,9 @@ func (bot *Bot[T]) SetErrorTemplate(s string) *Bot[T] {
// SetDebug enables or disables debug logging.
func (bot *Bot[T]) SetDebug(debug bool) *Bot[T] {
if !bot.configMutable("SetDebug") {
return bot
}
bot.debug = debug
level := sneklog.FATAL
if debug {
@@ -216,7 +219,7 @@ func (bot *Bot[T]) SetL10n(l *L10n) *Bot[T] {
return bot
}
if l == nil {
bot.logger.Warn("SetL10n called with nil L10n; localization will be disabled")
bot.logger.Warn("SetL10n called with nil L10n; localization will not change")
return bot
}
bot.l10n = l
+37 -11
View File
@@ -54,9 +54,9 @@ type BotOpts struct {
// Telegram allows up to 30 req/s for most bots. Defaults to 30.
RateLimit int
// DropRLOverflow drops incoming updates when rate limit is exceeded instead of queuing.
// DropRateLimitOverflow drops incoming updates when rate limit is exceeded instead of queuing.
// Use this to prioritize responsiveness over reliability.
DropRLOverflow bool
DropRateLimitOverflow bool
// StrictPayloadType disables callback payload fallback decoding.
// When enabled, the bot accepts only the configured default payload type.
@@ -65,13 +65,20 @@ type BotOpts struct {
// MaxWorkers is the maximum number of update handlers that may run concurrently.
MaxWorkers int
// PollTimeout is the long-polling timeout in seconds for getUpdates.
// Defaults to 30. Telegram allows 0..50; values outside that range are accepted
// by the bot but rejected by Telegram at runtime.
PollTimeout int
// FileConfigVersion stores the version declared by the config file used to
// load these options.
//
// It is zero when the options were not loaded from a versioned file.
FileConfigVersion int
LogFormat utils.LogFormat
// LogFormat selects text or JSON output for bot-managed loggers.
LogFormat utils.LogFormat
// LogFormatter customizes bot-managed logger writers when supported.
LogFormatter *sneklog.Formatter
}
@@ -92,13 +99,15 @@ type BotOpts struct {
// - DROP_RL_OVERFLOW: "true" to drop updates on rate limit overflow
// - STRICT_PAYLOAD_TYPE: "true" to reject callback payloads encoded in a different format
// - MAX_WORKERS: maximum number of concurrent update handlers (default: 32)
// - JSON_LOG:
// - POLL_TIMEOUT: long-polling timeout in seconds for getUpdates (default: 30)
// - LOG_FORMAT: logger output format, "text" or "json" (default: "text")
//
// Returns a populated BotOpts.
// NewBot validates required fields and returns ErrTokenRequired when TG_TOKEN is missing.
func LoadOptsFromEnv() *BotOpts {
rateLimit := 30
maxWorkers := 32
pollTimeout := 30
stringUpdateTypes := splitEnvList(os.Getenv("UPDATE_TYPES"))
updateTypes := make([]tgapi.UpdateType, 0, len(stringUpdateTypes))
@@ -113,11 +122,17 @@ func LoadOptsFromEnv() *BotOpts {
}
if mw := os.Getenv("MAX_WORKERS"); mw != "" {
if n, err := strconv.Atoi(os.Getenv("MAX_WORKERS")); err == nil {
if n, err := strconv.Atoi(mw); err == nil {
maxWorkers = n
}
}
if pt := os.Getenv("POLL_TIMEOUT"); pt != "" {
if n, err := strconv.Atoi(pt); err == nil {
pollTimeout = n
}
}
return &BotOpts{
Token: os.Getenv("TG_TOKEN"),
UpdateTypes: updateTypes,
@@ -133,11 +148,12 @@ func LoadOptsFromEnv() *BotOpts {
UseTestServer: os.Getenv("USE_TEST_SERVER") == "true",
APIURL: os.Getenv("API_URL"),
RateLimit: rateLimit,
DropRLOverflow: os.Getenv("DROP_RL_OVERFLOW") == "true",
StrictPayloadType: os.Getenv("STRICT_PAYLOAD_TYPE") == "true",
RateLimit: rateLimit,
DropRateLimitOverflow: os.Getenv("DROP_RL_OVERFLOW") == "true",
StrictPayloadType: os.Getenv("STRICT_PAYLOAD_TYPE") == "true",
MaxWorkers: maxWorkers,
PollTimeout: pollTimeout,
FileConfigVersion: 0,
LogFormat: utils.LogFormat(os.Getenv("LOG_FORMAT")),
}
@@ -221,10 +237,10 @@ func (opts *BotOpts) SetRateLimit(limit int) *BotOpts {
return opts
}
// SetDropRLOverflow drops incoming updates when rate limit is exceeded instead of queuing.
// SetDropRateLimitOverflow drops incoming updates when rate limit is exceeded instead of queuing.
// Use this to prioritize responsiveness over reliability. Default is false.
func (opts *BotOpts) SetDropRLOverflow(drop bool) *BotOpts {
opts.DropRLOverflow = drop
func (opts *BotOpts) SetDropRateLimitOverflow(drop bool) *BotOpts {
opts.DropRateLimitOverflow = drop
return opts
}
@@ -254,10 +270,20 @@ func (opts *BotOpts) SetMaxWorkers(workers int) *BotOpts {
return opts
}
// SetPollTimeout sets the long-polling timeout in seconds for getUpdates.
// Defaults to 30. Telegram accepts 0..50.
func (opts *BotOpts) SetPollTimeout(seconds int) *BotOpts {
opts.PollTimeout = seconds
return opts
}
// SetLogFormat sets the output format used by bot-managed loggers.
func (opts *BotOpts) SetLogFormat(format utils.LogFormat) *BotOpts {
opts.LogFormat = format
return opts
}
// SetLogFormatter sets the formatter used by bot-managed logger writers.
func (opts *BotOpts) SetLogFormatter(formatter *sneklog.Formatter) *BotOpts {
opts.LogFormatter = formatter
return opts
+12 -6
View File
@@ -28,6 +28,7 @@ type botOptsFileJSONAPI struct {
UseTestServer bool `json:"use_test_server"`
APIURL string `json:"url"`
RateLimit int `json:"rate_limit"`
PollTimeout int `json:"poll_timeout"`
DropRLOverflow bool `json:"drop_overflow"`
}
@@ -70,10 +71,11 @@ func (codec BotOptsFileJSONCodec) FromBytes(data []byte) (*BotOpts, error) {
WriteToFile: fileOpts.Logger.WriteToFile,
LogFormat: fileOpts.Logger.LogFormat,
UseTestServer: fileOpts.API.UseTestServer,
APIURL: fileOpts.API.APIURL,
RateLimit: fileOpts.API.RateLimit,
DropRLOverflow: fileOpts.API.DropRLOverflow,
UseTestServer: fileOpts.API.UseTestServer,
APIURL: fileOpts.API.APIURL,
RateLimit: fileOpts.API.RateLimit,
PollTimeout: fileOpts.API.PollTimeout,
DropRateLimitOverflow: fileOpts.API.DropRLOverflow,
StrictPayloadType: fileOpts.StrictPayloadType,
MaxWorkers: fileOpts.MaxWorkers,
@@ -102,7 +104,8 @@ func (codec BotOptsFileJSONCodec) ToBytes(opts *BotOpts) ([]byte, error) {
UseTestServer: opts.UseTestServer,
APIURL: opts.APIURL,
RateLimit: opts.RateLimit,
DropRLOverflow: opts.DropRLOverflow,
PollTimeout: opts.PollTimeout,
DropRLOverflow: opts.DropRateLimitOverflow,
},
StrictPayloadType: opts.StrictPayloadType,
MaxWorkers: opts.MaxWorkers,
@@ -114,9 +117,12 @@ func (codec BotOptsFileJSONCodec) ToBytes(opts *BotOpts) ([]byte, error) {
return data, nil
}
// Load reads BotOpts from a JSON config file.
func (codec BotOptsFileJSONCodec) Load(filename string) (*BotOpts, error) {
return LoadBotOptsFile(codec, filename)
}
// Save writes BotOpts to a JSON config file.
func (codec BotOptsFileJSONCodec) Save(filename string, opts *BotOpts) error {
return SaveBotOptsFile(codec, filename, opts)
}
@@ -152,7 +158,7 @@ func SaveBotOptsFile(codec BotOptsFileCodec, filename string, opts *BotOpts) err
if err != nil {
return err
}
err = os.WriteFile(filename, data, 0644)
err = os.WriteFile(filename, data, 0600)
if err != nil {
return err
}
+16 -15
View File
@@ -13,21 +13,22 @@ import (
func TestBotOptsFileJSONCodecRoundTrip(t *testing.T) {
codec := BotOptsFileJSONCodec{}
want := &BotOpts{
Token: "TOKEN",
UpdateTypes: []tgapi.UpdateType{tgapi.UpdateTypeMessage, tgapi.UpdateTypeCallbackQuery},
Debug: true,
ErrorTemplate: "Error: %s",
Prefixes: []string{"/", "!"},
LoggerBasePath: "/tmp/logs",
UseRequestLogger: true,
WriteToFile: true,
UseTestServer: true,
APIURL: "https://api.example.invalid",
RateLimit: 42,
DropRLOverflow: true,
StrictPayloadType: true,
MaxWorkers: 64,
FileConfigVersion: ConfigVersion,
Token: "TOKEN",
UpdateTypes: []tgapi.UpdateType{tgapi.UpdateTypeMessage, tgapi.UpdateTypeCallbackQuery},
Debug: true,
ErrorTemplate: "Error: %s",
Prefixes: []string{"/", "!"},
LoggerBasePath: "/tmp/logs",
UseRequestLogger: true,
WriteToFile: true,
UseTestServer: true,
APIURL: "https://api.example.invalid",
RateLimit: 42,
PollTimeout: 7,
DropRateLimitOverflow: true,
StrictPayloadType: true,
MaxWorkers: 64,
FileConfigVersion: ConfigVersion,
}
data, err := codec.ToBytes(want)
+2 -1
View File
@@ -29,6 +29,7 @@ func (bot *Bot[T]) AddPlugins(plugin ...*Plugin[T]) *Bot[T] {
cloned := clonePlugin(p)
if cloned.logger == nil {
cloned.logger = utils.CreateLogger(cloned.name, level, bot.logFormat, bot.logFormatter)
cloned.loggerOwned = true
}
bot.addTokenReplacer(cloned.logger)
bot.plugins = append(bot.plugins, cloned)
@@ -95,7 +96,7 @@ func (bot *Bot[T]) UsePolicy(name string, policy Policy[T]) *Bot[T] {
// - Scheduled tasks (e.g., daily announcements)
//
// Runners start from the bot runtime entry points, immediately after
// RunWithContext or RunWebHookWithContext begins.
// RunWithContext or RunWebhookWithContext begins.
//
// Example:
//
+4 -8
View File
@@ -24,16 +24,16 @@ func (bot *Bot[T]) findScene(name string) (*sceneMeta, bool) {
}
return &sceneMeta{
Name: scene.Name,
Scope: scene.Scope,
Entry: scene.Entry,
Name: scene.name,
Scope: scene.scope,
Entry: scene.entry,
Steps: steps,
}, true
}
return nil, false
}
func (bot *Bot[T]) findSceneSession(ctx *MsgContext) (string, SceneSession, error) {
func (bot *Bot[T]) findSceneSession(ctx *MessageContext) (string, SceneSession, error) {
var zero SceneSession
for _, scope := range bot.sceneScopePriority {
@@ -53,7 +53,3 @@ func (bot *Bot[T]) findSceneSession(ctx *MsgContext) (string, SceneSession, erro
return "", zero, ErrCantFindSession
}
func (bot *Bot[T]) buildSceneKey(scope SceneScope, ctx *MsgContext) (string, bool) {
return buildSceneKey(scope, ctx)
}
+62 -9
View File
@@ -37,8 +37,8 @@ func (o *pollingRetryObserver) OnPollingRetry(ctx context.Context, ev PollingRet
type testObserver struct{}
func (testObserver) OnReceiveUpdate(context.Context, UpdateReceivedEvent) {}
func (testObserver) OnHandledUpdate(context.Context, UpdateHandledEvent) {}
func (testObserver) OnUpdateReceived(context.Context, UpdateReceivedEvent) {}
func (testObserver) OnUpdateHandled(context.Context, UpdateHandledEvent) {}
func (testObserver) OnHandlerStarted(context.Context, HandlerStartedEvent) {}
func (testObserver) OnHandlerFinished(context.Context, HandlerFinishedEvent) {
}
@@ -63,14 +63,14 @@ func TestAddPluginsSnapshotsConfiguration(t *testing.T) {
bot := &Bot[NoData]{logger: sneklog.NewLogger()}
plugin := NewPlugin[NoData]("demo")
cmd := plugin.NewCommand(func(ctx *MsgContext, db NoData) error { return nil }, "start")
plugin.AddMiddleware(NewMiddleware("base", func(ctx *MsgContext, db NoData) bool { return true }))
cmd := plugin.Command("start", func(ctx *MessageContext, db NoData) error { return nil })
plugin.AddMiddleware(NewMiddleware("base", func(ctx *MessageContext, db NoData) bool { return true }))
bot.AddPlugins(plugin)
cmd.SetDescription("mutated after registration")
plugin.NewCommand(func(ctx *MsgContext, db NoData) error { return nil }, "late")
plugin.AddMiddleware(NewMiddleware("late", func(ctx *MsgContext, db NoData) bool { return true }))
plugin.Command("late", func(ctx *MessageContext, db NoData) error { return nil })
plugin.AddMiddleware(NewMiddleware("late", func(ctx *MessageContext, db NoData) bool { return true }))
registered := bot.plugins[0]
if _, exists := registered.commands["late"]; exists {
@@ -446,7 +446,7 @@ func TestCloseDoesNotDeleteWebhook(t *testing.T) {
bot := &Bot[NoData]{
logger: sneklog.NewLogger(),
webHookLogger: sneklog.NewLogger(),
webhookLogger: sneklog.NewLogger(),
api: api,
uploader: uploader,
}
@@ -560,6 +560,59 @@ func TestRunWithContextPreservesPollingRetryBackoff(t *testing.T) {
}
}
func TestRunWithContextUsesTelegramRetryAfterForPollingRateLimit(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
observer := &pollingRetryObserver{cancel: cancel}
client := &http.Client{
Transport: pollingRoundTripFunc(func(r *http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: http.StatusOK,
Header: http.Header{"Content-Type": []string{"application/json"}},
Body: io.NopCloser(strings.NewReader(`{"ok":false,"error_code":429,"description":"Too Many Requests: retry after 5","parameters":{"retry_after":5}}`)),
}, nil
}),
}
api := tgapi.NewAPI(
tgapi.NewAPIOpts("token").
SetAPIURL("http://example.invalid").
SetHTTPClient(client),
)
defer func() {
_ = api.Close()
}()
bot := &Bot[NoData]{
logger: sneklog.NewLogger(),
api: api,
prefixes: []string{"/"},
plugins: []Plugin[NoData]{{name: "demo"}},
updateQueue: make(chan *tgapi.Update, 1),
maxWorkers: 1,
observer: observer,
}
if err := bot.RunWithContext(ctx); err != nil {
t.Fatalf("RunWithContext returned error: %v", err)
}
if len(observer.retries) != 1 {
t.Fatalf("expected one polling retry event, got %d", len(observer.retries))
}
if got := observer.retries[0]; got.Attempt != 1 || got.Delay != 5*time.Second {
t.Fatalf("unexpected polling retry event: %#v", got)
}
var responseErr *tgapi.ResponseError
if !errors.As(observer.retries[0].Err, &responseErr) {
t.Fatalf("expected ResponseError, got %T", observer.retries[0].Err)
}
if responseErr.Code != 429 || responseErr.Parameters == nil || responseErr.Parameters.RetryAfter == nil || *responseErr.Parameters.RetryAfter != 5 {
t.Fatalf("unexpected response error: %#v", responseErr)
}
}
func TestBotConfigurationFreezesAfterRunStarts(t *testing.T) {
type testDB struct{ Name string }
@@ -759,7 +812,7 @@ func TestAddPluginsAndRuntimeRegistrationsNoOpAfterRunStarts(t *testing.T) {
bot := &Bot[NoData]{
logger: sneklog.NewLogger(),
prefixes: []string{"/"},
middlewares: []Middleware[NoData]{NewMiddleware("base", func(ctx *MsgContext, db NoData) bool { return true })},
middlewares: []Middleware[NoData]{NewMiddleware("base", func(ctx *MessageContext, db NoData) bool { return true })},
runners: []Runner[NoData]{NewRunner("base", func(bot *Bot[NoData]) error { return nil })},
}
plugin := NewPlugin[NoData]("late")
@@ -770,7 +823,7 @@ func TestAddPluginsAndRuntimeRegistrationsNoOpAfterRunStarts(t *testing.T) {
defer bot.finishRun()
bot.AddPlugins(plugin)
bot.AddMiddleware(NewMiddleware("late", func(ctx *MsgContext, db NoData) bool { return true }))
bot.AddMiddleware(NewMiddleware("late", func(ctx *MessageContext, db NoData) bool { return true }))
bot.AddRunner(NewRunner("late", func(bot *Bot[NoData]) error { return nil }))
if len(bot.plugins) != 0 {
+18 -32
View File
@@ -2,6 +2,7 @@ package laniakea
import (
"context"
"errors"
"fmt"
"maps"
"reflect"
@@ -67,7 +68,7 @@ func (bot *Bot[T]) startUpdateWorkers(ctx context.Context) {
bot.handle(ctx, u)
})
}
pool.Stop() // Wait for all tasks to complete and stop the pool
pool.StopAndWait() // Wait for all tasks to complete and stop the pool
}
func (bot *Bot[T]) initLoggers(opts *BotOpts) {
@@ -135,6 +136,18 @@ func nextPollRetryDelay(prev time.Duration) time.Duration {
return next
}
func pollRetryAfterDelay(err error) (time.Duration, bool) {
var responseErr *tgapi.ResponseError
if !errors.As(err, &responseErr) || responseErr.Code != 429 || responseErr.Parameters == nil || responseErr.Parameters.RetryAfter == nil {
return 0, false
}
after := *responseErr.Parameters.RetryAfter
if after <= 0 {
return 0, false
}
return time.Duration(after) * time.Second, true
}
func isNilValue[T any](v T) bool {
rv := reflect.ValueOf(v)
if !rv.IsValid() {
@@ -170,49 +183,22 @@ func clonePlugin[T AppData](p *Plugin[T]) Plugin[T] {
middlewares: append(extypes.Slice[Middleware[T]](nil), p.middlewares...),
skipAutoCmd: p.skipAutoCmd,
logger: p.logger,
loggerOwned: false, // user-supplied loggers stay caller-owned; bot may take ownership during registration
messageFallback: p.messageFallback,
handlers: make(map[tgapi.UpdateType]CommandExecutor[T]),
onClose: p.onClose,
}
for name, command := range p.commands {
cloned.commands[name] = cloneCommand(command)
cloned.commands[name] = command.clone()
}
for name, command := range p.payloads {
cloned.payloads[name] = cloneCommand(command)
cloned.payloads[name] = command.clone()
}
for name, scene := range p.scenes {
cloned.scenes[name] = cloneScene(scene)
cloned.scenes[name] = scene.clone()
}
maps.Copy(cloned.handlers, p.handlers)
return cloned
}
func cloneCommand[T AppData](command *Command[T]) *Command[T] {
if command == nil {
return nil
}
cloned := *command
cloned.args = append(extypes.Slice[CommandArg](nil), command.args...)
cloned.middlewares = append(extypes.Slice[Middleware[T]](nil), command.middlewares...)
return &cloned
}
func cloneScene[T AppData](scene *Scene[T]) *Scene[T] {
if scene == nil {
return nil
}
cloned := *scene
cloned.steps = make(map[string]SceneHandler[T], len(scene.steps))
cloned.commands = make(map[string]SceneHandler[T], len(scene.commands))
cloned.payloads = make(map[string]SceneHandler[T], len(scene.payloads))
maps.Copy(cloned.steps, scene.steps)
maps.Copy(cloned.commands, scene.commands)
maps.Copy(cloned.payloads, scene.payloads)
return &cloned
}
+106 -114
View File
@@ -2,6 +2,7 @@ package laniakea
import (
"context"
"crypto/subtle"
"encoding/json"
"errors"
"fmt"
@@ -15,8 +16,8 @@ import (
"git.scuroneko.dev/scuroneko/laniakea/utils"
)
// BotWebHookOpts configures Telegram webhook registration and the local HTTP server.
type BotWebHookOpts struct {
// BotWebhookOpts configures Telegram webhook registration and the local HTTP server.
type BotWebhookOpts struct {
Path string
LocalPort int
UseStatusPath bool
@@ -30,9 +31,9 @@ type BotWebHookOpts struct {
SecretToken string
}
// NewBotWebHookOpts returns webhook options with the default path, local port, and max connections.
func NewBotWebHookOpts() *BotWebHookOpts {
return &BotWebHookOpts{
// NewBotWebhookOpts returns webhook options with the default path, local port, and max connections.
func NewBotWebhookOpts() *BotWebhookOpts {
return &BotWebhookOpts{
Path: "/",
LocalPort: 8080,
MaxConnections: 40,
@@ -40,38 +41,38 @@ func NewBotWebHookOpts() *BotWebHookOpts {
}
// SetPath sets the local HTTP path that receives Telegram webhook requests.
func (opts *BotWebHookOpts) SetPath(path string) *BotWebHookOpts {
func (opts *BotWebhookOpts) SetPath(path string) *BotWebhookOpts {
opts.Path = path
return opts
}
// SetLocalPort sets the local HTTP port used by the webhook server.
func (opts *BotWebHookOpts) SetLocalPort(port int) *BotWebHookOpts {
func (opts *BotWebhookOpts) SetLocalPort(port int) *BotWebhookOpts {
opts.LocalPort = port
return opts
}
// SetUseStatusPath enables or disables the optional /status endpoint.
// A non-empty SecretToken is required when this endpoint is enabled.
func (opts *BotWebHookOpts) SetUseStatusPath(use bool) *BotWebHookOpts {
func (opts *BotWebhookOpts) SetUseStatusPath(use bool) *BotWebhookOpts {
opts.UseStatusPath = use
return opts
}
// SetURL sets the public base URL Telegram should call for incoming updates.
func (opts *BotWebHookOpts) SetURL(url string) *BotWebHookOpts {
func (opts *BotWebhookOpts) SetURL(url string) *BotWebhookOpts {
opts.URL = url
return opts
}
// SetCertificate sets the self-signed webhook certificate bytes to upload.
func (opts *BotWebHookOpts) SetCertificate(certificate []byte) *BotWebHookOpts {
func (opts *BotWebhookOpts) SetCertificate(certificate []byte) *BotWebhookOpts {
opts.Certificate = certificate
return opts
}
// MustLoadCertificate loads a webhook certificate from disk and panics on failure.
func (opts *BotWebHookOpts) MustLoadCertificate(filename string) *BotWebHookOpts {
func (opts *BotWebhookOpts) MustLoadCertificate(filename string) *BotWebhookOpts {
f, err := os.Open(filename)
if err != nil {
panic(err)
@@ -87,37 +88,37 @@ func (opts *BotWebHookOpts) MustLoadCertificate(filename string) *BotWebHookOpts
}
// SetIPAddress sets the fixed IP address Telegram should use for webhook delivery.
func (opts *BotWebHookOpts) SetIPAddress(ip string) *BotWebHookOpts {
func (opts *BotWebhookOpts) SetIPAddress(ip string) *BotWebhookOpts {
opts.IPAddress = ip
return opts
}
// SetMaxConnections sets Telegram's maximum number of simultaneous webhook connections.
func (opts *BotWebHookOpts) SetMaxConnections(max int8) *BotWebHookOpts {
func (opts *BotWebhookOpts) SetMaxConnections(max int8) *BotWebhookOpts {
opts.MaxConnections = max
return opts
}
// SetAllowedUpdates sets the Telegram update types that should be delivered to the webhook.
func (opts *BotWebHookOpts) SetAllowedUpdates(updates ...tgapi.UpdateType) *BotWebHookOpts {
func (opts *BotWebhookOpts) SetAllowedUpdates(updates ...tgapi.UpdateType) *BotWebhookOpts {
opts.AllowedUpdates = append([]tgapi.UpdateType(nil), updates...)
return opts
}
// SetDropPendingUpdates configures whether Telegram should drop pending updates while setting the webhook.
func (opts *BotWebHookOpts) SetDropPendingUpdates(drop bool) *BotWebHookOpts {
func (opts *BotWebhookOpts) SetDropPendingUpdates(drop bool) *BotWebhookOpts {
opts.DropPendingUpdates = drop
return opts
}
// SetSecretToken sets the secret token expected in Telegram webhook requests.
// The same token is also required to access /status when that endpoint is enabled.
func (opts *BotWebHookOpts) SetSecretToken(secretToken string) *BotWebHookOpts {
func (opts *BotWebhookOpts) SetSecretToken(secretToken string) *BotWebhookOpts {
opts.SecretToken = secretToken
return opts
}
// RunWebHookWithContext registers the webhook and serves incoming updates until ctx is canceled.
// RunWebhookWithContext registers the webhook and serves incoming updates until ctx is canceled.
//
// The bot uses the same update queue, worker pool, runner startup, and single-use lifecycle
// guarantees as RunWithContext. When opts.AllowedUpdates is empty, the bot-level update types
@@ -126,9 +127,9 @@ func (opts *BotWebHookOpts) SetSecretToken(secretToken string) *BotWebHookOpts {
//
// When two TLS files are provided, the method serves HTTPS using the existing key-then-cert
// argument order.
func (bot *Bot[T]) RunWebHookWithContext(ctx context.Context, opts *BotWebHookOpts, tlsFiles ...string) error {
func (bot *Bot[T]) RunWebhookWithContext(ctx context.Context, opts *BotWebhookOpts, tlsFiles ...string) error {
if opts == nil {
return errors.New("nil BotWebHookOpts")
return ErrNilBotWebhookOpts
}
if len(bot.prefixes) == 0 {
return ErrNoPrefixes
@@ -136,45 +137,48 @@ func (bot *Bot[T]) RunWebHookWithContext(ctx context.Context, opts *BotWebHookOp
if len(bot.plugins) == 0 {
return ErrNoPlugins
}
autoSecret := ""
if opts.SecretToken == "" {
rndSecret, err := generateToken(32)
if err != nil {
return err
}
opts.SecretToken = rndSecret
autoSecret = rndSecret
}
if opts.URL == "" {
return errors.New("empty BotWebHookOpts.URL")
return ErrNoBotWebhookOptsURL
}
if opts.MaxConnections > 100 || opts.MaxConnections <= 0 {
return errors.New("BotWebHookOpts.MaxConnections must between 1 and 100")
return ErrBotWebhookOptsMaxConnectionsRange
}
if err := validateWebhookPath(opts.Path, opts.UseStatusPath); err != nil {
return err
}
if opts.UseStatusPath && opts.SecretToken == "" {
return errors.New("BotWebHookOpts.SecretToken required when status path is enabled")
}
if err := validateWebhookTLSFiles(tlsFiles); err != nil {
return err
}
bot.webHookLogger = utils.CreateLogger("WEBHOOK", bot.GetLoggerLevel(), bot.logFormat, bot.logFormatter)
bot.addTokenReplacer(bot.webHookLogger)
if opts.SecretToken == "" {
bot.webHookLogger.Warnln("Bot webhook secret token empty. It's VERY recommended to set secret.")
}
if opts.Certificate != nil && bot.uploader == nil {
return errors.New("bot uploader nil, but certificate set")
return ErrBotUploaderWhenCertificate
}
return bot.runWebhookRuntime(ctx, func(runCtx context.Context) error {
if autoSecret != "" {
bot.webhookLogger.Warnln("Using webhook without secret is very dangerous. Using random 32 bytes token:", autoSecret)
}
i, err := bot.api.GetWebhookInfoWithContext(runCtx)
if err != nil {
return err
}
if i.URL == "" {
bot.webHookLogger.Warnln("API returned webhook info with empty URL. There may be a long-poll")
bot.webhookLogger.Warnln("API returned webhook info with empty URL. There may be a long-poll")
} else {
_, err = bot.api.DeleteWebhookWithContext(runCtx, tgapi.DeleteWebhook{})
if err != nil {
return err
}
bot.webHookLogger.Infof("Bot webhook deleted: %s", i.URL)
bot.webhookLogger.Infof("Bot webhook deleted: %s", i.URL)
}
allowedUpdates := bot.webhookAllowedUpdates(opts)
@@ -203,52 +207,52 @@ func (bot *Bot[T]) RunWebHookWithContext(ctx context.Context, opts *BotWebHookOp
return err
}
if !ok {
return errors.New("failed to set webhook")
return ErrSetWebhookFailed
}
if len(tlsFiles) == 2 {
return bot.runWebHookTLS(runCtx, opts, tlsFiles[0], tlsFiles[1])
return bot.runWebhookTLS(runCtx, opts, tlsFiles[0], tlsFiles[1])
}
return bot.runWebHook(runCtx, opts)
return bot.runWebhook(runCtx, opts)
})
}
// RunWebHook starts the webhook runtime with a background context.
// RunWebhook starts the webhook runtime with a background context.
//
// It is shorthand for RunWebHookWithContext(context.Background(), opts, tlsFiles...).
func (bot *Bot[T]) RunWebHook(opts *BotWebHookOpts, tlsFiles ...string) error {
return bot.RunWebHookWithContext(context.Background(), opts, tlsFiles...)
// It is shorthand for RunWebhookWithContext(context.Background(), opts, tlsFiles...).
func (bot *Bot[T]) RunWebhook(opts *BotWebhookOpts, tlsFiles ...string) error {
return bot.RunWebhookWithContext(context.Background(), opts, tlsFiles...)
}
// CloseWebHook removes the current Telegram webhook registration.
// CloseWebhook removes the current Telegram webhook registration.
//
// It is separate from Close, which only releases local resources.
// Call it before switching a deployment from webhook delivery to polling.
func (bot *Bot[T]) CloseWebHook() error {
func (bot *Bot[T]) CloseWebhook() error {
var e []error
if bot.api == nil {
e = append(e, errors.New("bot api nil"))
e = append(e, ErrBotAPINil)
} else {
if _, err := bot.api.DeleteWebhook(tgapi.DeleteWebhook{}); err != nil {
if bot.webHookLogger != nil {
bot.webHookLogger.Errorf("Failed to close webhook: %s", err.Error())
if bot.webhookLogger != nil {
bot.webhookLogger.Errorf("Failed to close webhook: %s", err.Error())
} else if bot.logger != nil {
bot.logger.Errorf("Failed to close webhook: %s", err.Error())
}
e = append(e, err)
}
}
if bot.webHookLogger != nil {
if err := bot.webHookLogger.Close(); err != nil {
if bot.webhookLogger != nil {
if err := bot.webhookLogger.Close(); err != nil {
e = append(e, err)
}
bot.webHookLogger = nil
bot.webhookLogger = nil
}
return errors.Join(e...)
}
func (bot *Bot[T]) webhookAllowedUpdates(opts *BotWebHookOpts) []tgapi.UpdateType {
func (bot *Bot[T]) webhookAllowedUpdates(opts *BotWebhookOpts) []tgapi.UpdateType {
if len(opts.AllowedUpdates) > 0 {
return append([]tgapi.UpdateType(nil), opts.AllowedUpdates...)
}
@@ -264,6 +268,10 @@ func (bot *Bot[T]) runWebhookRuntime(ctx context.Context, run func(context.Conte
runCtx, cancel := context.WithCancel(ctx)
defer cancel()
if bot.webhookLogger == nil {
bot.webhookLogger = utils.CreateLogger("WEBHOOK", bot.GetLoggerLevel(), bot.logFormat, bot.logFormatter)
}
bot.addTokenReplacer(bot.webhookLogger)
bot.ExecRunners(runCtx)
workersDone := make(chan struct{})
@@ -282,7 +290,7 @@ func (bot *Bot[T]) runWebhookRuntime(ctx context.Context, run func(context.Conte
return runErr
}
func updateHandler[T any](ctx context.Context, bot *Bot[T], secret string) http.HandlerFunc {
func updateHandler[T any](ctx context.Context, bot *Bot[T], secret []byte) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
defer func() {
_ = r.Body.Close()
@@ -291,7 +299,9 @@ func updateHandler[T any](ctx context.Context, bot *Bot[T], secret string) http.
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
if secret != "" && r.Header.Get("X-Telegram-Bot-Api-Secret-Token") != secret {
provided := []byte(r.Header.Get("X-Telegram-Bot-Api-Secret-Token"))
if len(secret) > 0 && subtle.ConstantTimeCompare(secret, provided) != 1 {
w.WriteHeader(http.StatusForbidden)
return
}
@@ -315,12 +325,12 @@ func updateHandler[T any](ctx context.Context, bot *Bot[T], secret string) http.
var up tgapi.Update
if err := json.Unmarshal(data, &up); err != nil {
w.WriteHeader(http.StatusBadRequest)
bot.webHookLogger.Errorln(err)
bot.webhookLogger.Errorln(err)
return
}
bot.webHookLogger.Debugf("UPDATE id=%d type=%s size=%d from=%s", up.UpdateID, up.Type, len(data), r.RemoteAddr)
bot.webhookLogger.Debugf("UPDATE id=%d type=%s size=%d from=%s", up.UpdateID, up.Type, len(data), r.RemoteAddr)
if err := bot.enqueueUpdate(ctx, up); err != nil {
bot.webHookLogger.Errorln(err)
bot.webhookLogger.Errorln(err)
w.WriteHeader(http.StatusServiceUnavailable)
return
}
@@ -328,7 +338,7 @@ func updateHandler[T any](ctx context.Context, bot *Bot[T], secret string) http.
}
}
func statusHandler[T any](bot *Bot[T], opts *BotWebHookOpts) http.HandlerFunc {
func statusHandler[T any](bot *Bot[T], secret []byte) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
auth := ""
if r.Header.Get("Authorization") != "" {
@@ -336,116 +346,98 @@ func statusHandler[T any](bot *Bot[T], opts *BotWebHookOpts) http.HandlerFunc {
} else if r.Header.Get("X-Telegram-Bot-Api-Secret-Token") != "" {
auth = r.Header.Get("X-Telegram-Bot-Api-Secret-Token")
}
if auth != opts.SecretToken {
if len(secret) > 0 && subtle.ConstantTimeCompare(secret, []byte(auth)) != 1 {
w.WriteHeader(http.StatusNotFound)
return
}
i, err := bot.api.GetWebhookInfoWithContext(r.Context())
if err != nil {
bot.webHookLogger.Errorln(err)
bot.webhookLogger.Errorln(err)
w.WriteHeader(http.StatusInternalServerError)
return
}
data, err := json.MarshalIndent(i, "", " ")
if err != nil {
bot.webHookLogger.Errorln(err)
bot.webhookLogger.Errorln(err)
w.WriteHeader(http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
if _, err := fmt.Fprint(w, string(data)); err != nil {
bot.webHookLogger.Errorln(err)
bot.webhookLogger.Errorln(err)
}
}
}
func (bot *Bot[T]) newWebHookMux(ctx context.Context, opts *BotWebHookOpts) *http.ServeMux {
func (bot *Bot[T]) newWebhookMux(ctx context.Context, opts *BotWebhookOpts) *http.ServeMux {
token := []byte(opts.SecretToken)
r := http.NewServeMux()
if opts.UseStatusPath {
r.HandleFunc("/status", statusHandler(bot, opts))
r.HandleFunc("/status", statusHandler(bot, token))
}
r.HandleFunc(opts.Path, updateHandler(ctx, bot, opts.SecretToken))
r.HandleFunc(opts.Path, updateHandler(ctx, bot, token))
return r
}
func (bot *Bot[T]) runWebHook(ctx context.Context, opts *BotWebHookOpts) error {
func (bot *Bot[T]) baseRunWebhook(ctx context.Context, opts *BotWebhookOpts, runFunc func(*http.Server, chan error)) error {
srv := &http.Server{
Addr: fmt.Sprintf(":%d", opts.LocalPort),
Handler: bot.newWebHookMux(ctx, opts),
Handler: bot.newWebhookMux(ctx, opts),
}
errCh := make(chan error, 1)
go func() {
go runFunc(srv, errCh)
bot.webhookLogger.Infoln(fmt.Sprintf("Bot Webhook started at %s; waiting for updates at %s", srv.Addr, opts.URL))
select {
case <-ctx.Done():
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := srv.Shutdown(shutdownCtx); err != nil {
return err
}
return <-errCh
case err := <-errCh:
return err
}
}
func (bot *Bot[T]) runWebhook(ctx context.Context, opts *BotWebhookOpts) error {
return bot.baseRunWebhook(ctx, opts, func(srv *http.Server, errCh chan error) {
err := srv.ListenAndServe()
if err != nil && !errors.Is(err, http.ErrServerClosed) {
errCh <- err
return
}
errCh <- nil
}()
})
bot.webHookLogger.Infoln(fmt.Sprintf("Bot WebHook started at %s; waiting for updates at %s", srv.Addr, opts.URL))
select {
case <-ctx.Done():
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := srv.Shutdown(shutdownCtx); err != nil {
return err
}
return <-errCh
case err := <-errCh:
return err
}
}
func (bot *Bot[T]) runWebHookTLS(ctx context.Context, opts *BotWebHookOpts, key, cert string) error {
srv := &http.Server{
Addr: fmt.Sprintf(":%d", opts.LocalPort),
Handler: bot.newWebHookMux(ctx, opts),
}
errCh := make(chan error, 1)
go func() {
func (bot *Bot[T]) runWebhookTLS(ctx context.Context, opts *BotWebhookOpts, key, cert string) error {
return bot.baseRunWebhook(ctx, opts, func(srv *http.Server, errCh chan error) {
err := srv.ListenAndServeTLS(cert, key)
if err != nil && !errors.Is(err, http.ErrServerClosed) {
errCh <- err
return
}
errCh <- nil
}()
bot.webHookLogger.Infoln(fmt.Sprintf("Bot webhook started with TLS(%s, %s) at %s; waiting for updates at %s", key, cert, srv.Addr, opts.URL))
select {
case <-ctx.Done():
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := srv.Shutdown(shutdownCtx); err != nil {
return err
}
return <-errCh
case err := <-errCh:
return err
}
})
}
func validateWebhookPath(path string, useStatusPath bool) error {
if path == "" {
return errors.New("empty BotWebHookOpts.Path")
return ErrBotWebhookOptsEmptyPath
}
if !strings.HasPrefix(path, "/") {
return errors.New("BotWebHookOpts.Path must start with '/'")
return ErrBotWebhookOptsPathNoSlash
}
if strings.Contains(path, "?") || strings.Contains(path, "#") {
return errors.New("BotWebHookOpts.Path must not contain query or fragment")
return ErrBotWebhookOptsPathHasQueryOrFragment
}
if useStatusPath && path == "/status" {
return errors.New("BotWebHookOpts.Path must not be '/status' when status path is enabled")
return ErrBotWebhookOptsPathCollidesStatus
}
return nil
}
@@ -455,8 +447,8 @@ func validateWebhookTLSFiles(tlsFiles []string) error {
case 0, 2:
return nil
case 1:
return errors.New("you must specify both private and public keys")
return ErrBotWebhookTLSFilesIncomplete
default:
return errors.New("too many files; you must specify only private and public keys")
return ErrBotWebhookTLSFilesTooMany
}
}
+99 -23
View File
@@ -9,6 +9,7 @@ import (
"strings"
"sync/atomic"
"testing"
"time"
"git.scuroneko.dev/scuroneko/laniakea/tgapi"
"git.scuroneko.dev/scuroneko/sneklog/v2"
@@ -35,17 +36,17 @@ func TestEnqueueUpdateCopiesValue(t *testing.T) {
func TestUpdateHandlerEnqueuesUpdate(t *testing.T) {
bot := &Bot[NoData]{
updateQueue: make(chan *tgapi.Update, 1),
webHookLogger: sneklog.NewLogger(),
webhookLogger: sneklog.NewLogger(),
}
t.Cleanup(func() {
_ = bot.webHookLogger.Close()
_ = bot.webhookLogger.Close()
})
req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(`{"update_id":7,"message":{"message_id":1,"date":1,"chat":{"id":1,"type":"private"},"text":"/start"}}`))
req.Header.Set("X-Telegram-Bot-Api-Secret-Token", "secret")
rec := httptest.NewRecorder()
updateHandler(context.Background(), bot, "secret").ServeHTTP(rec, req)
updateHandler(context.Background(), bot, []byte("secret")).ServeHTTP(rec, req)
if rec.Result().StatusCode != http.StatusOK {
t.Fatalf("unexpected status: got %d want %d", rec.Result().StatusCode, http.StatusOK)
@@ -93,7 +94,7 @@ func TestRunWebhookRuntimeExecutesRunners(t *testing.T) {
NewRunner("runner", func(bot *Bot[NoData]) error {
calls.Add(1)
return nil
}).Onetime(true).Async(false),
}).Async(false),
},
}
t.Cleanup(func() {
@@ -108,11 +109,83 @@ func TestRunWebhookRuntimeExecutesRunners(t *testing.T) {
}
}
func TestRunWebhookRuntimePreservesConfiguredWebhookLogger(t *testing.T) {
webhookLogger := sneklog.NewLogger()
bot := &Bot[NoData]{
logger: sneklog.NewLogger(),
webhookLogger: webhookLogger,
updateQueue: make(chan *tgapi.Update, 1),
maxWorkers: 1,
}
t.Cleanup(func() {
_ = bot.logger.Close()
if bot.webhookLogger != nil {
_ = bot.webhookLogger.Close()
}
})
if err := bot.runWebhookRuntime(context.Background(), func(context.Context) error { return nil }); err != nil {
t.Fatalf("runWebhookRuntime returned error: %v", err)
}
if bot.webhookLogger != webhookLogger {
t.Fatal("expected runWebhookRuntime to preserve configured webhook logger")
}
}
func TestRunWebhookRuntimeProcessesEnqueuedUpdate(t *testing.T) {
var calls atomic.Int32
plugin := NewPlugin[NoData]("demo")
plugin.Command("start", func(ctx *MessageContext, db NoData) error {
calls.Add(1)
return nil
})
bot := &Bot[NoData]{
logger: sneklog.NewLogger(),
webhookLogger: sneklog.NewLogger(),
prefixes: []string{"/"},
plugins: []Plugin[NoData]{*plugin},
updateQueue: make(chan *tgapi.Update, 1),
maxWorkers: 1,
}
t.Cleanup(func() {
_ = bot.logger.Close()
_ = bot.webhookLogger.Close()
})
err := bot.runWebhookRuntime(context.Background(), func(ctx context.Context) error {
req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(`{"update_id":9,"message":{"message_id":1,"date":1,"chat":{"id":1,"type":"private"},"from":{"id":2,"is_bot":false,"first_name":"Test"},"text":"/start"}}`))
rec := httptest.NewRecorder()
updateHandler(ctx, bot, []byte("")).ServeHTTP(rec, req)
if rec.Result().StatusCode != http.StatusOK {
t.Fatalf("unexpected status: got %d want %d", rec.Result().StatusCode, http.StatusOK)
}
deadline := time.After(time.Second)
for calls.Load() == 0 {
select {
case <-deadline:
t.Fatal("webhook runtime did not process enqueued update")
default:
time.Sleep(time.Millisecond)
}
}
return nil
})
if err != nil {
t.Fatalf("runWebhookRuntime returned error: %v", err)
}
if calls.Load() != 1 {
t.Fatalf("expected command handler to run once, got %d", calls.Load())
}
}
func TestWebhookAllowedUpdatesUsesBotUpdateTypesByDefault(t *testing.T) {
bot := &Bot[NoData]{
updateTypes: []tgapi.UpdateType{tgapi.UpdateTypeMessage, tgapi.UpdateTypeCallbackQuery},
}
opts := NewBotWebHookOpts()
opts := NewBotWebhookOpts()
got := bot.webhookAllowedUpdates(opts)
if len(got) != 2 {
@@ -185,16 +258,16 @@ func TestValidateWebhookTLSFiles(t *testing.T) {
func TestUpdateHandlerRejectsOversizedBody(t *testing.T) {
bot := &Bot[NoData]{
updateQueue: make(chan *tgapi.Update, 1),
webHookLogger: sneklog.NewLogger(),
webhookLogger: sneklog.NewLogger(),
}
t.Cleanup(func() {
_ = bot.webHookLogger.Close()
_ = bot.webhookLogger.Close()
})
req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(strings.Repeat("a", (256<<10)+1)))
rec := httptest.NewRecorder()
updateHandler(context.Background(), bot, "").ServeHTTP(rec, req)
updateHandler(context.Background(), bot, []byte("")).ServeHTTP(rec, req)
if rec.Result().StatusCode != http.StatusRequestEntityTooLarge {
t.Fatalf("unexpected status: got %d want %d", rec.Result().StatusCode, http.StatusRequestEntityTooLarge)
@@ -222,13 +295,13 @@ func TestStatusHandlerRequiresMatchingSecret(t *testing.T) {
bot := &Bot[NoData]{
api: api,
webHookLogger: sneklog.NewLogger(),
webhookLogger: sneklog.NewLogger(),
}
t.Cleanup(func() {
_ = bot.webHookLogger.Close()
_ = bot.webhookLogger.Close()
})
handler := statusHandler(bot, &BotWebHookOpts{SecretToken: "secret"})
handler := statusHandler(bot, []byte("secret"))
tests := []struct {
name string
@@ -238,6 +311,9 @@ func TestStatusHandlerRequiresMatchingSecret(t *testing.T) {
}{
{name: "missing auth", wantStatus: http.StatusNotFound},
{name: "wrong auth", headerName: "Authorization", headerVal: "wrong", wantStatus: http.StatusNotFound},
{name: "matching length wrong content", headerName: "X-Telegram-Bot-Api-Secret-Token", headerVal: "secres", wantStatus: http.StatusNotFound},
{name: "shared prefix shorter", headerName: "X-Telegram-Bot-Api-Secret-Token", headerVal: "secre", wantStatus: http.StatusNotFound},
{name: "shared prefix longer", headerName: "X-Telegram-Bot-Api-Secret-Token", headerVal: "secretxx", wantStatus: http.StatusNotFound},
{name: "matching telegram header", headerName: "X-Telegram-Bot-Api-Secret-Token", headerVal: "secret", wantStatus: http.StatusOK},
}
@@ -258,14 +334,14 @@ func TestStatusHandlerRequiresMatchingSecret(t *testing.T) {
}
}
func TestRunWebHookWithContextRejectsInvalidTLSFilesBeforeRemoteSetup(t *testing.T) {
func TestRunWebhookWithContextRejectsInvalidTLSFilesBeforeRemoteSetup(t *testing.T) {
bot := &Bot[NoData]{
prefixes: []string{"/"},
plugins: []Plugin[NoData]{{name: "demo"}},
}
opts := NewBotWebHookOpts().SetURL("https://bot.example.com")
opts := NewBotWebhookOpts().SetURL("https://bot.example.com")
err := bot.RunWebHookWithContext(context.Background(), opts, "cert.pem")
err := bot.RunWebhookWithContext(context.Background(), opts, "cert.pem")
if err == nil {
t.Fatal("expected tls validation error, got nil")
}
@@ -274,20 +350,20 @@ func TestRunWebHookWithContextRejectsInvalidTLSFilesBeforeRemoteSetup(t *testing
}
}
func TestRunWebHookWithContextRequiresSecretWhenStatusPathEnabled(t *testing.T) {
func TestRunWebhookWithContextAutoGeneratesSecretWhenEmpty(t *testing.T) {
bot := &Bot[NoData]{
prefixes: []string{"/"},
plugins: []Plugin[NoData]{{name: "demo"}},
}
opts := NewBotWebHookOpts().
SetURL("https://bot.example.com").
SetUseStatusPath(true)
// No SecretToken, no URL — function should auto-generate the token
// and then fail with ErrNoBotWebhookOptsURL before any network call.
opts := NewBotWebhookOpts().SetUseStatusPath(true)
err := bot.RunWebHookWithContext(context.Background(), opts)
if err == nil {
t.Fatal("expected status-path secret validation error, got nil")
err := bot.RunWebhookWithContext(context.Background(), opts)
if !errors.Is(err, ErrNoBotWebhookOptsURL) {
t.Fatalf("expected ErrNoBotWebhookOptsURL after auto-generation, got: %v", err)
}
if !strings.Contains(err.Error(), "SecretToken required") {
t.Fatalf("unexpected error: %v", err)
if opts.SecretToken == "" {
t.Fatal("expected SecretToken to be auto-generated, got empty string")
}
}
+3 -7
View File
@@ -10,8 +10,8 @@ import (
"git.scuroneko.dev/scuroneko/laniakea/tgapi"
)
// CmdRegexp matches command names allowed for Telegram command registration.
var CmdRegexp = regexp.MustCompile("^[_a-z0-9]{1,32}$")
// cmdRegexp matches command names allowed for Telegram command registration.
var cmdRegexp = regexp.MustCompile("^[_a-z0-9]{1,32}$")
// ErrTooManyCommands is returned when the total number of registered commands
// exceeds Telegram's limit of 100 bot commands per bot.
@@ -21,7 +21,6 @@ var CmdRegexp = regexp.MustCompile("^[_a-z0-9]{1,32}$")
// bot initialization.
var ErrTooManyCommands = errors.New("too many commands. max 100")
// Internal helper to build a BotCommand description with generated usage text.
func generateBotCommand[T any](cmd *Command[T]) tgapi.BotCommand {
desc := ""
if len(cmd.description) > 0 {
@@ -45,10 +44,8 @@ func generateBotCommand[T any](cmd *Command[T]) tgapi.BotCommand {
return tgapi.BotCommand{Command: cmd.command, Description: usage}
}
// Internal helper to validate Telegram command names.
func checkCmdRegex(cmd string) bool { return CmdRegexp.MatchString(cmd) }
func checkCmdRegex(cmd string) bool { return cmdRegexp.MatchString(cmd) }
// Internal helper to collect non-skipped, valid commands from one plugin.
func gatherCommandsForPlugin[T any](pl Plugin[T]) []tgapi.BotCommand {
commands := make([]tgapi.BotCommand, 0)
names := make([]string, 0, len(pl.commands))
@@ -70,7 +67,6 @@ func gatherCommandsForPlugin[T any](pl Plugin[T]) []tgapi.BotCommand {
return commands
}
// Internal helper to collect all auto-generated commands from registered plugins.
func gatherCommands[T any](bot *Bot[T]) []tgapi.BotCommand {
commands := make([]tgapi.BotCommand, 0)
for _, pl := range bot.plugins {
+6 -6
View File
@@ -44,9 +44,9 @@ func TestAutoGenerateCommandsChecksLimitBeforeDelete(t *testing.T) {
}()
plugin := NewPlugin[NoData]("overflow")
exec := func(ctx *MsgContext, db NoData) error { return nil }
exec := func(ctx *MessageContext, db NoData) error { return nil }
for i := 0; i < 101; i++ {
plugin.AddCommand(NewCommand(exec, "cmd"+strconv.Itoa(i)))
plugin.Command("cmd"+strconv.Itoa(i), exec)
}
bot := &Bot[NoData]{
@@ -66,11 +66,11 @@ func TestAutoGenerateCommandsChecksLimitBeforeDelete(t *testing.T) {
func TestGatherCommandsForPluginReturnsSortedCommands(t *testing.T) {
plugin := NewPlugin[NoData]("sorted")
exec := func(ctx *MsgContext, db NoData) error { return nil }
exec := func(ctx *MessageContext, db NoData) error { return nil }
plugin.AddCommand(NewCommand(exec, "zeta"))
plugin.AddCommand(NewCommand(exec, "alpha"))
plugin.AddCommand(NewCommand(exec, "mid"))
plugin.Command("zeta", exec)
plugin.Command("alpha", exec)
plugin.Command("mid", exec)
commands := gatherCommandsForPlugin(*plugin)
got := make([]string, 0, len(commands))
+214
View File
@@ -0,0 +1,214 @@
package laniakea
import (
"errors"
"regexp"
"git.scuroneko.dev/scuroneko/extypes"
)
// CommandValueType defines the expected type of command argument.
type CommandValueType string
const (
// CommandValueString expects any non-empty string.
CommandValueString CommandValueType = "string"
// CommandValueInt expects a decimal integer (digits only).
CommandValueInt CommandValueType = "int"
// CommandValueBool expects an exact "true" or "false".
CommandValueBool CommandValueType = "bool"
// CommandValueAny accepts any input without validation.
CommandValueAny CommandValueType = "any"
)
var (
// CommandRegexInt matches one or more digits.
CommandRegexInt = regexp.MustCompile(`^\d+$`)
// CommandRegexString matches any non-empty string.
CommandRegexString = regexp.MustCompile(`^.+$`)
// CommandRegexBool matches true or false.
CommandRegexBool = regexp.MustCompile(`^(true|false)$`)
)
// ErrCmdArgCountMismatch is returned when the number of provided arguments
// is less than the number of required arguments.
var ErrCmdArgCountMismatch = errors.New("command arg count mismatch")
// ErrCmdArgRegexpMismatch is returned when an argument fails regex validation.
var ErrCmdArgRegexpMismatch = errors.New("command arg regexp mismatch")
var (
errCommandNotFound = errors.New("command not found")
errPayloadNotFound = errors.New("payload not found")
)
// CommandArg defines a single argument for a command, including type, regex,
// and whether it is required.
type CommandArg struct {
valueType CommandValueType // Type of expected value
text string // Human-readable description (not used in validation)
regex *regexp.Regexp // Regex used to validate input
required bool // Whether this argument must be provided
}
// NewCommandArg creates an optional argument without value validation.
func NewCommandArg(text string) CommandArg {
return CommandArg{CommandValueAny, text, nil, false}
}
// SetValueType sets expected value type and switches built-in validation regexp.
func (c CommandArg) SetValueType(t CommandValueType) CommandArg {
var regex *regexp.Regexp
switch t {
case CommandValueInt:
regex = CommandRegexInt
case CommandValueBool:
regex = CommandRegexBool
case CommandValueString:
regex = CommandRegexString
}
c.valueType = t
c.regex = regex
return c
}
// SetRequired marks this argument as required.
// Returns the receiver for method chaining.
func (c CommandArg) SetRequired() CommandArg {
c.required = true
return c
}
// CommandExecutor is the function type that executes a command.
// It receives the message context and injected application data.
// Returning a non-nil error routes it through the bot's error handler.
type CommandExecutor[T AppData] func(ctx *MessageContext, dbContext T) error
// Command represents a bot command with arguments, description, and executor.
// Can be registered in a Plugin and optionally skipped from auto-generation.
type Command[T AppData] struct {
command string // The command trigger (e.g., "/start")
description string // Human-readable description for help
exec CommandExecutor[T] // Function to execute when command is triggered
args extypes.Slice[CommandArg] // List of expected arguments
middlewares extypes.Slice[Middleware[T]] // Optional middleware chain
skipAutoCmd bool // If true, this command won't be auto-added to help menus
}
// NewCommand creates a new Command with the given identifier, executor, and arguments.
//
// The identifier is used as the routing key for both /-prefixed commands and
// callback payloads — the difference is registration: pass the result to
// Plugin.AddCommand/Plugin.Command for message routing, or to
// Plugin.AddPayload/Plugin.Payload for callback_data routing.
//
// For /-commands the identifier must not include the leading slash
// (e.g. "start", not "/start") and should match [_a-z0-9]{1,32} to satisfy
// Telegram's BotCommand validation. Payload identifiers may use any bytes
// that fit Telegram's callback_data limit, though the configured payload
// encoding may impose its own restrictions.
func NewCommand[T any](command string, exec CommandExecutor[T], args ...CommandArg) *Command[T] {
return &Command[T]{command, "", exec, args, make(extypes.Slice[Middleware[T]], 0), false}
}
// Use adds a middleware to the command's execution chain.
// Middlewares are executed in the order they are added.
func (c *Command[T]) Use(m Middleware[T]) *Command[T] {
c.middlewares = c.middlewares.Push(m)
return c
}
// SetDescription sets the human-readable description of the command.
func (c *Command[T]) SetDescription(desc string) *Command[T] {
c.description = desc
return c
}
// SkipCommandAutoGen marks this command to be excluded from auto-generated help menus.
func (c *Command[T]) SkipCommandAutoGen() *Command[T] {
c.skipAutoCmd = true
return c
}
func (c *Command[T]) validateArgs(args []string) error {
for i := range c.args.Len() {
if i >= len(args) && c.args.Get(i).required {
return ErrCmdArgCountMismatch
}
}
// Validate each argument against its regex
for i, arg := range args {
if i >= c.args.Len() {
// Extra arguments beyond defined args are ignored
break
}
cmdArg := c.args.Get(i)
if cmdArg.regex == nil {
continue // Skip validation for CommandValueAny.
}
if !cmdArg.regex.MatchString(arg) {
return ErrCmdArgRegexpMismatch
}
}
return nil
}
func (c *Command[T]) clone() *Command[T] {
if c == nil {
return nil
}
cloned := *c
cloned.args = append(extypes.Slice[CommandArg](nil), c.args...)
cloned.middlewares = append(extypes.Slice[Middleware[T]](nil), c.middlewares...)
return &cloned
}
// CommandGroup builds a set of commands with a shared name prefix and middleware.
type CommandGroup[T any] struct {
prefix string
middlewares extypes.Slice[Middleware[T]]
commands extypes.Slice[*Command[T]]
}
// NewCommandGroup creates a command group that prefixes every added command.
func NewCommandGroup[T any](prefix string) *CommandGroup[T] {
return &CommandGroup[T]{
prefix: prefix,
middlewares: make([]Middleware[T], 0),
commands: make([]*Command[T], 0),
}
}
// Use adds middleware that runs before each command's own middleware.
func (g *CommandGroup[T]) Use(m Middleware[T]) *CommandGroup[T] {
g.middlewares = append(g.middlewares, m)
return g
}
// AddCommand adds a prefixed copy of cmd to the group.
func (g *CommandGroup[T]) AddCommand(cmd *Command[T]) *CommandGroup[T] {
if cmd == nil {
return g
}
newCmd := cmd.clone()
newCmd.command = g.prefix + cmd.command
g.commands = g.commands.Push(newCmd)
return g
}
// Build returns command copies with group middleware prepended.
func (g *CommandGroup[T]) Build() []*Command[T] {
commands := make([]*Command[T], 0)
for _, cmd := range g.commands {
cloned := cmd.clone()
cloned.middlewares = append(
append(extypes.Slice[Middleware[T]]{}, g.middlewares...),
cloned.middlewares...,
)
commands = append(commands, cloned)
}
return commands
}
+2 -2
View File
@@ -5,7 +5,7 @@ Core concepts:
- Bot manages Telegram API access, update processing, logging, rate limiting, and dependency injection.
- Plugins group commands, payloads, and non-command update handlers behind shared middleware.
- MsgContext provides access to the current update and reply/edit/delete helpers.
- MessageContext provides access to the current update and reply/edit/delete helpers.
- InlineKeyboard builds callback-driven keyboards and structured payloads.
- DraftProvider accumulates multi-step replies before sending them.
- L10n stores key-based translations with fallback behavior.
@@ -27,7 +27,7 @@ Example usage:
return bot.Run()
Configure bots, plugins, and localization before starting Run, RunWithContext, or RunWebHookWithContext.
Configure bots, plugins, and localization before starting Run, RunWithContext, or RunWebhookWithContext.
Runtime accessors are safe for concurrent use unless stated otherwise.
*/
package laniakea
+16 -11
View File
@@ -8,14 +8,15 @@ import (
"git.scuroneko.dev/scuroneko/laniakea/tgapi"
)
// Interface for generating unique draft IDs.
type draftIDGenerator interface {
// Next returns the next unique draft ID.
Next() uint64
}
// RandomDraftIDGenerator generates draft IDs using cryptographically secure random numbers.
// Suitable for distributed systems or when ID predictability is undesirable.
// RandomDraftIDGenerator generates draft IDs using math/rand/v2.
//
// Suitable for general use thanks to the wide 64-bit value space. Not suitable
// for security-sensitive purposes — use crypto/rand if unpredictability against
// an adversary matters.
type RandomDraftIDGenerator struct{}
// Next returns a random 64-bit unsigned integer.
@@ -29,7 +30,7 @@ type LinearDraftIDGenerator struct {
lastID atomic.Uint64
}
// Next returns the next linear ID, atomically incremented.о
// Next returns the next linear ID, atomically incremented.
func (g *LinearDraftIDGenerator) Next() uint64 {
return g.lastID.Add(1)
}
@@ -46,7 +47,7 @@ type DraftProvider struct {
// NewRandomDraftProvider creates a new DraftProvider using random draft IDs.
//
// The provider will use cryptographically secure random numbers for draft IDs.
// The provider will use random numbers for draft IDs.
// All drafts created via this provider will have unpredictable, unique IDs.
func NewRandomDraftProvider(api *tgapi.API) *DraftProvider {
return &DraftProvider{
@@ -186,8 +187,7 @@ func (d *Draft) Clear() {
// Delete removes the draft from its provider and clears its content.
//
// This is an internal method used by Flush(). You may call it manually if you
// want to cancel a draft without sending it.
// You may call it manually if you want to cancel a draft without sending it.
func (d *Draft) Delete() {
if d.provider != nil {
d.provider.mu.Lock()
@@ -238,15 +238,20 @@ func (d *Draft) Flush() error {
return err
}
// Internal helper for Push that updates the server-side draft.
// The candidate Message (current content + new text) is validated before any
// mutation, so a validation failure leaves the draft unchanged. After the
// validation passes, Message is committed locally regardless of whether the
// API call succeeds (per the Push docs: local state reflects the user's
// intent, network failures can be retried).
func (d *Draft) push(text string) error {
if d.chatID == 0 {
return ErrDraftChatIDZero
}
d.Message += text
if err := validateMessageText(d.Message); err != nil {
candidate := d.Message + text
if err := validateMessageText(candidate); err != nil {
return err
}
d.Message = candidate
params := tgapi.SendMessageDraft{
ChatID: d.chatID,
DraftID: d.ID,
+19 -1
View File
@@ -19,7 +19,7 @@ func TestDraftFlushRequiresChatID(t *testing.T) {
}
func TestMsgContextNewDraftWorksWithoutLimiter(t *testing.T) {
ctx := &MsgContext{
ctx := &MessageContext{
API: &tgapi.API{},
Msg: &tgapi.Message{
Chat: &tgapi.Chat{ID: 42, Type: tgapi.ChatTypePrivate},
@@ -31,6 +31,7 @@ func TestMsgContextNewDraftWorksWithoutLimiter(t *testing.T) {
draft := ctx.NewDraft()
if draft == nil {
t.Fatal("expected draft")
return
}
if draft.chatID != 42 {
t.Fatalf("unexpected chat id: %d", draft.chatID)
@@ -53,3 +54,20 @@ func TestDraftPushRejectsLongMessage(t *testing.T) {
t.Fatalf("expected ErrMessageTooLong, got %v", err)
}
}
// TestDraftPushLeavesMessageUnchangedOnValidationFailure covers the validation
// order fix: when the candidate Message (current + new text) overflows the
// Telegram limit, the existing Message must remain intact so callers can
// recover and retry with a shorter payload instead of finding the draft in
// a half-mutated state.
func TestDraftPushLeavesMessageUnchangedOnValidationFailure(t *testing.T) {
draft := NewRandomDraftProvider(&tgapi.API{}).NewDraft(tgapi.ParseNone).SetChat(42, 0)
draft.Message = "hello"
if err := draft.Push(strings.Repeat("a", maxMessageTextLen+1)); !errors.Is(err, ErrMessageTooLong) {
t.Fatalf("expected ErrMessageTooLong, got %v", err)
}
if draft.Message != "hello" {
t.Fatalf("expected draft Message to stay %q, got %q", "hello", draft.Message)
}
}
+31
View File
@@ -38,6 +38,8 @@ var (
ErrAPIIsNil = errors.New("api is nil")
// ErrMessageIDZero reports that an operation requires a non-zero message ID.
ErrMessageIDZero = errors.New("message ID is zero")
)
var (
// ErrBindArgsTargetNotPointer reports that BindArgs received a nil or non-pointer destination.
ErrBindArgsTargetNotPointer = errors.New("bind args: dst must be a non-nil pointer")
// ErrBindArgsTargetNotStruct reports that BindArgs received a pointer to a non-struct value.
@@ -60,6 +62,35 @@ var (
ErrSceneRuntimeNil = errors.New("scene runtime is nil")
)
var (
// ErrNilBotWebhookOpts reports that a nil BotWebhookOpts was passed.
ErrNilBotWebhookOpts = errors.New("nil BotWebhookOpts")
// ErrNoBotWebhookOptsURL reports that BotWebhookOpts.URL is empty.
ErrNoBotWebhookOptsURL = errors.New("empty BotWebhookOpts.URL")
// ErrBotWebhookOptsMaxConnectionsRange reports that BotWebhookOpts.MaxConnections is out of range.
ErrBotWebhookOptsMaxConnectionsRange = errors.New("BotWebhookOpts.MaxConnections must be between 1 and 100")
// ErrBotUploaderWhenCertificate reports that a certificate was set without an uploader.
ErrBotUploaderWhenCertificate = errors.New("bot uploader nil, but certificate set")
// ErrStatusPathSecretRequired reports that UseStatusPath requires SecretToken to be set.
ErrStatusPathSecretRequired = errors.New("SecretToken required when UseStatusPath is enabled")
// ErrSetWebhookFailed reports that Telegram rejected the setWebhook request.
ErrSetWebhookFailed = errors.New("failed to set webhook")
// ErrBotAPINil reports that an operation requires an API client but none is set.
ErrBotAPINil = errors.New("bot api is nil")
// ErrBotWebhookOptsEmptyPath reports that BotWebhookOpts.Path is empty.
ErrBotWebhookOptsEmptyPath = errors.New("empty BotWebhookOpts.Path")
// ErrBotWebhookOptsPathNoSlash reports that BotWebhookOpts.Path does not start with '/'.
ErrBotWebhookOptsPathNoSlash = errors.New("BotWebhookOpts.Path must start with '/'")
// ErrBotWebhookOptsPathHasQueryOrFragment reports that BotWebhookOpts.Path contains a query or fragment.
ErrBotWebhookOptsPathHasQueryOrFragment = errors.New("BotWebhookOpts.Path must not contain query or fragment")
// ErrBotWebhookOptsPathCollidesStatus reports that BotWebhookOpts.Path collides with the reserved /status endpoint.
ErrBotWebhookOptsPathCollidesStatus = errors.New("BotWebhookOpts.Path must not be '/status' when status path is enabled")
// ErrBotWebhookTLSFilesIncomplete reports that only one of the two TLS files was provided.
ErrBotWebhookTLSFilesIncomplete = errors.New("you must specify both private and public keys")
// ErrBotWebhookTLSFilesTooMany reports that more than two TLS files were provided.
ErrBotWebhookTLSFilesTooMany = errors.New("too many files; you must specify only private and public keys")
)
func validateMessageText(text string) error {
length := utf8.RuneCountInString(text)
switch {
+169 -29
View File
@@ -6,6 +6,7 @@ import (
"encoding/json"
"errors"
"fmt"
"strings"
"time"
"git.scuroneko.dev/scuroneko/laniakea/tgapi"
@@ -14,10 +15,27 @@ import (
// ErrInvalidPayloadType is returned when callback payload encoding type is unknown.
var ErrInvalidPayloadType = errors.New("invalid payload type")
// ErrInvalidPayload reports that a callback payload could not be decoded under the
// expected encoding (e.g. the compact format separator is missing).
var ErrInvalidPayload = errors.New("invalid payload")
func (bot *Bot[T]) handle(parentCtx context.Context, u *tgapi.Update) {
defer func() {
if r := recover(); r != nil {
bot.logger.Errorln(fmt.Sprintf("panic in handle: %v", r))
if bot.logger != nil {
bot.logger.Errorln(fmt.Sprintf("panic in handle: %v", r))
}
err, ok := r.(error)
if !ok {
err = fmt.Errorf("%v", r)
}
bot.safeEmitEvent(parentCtx, ErrorEvent{
UpdateID: u.UpdateID,
UpdateType: u.Type,
Err: err,
UserFacing: false,
})
}
}()
startTime := time.Now()
@@ -25,7 +43,7 @@ func (bot *Bot[T]) handle(parentCtx context.Context, u *tgapi.Update) {
ctx, cancel := context.WithCancel(parentCtx)
defer cancel()
msgCtx := &MsgContext{
msgCtx := &MessageContext{
Update: *u, API: bot.api,
Logger: bot.logger,
errorTemplate: bot.errorTemplate,
@@ -34,6 +52,7 @@ func (bot *Bot[T]) handle(parentCtx context.Context, u *tgapi.Update) {
sceneRuntime: bot,
observer: bot.observer,
payloadType: bot.payloadType,
botID: bot.userID,
ctx: ctx,
}
bot.prepareUpdateCtx(u, msgCtx)
@@ -46,6 +65,14 @@ func (bot *Bot[T]) handle(parentCtx context.Context, u *tgapi.Update) {
for _, middleware := range bot.middlewares {
if !middleware.Execute(msgCtx, bot.appData) {
bot.safeEmitEvent(ctx, UpdateHandledEvent{
UpdateID: u.UpdateID,
UpdateType: u.Type,
FromID: msgCtx.FromID,
ChatID: msgCtx.ChatID,
Duration: time.Since(startTime),
Handled: false,
})
return
}
}
@@ -105,7 +132,7 @@ func (bot *Bot[T]) handle(parentCtx context.Context, u *tgapi.Update) {
})
}
func cloneMsgContext(src *MsgContext) *MsgContext {
func cloneMsgContext(src *MessageContext) *MessageContext {
cloned := *src
if src.Args != nil {
cloned.Args = append([]string(nil), src.Args...)
@@ -145,36 +172,149 @@ func decodeBase64Payload(s string) (CallbackData, error) {
return decodeJSONPayload(string(b))
}
func decodePayload(payloadType BotPayloadType, s string, strict bool) (CallbackData, BotPayloadType, error) {
// Compact payload format: cmd|arg1,arg2,...
// Bytes \, |, and , inside a part are escaped with a leading backslash so the
// payload round-trips without ambiguity. Encoding/decoding operate byte-wise
// because all separators are single-byte ASCII; multi-byte UTF-8 code points
// pass through unchanged.
func encodeCompactPart(s string) string {
if !strings.ContainsAny(s, `\|,`) {
return s
}
var b strings.Builder
b.Grow(len(s) + 2)
for i := 0; i < len(s); i++ {
switch s[i] {
case '\\', '|', ',':
b.WriteByte('\\')
}
b.WriteByte(s[i])
}
return b.String()
}
func decodeCompactPart(s string) string {
if !strings.Contains(s, `\`) {
return s
}
var b strings.Builder
b.Grow(len(s))
for i := 0; i < len(s); i++ {
if s[i] == '\\' && i+1 < len(s) {
b.WriteByte(s[i+1])
i++
continue
}
b.WriteByte(s[i])
}
return b.String()
}
func encodeCompactPayload(d CallbackData) (string, error) {
var b strings.Builder
b.WriteString(encodeCompactPart(d.Command))
b.WriteByte('|')
for i, a := range d.Args {
if i > 0 {
b.WriteByte(',')
}
b.WriteString(encodeCompactPart(a))
}
return b.String(), nil
}
func decodeCompactPayload(s string) (CallbackData, error) {
sepIdx := -1
for i := 0; i < len(s); i++ {
if s[i] == '\\' && i+1 < len(s) {
i++
continue
}
if s[i] == '|' {
sepIdx = i
break
}
}
if sepIdx == -1 {
return CallbackData{}, ErrInvalidPayload
}
cmd := decodeCompactPart(s[:sepIdx])
argsRaw := s[sepIdx+1:]
if argsRaw == "" {
return CallbackData{Command: cmd}, nil
}
var args []string
start := 0
for i := 0; i < len(argsRaw); i++ {
if argsRaw[i] == '\\' && i+1 < len(argsRaw) {
i++
continue
}
if argsRaw[i] == ',' {
args = append(args, decodeCompactPart(argsRaw[start:i]))
start = i + 1
}
}
args = append(args, decodeCompactPart(argsRaw[start:]))
return CallbackData{Command: cmd, Args: args}, nil
}
func encodeCompactBase64Payload(d CallbackData) (string, error) {
payload, _ := encodeCompactPayload(d)
return base64.RawURLEncoding.EncodeToString([]byte(payload)), nil
}
func decodeCompactBase64Payload(s string) (CallbackData, error) {
b, err := base64.RawURLEncoding.DecodeString(s)
if err != nil {
return CallbackData{}, err
}
return decodeCompactPayload(string(b))
}
func decodePayloadAs(payloadType BotPayloadType, s string) (CallbackData, error) {
switch payloadType {
case BotPayloadBase64:
data, err := decodeBase64Payload(s)
if err == nil {
return data, BotPayloadBase64, nil
}
if strict {
return CallbackData{}, "", fmt.Errorf("%w: expected %s", ErrPayloadTypeMismatch, BotPayloadBase64)
}
data, err = decodeJSONPayload(s)
if err != nil {
return CallbackData{}, "", err
}
return data, BotPayloadJSON, nil
return decodeBase64Payload(s)
case BotPayloadJSON:
data, err := decodeJSONPayload(s)
if err == nil {
return data, BotPayloadJSON, nil
}
if strict {
return CallbackData{}, "", fmt.Errorf("%w: expected %s", ErrPayloadTypeMismatch, BotPayloadJSON)
}
data, err = decodeBase64Payload(s)
if err != nil {
return CallbackData{}, "", err
}
return data, BotPayloadBase64, nil
return decodeJSONPayload(s)
case BotPayloadCompact:
return decodeCompactPayload(s)
case BotPayloadCompactBase64:
return decodeCompactBase64Payload(s)
}
return CallbackData{}, "", ErrInvalidPayloadType
return CallbackData{}, ErrInvalidPayloadType
}
func decodePayload(payloadType BotPayloadType, s string, strict bool) (CallbackData, BotPayloadType, error) {
knownTypes := []BotPayloadType{
BotPayloadBase64,
BotPayloadJSON,
BotPayloadCompact,
BotPayloadCompactBase64,
}
if _, err := decodePayloadAs(payloadType, ""); errors.Is(err, ErrInvalidPayloadType) {
return CallbackData{}, "", ErrInvalidPayloadType
}
data, err := decodePayloadAs(payloadType, s)
if err == nil {
return data, payloadType, nil
}
if strict {
return CallbackData{}, "", fmt.Errorf("%w: expected %s", ErrPayloadTypeMismatch, payloadType)
}
for _, candidate := range knownTypes {
if candidate == payloadType {
continue
}
data, err = decodePayloadAs(candidate, s)
if err == nil {
return data, candidate, nil
}
}
return CallbackData{}, "", err
}
func (bot *Bot[T]) decodePayload(s string) (CallbackData, error) {
+235 -39
View File
@@ -10,6 +10,7 @@ import (
)
type recordingObserver struct {
received []UpdateReceivedEvent
started []HandlerStartedEvent
finished []HandlerFinishedEvent
errors []ErrorEvent
@@ -19,8 +20,10 @@ type recordingObserver struct {
retries []PollingRetryEvent
}
func (*recordingObserver) OnReceiveUpdate(context.Context, UpdateReceivedEvent) {}
func (o *recordingObserver) OnHandledUpdate(_ context.Context, ev UpdateHandledEvent) {
func (o *recordingObserver) OnUpdateReceived(_ context.Context, ev UpdateReceivedEvent) {
o.received = append(o.received, ev)
}
func (o *recordingObserver) OnUpdateHandled(_ context.Context, ev UpdateHandledEvent) {
o.handled = append(o.handled, ev)
}
func (o *recordingObserver) OnHandlerStarted(_ context.Context, ev HandlerStartedEvent) {
@@ -61,7 +64,7 @@ func TestBotMiddlewareReceivesLogger(t *testing.T) {
bot := &Bot[NoData]{
logger: logger,
middlewares: []Middleware[NoData]{
NewMiddleware("logger-check", func(ctx *MsgContext, db NoData) bool {
NewMiddleware("logger-check", func(ctx *MessageContext, db NoData) bool {
called = true
if ctx.Logger != logger {
t.Fatalf("expected bot logger in middleware context, got %#v", ctx.Logger)
@@ -87,7 +90,7 @@ func TestBotMiddlewareReceivesLogger(t *testing.T) {
func TestAddUpdateHandlerRejectsReservedUpdateTypes(t *testing.T) {
plugin := NewPlugin[NoData]("test")
handler := func(ctx *MsgContext, db NoData) error { return nil }
handler := func(ctx *MessageContext, db NoData) error { return nil }
for _, updateType := range []tgapi.UpdateType{
tgapi.UpdateTypeMessage,
@@ -373,7 +376,7 @@ func TestPrepareUpdateCtxContract(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
bot := &Bot[NoData]{}
ctx := &MsgContext{}
ctx := &MessageContext{}
bot.prepareUpdateCtx(tt.update, ctx)
if got := ctx.Msg != nil; got != tt.wantMsg {
@@ -447,7 +450,7 @@ func TestHandleUpdateHandlersPopulateFromContext(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
called := false
plugin := NewPlugin[NoData]("test").AddUpdateHandler(tt.update.Type, func(ctx *MsgContext, db NoData) error {
plugin := NewPlugin[NoData]("test").AddUpdateHandler(tt.update.Type, func(ctx *MessageContext, db NoData) error {
called = true
if ctx.Update.UpdateID != tt.update.UpdateID {
t.Fatalf("unexpected update in context: got %d want %d", ctx.Update.UpdateID, tt.update.UpdateID)
@@ -485,7 +488,7 @@ func TestHandleUpdateHandlersReceiveIsolatedContexts(t *testing.T) {
firstCalled := false
secondCalled := false
first := NewPlugin[NoData]("first").AddUpdateHandler(tgapi.UpdateTypeInlineQuery, func(ctx *MsgContext, db NoData) error {
first := NewPlugin[NoData]("first").AddUpdateHandler(tgapi.UpdateTypeInlineQuery, func(ctx *MessageContext, db NoData) error {
firstCalled = true
if ctx.FromID != 41 {
t.Fatalf("unexpected FromID in first handler: got %d want 41", ctx.FromID)
@@ -496,7 +499,7 @@ func TestHandleUpdateHandlersReceiveIsolatedContexts(t *testing.T) {
ctx.Args = []string{"mutated"}
return nil
})
second := NewPlugin[NoData]("second").AddUpdateHandler(tgapi.UpdateTypeInlineQuery, func(ctx *MsgContext, db NoData) error {
second := NewPlugin[NoData]("second").AddUpdateHandler(tgapi.UpdateTypeInlineQuery, func(ctx *MessageContext, db NoData) error {
secondCalled = true
if ctx.From == nil {
t.Fatal("expected ctx.From to remain populated for second handler")
@@ -538,7 +541,7 @@ func TestHandleUpdateHandlersReceiveIsolatedContexts(t *testing.T) {
func TestHandleUpdateObserverEmitsUpdateErrors(t *testing.T) {
observer := &recordingObserver{}
plugin := NewPlugin[NoData]("test").AddUpdateHandler(tgapi.UpdateTypeInlineQuery, func(ctx *MsgContext, db NoData) error {
plugin := NewPlugin[NoData]("test").AddUpdateHandler(tgapi.UpdateTypeInlineQuery, func(ctx *MessageContext, db NoData) error {
return AsUserError(errors.New("update failed"))
})
@@ -587,11 +590,49 @@ func TestHandleUpdateObserverEmitsUpdateErrors(t *testing.T) {
}
}
func TestHandleObserverCompletesUpdateWhenBotMiddlewareBlocks(t *testing.T) {
observer := &recordingObserver{}
bot := &Bot[NoData]{
logger: sneklog.NewLogger(),
observer: observer,
middlewares: []Middleware[NoData]{
NewMiddleware("block", func(ctx *MessageContext, db NoData) bool {
return false
}),
},
}
bot.handle(context.Background(), &tgapi.Update{
UpdateID: 8,
Type: tgapi.UpdateTypeMessage,
Message: &tgapi.Message{
MessageID: 1,
Date: 1,
Chat: &tgapi.Chat{ID: 42, Type: tgapi.ChatTypePrivate},
Text: "/start",
},
})
if len(observer.received) != 1 {
t.Fatalf("expected one received event, got %d", len(observer.received))
}
if len(observer.handled) != 1 {
t.Fatalf("expected one handled event, got %d", len(observer.handled))
}
got := observer.handled[0]
if got.UpdateID != 8 || got.UpdateType != tgapi.UpdateTypeMessage || got.ChatID != 42 || got.Handled {
t.Fatalf("unexpected handled event: %#v", got)
}
if len(observer.started) != 0 || len(observer.finished) != 0 || len(observer.errors) != 0 {
t.Fatalf("middleware block should not emit handler lifecycle or errors: started=%d finished=%d errors=%d", len(observer.started), len(observer.finished), len(observer.errors))
}
}
func TestHandleMessageFallbackRunsAfterCommandMiss(t *testing.T) {
observer := &recordingObserver{}
called := false
plugin := NewPlugin[NoData]("test")
plugin.SetMessageFallback(func(ctx *MsgContext, db NoData) error {
plugin.SetMessageFallback(func(ctx *MessageContext, db NoData) error {
called = true
if ctx.Text != "/missing hello world" {
t.Fatalf("unexpected fallback text: got %q", ctx.Text)
@@ -646,7 +687,7 @@ func TestHandleMessageFallbackRunsAfterCommandMiss(t *testing.T) {
func TestHandleMessageFallbackRunsForPlainText(t *testing.T) {
called := false
plugin := NewPlugin[NoData]("test").SetMessageFallback(func(ctx *MsgContext, db NoData) error {
plugin := NewPlugin[NoData]("test").SetMessageFallback(func(ctx *MessageContext, db NoData) error {
called = true
if ctx.Text != "hello fallback" {
t.Fatalf("unexpected fallback text: got %q", ctx.Text)
@@ -682,10 +723,10 @@ func TestHandleMessageFallbackRunsForPlainText(t *testing.T) {
func TestHandleMessageFallbackRespectsMiddleware(t *testing.T) {
called := false
plugin := NewPlugin[NoData]("test")
plugin.AddMiddleware(NewMiddleware("block", func(ctx *MsgContext, db NoData) bool {
plugin.AddMiddleware(NewMiddleware("block", func(ctx *MessageContext, db NoData) bool {
return false
}))
plugin.SetMessageFallback(func(ctx *MsgContext, db NoData) error {
plugin.SetMessageFallback(func(ctx *MessageContext, db NoData) error {
called = true
return nil
})
@@ -716,11 +757,11 @@ func TestHandleMessageFallbackDoesNotRunWhenCommandMatches(t *testing.T) {
commandCalled := false
fallbackCalled := false
plugin := NewPlugin[NoData]("test")
plugin.NewCommand(func(ctx *MsgContext, db NoData) error {
plugin.Command("start", func(ctx *MessageContext, db NoData) error {
commandCalled = true
return nil
}, "start")
plugin.SetMessageFallback(func(ctx *MsgContext, db NoData) error {
})
plugin.SetMessageFallback(func(ctx *MessageContext, db NoData) error {
fallbackCalled = true
return nil
})
@@ -753,7 +794,7 @@ func TestHandleMessageFallbackDoesNotRunWhenCommandMatches(t *testing.T) {
func TestHandleChannelPostCommandWithSenderChat(t *testing.T) {
called := false
plugin := NewPlugin[NoData]("test")
plugin.NewCommand(func(ctx *MsgContext, db NoData) error {
plugin.Command("ping", func(ctx *MessageContext, db NoData) error {
called = true
if ctx.Msg == nil {
t.Fatal("expected message context")
@@ -768,7 +809,7 @@ func TestHandleChannelPostCommandWithSenderChat(t *testing.T) {
t.Fatalf("expected zero FromID for sender_chat updates, got %d", ctx.FromID)
}
return nil
}, "ping")
})
bot := &Bot[NoData]{
logger: sneklog.NewLogger(),
@@ -800,10 +841,10 @@ func TestCommandHandlerBindArgsEndToEnd(t *testing.T) {
var got banInput
plugin := NewPlugin[NoData]("test")
plugin.NewCommand(func(ctx *MsgContext, db NoData) error {
plugin.Command("ban", func(ctx *MessageContext, db NoData) error {
return ctx.BindArgs(&got)
}, "ban",
NewCommandArg("user_id").SetValueType(CommandValueIntType).SetRequired(),
},
NewCommandArg("user_id").SetValueType(CommandValueInt).SetRequired(),
NewCommandArg("reason").SetRequired(),
)
@@ -837,10 +878,10 @@ func TestPayloadHandlerBindArgsEndToEnd(t *testing.T) {
var got payloadInput
plugin := NewPlugin[NoData]("test")
plugin.NewPayload(func(ctx *MsgContext, db NoData) error {
plugin.Payload("approve", func(ctx *MessageContext, db NoData) error {
return ctx.BindArgs(&got)
}, "approve",
NewCommandArg("id").SetValueType(CommandValueIntType).SetRequired(),
},
NewCommandArg("id").SetValueType(CommandValueInt).SetRequired(),
NewCommandArg("note").SetRequired(),
)
@@ -879,11 +920,11 @@ func TestHandleEditedMessageStaysOutOfCommandFlow(t *testing.T) {
updateCalled := false
plugin := NewPlugin[NoData]("test")
plugin.NewCommand(func(ctx *MsgContext, db NoData) error {
plugin.Command("ping", func(ctx *MessageContext, db NoData) error {
commandCalled = true
return nil
}, "ping")
plugin.AddUpdateHandler(tgapi.UpdateTypeEditedMessage, func(ctx *MsgContext, db NoData) error {
})
plugin.AddUpdateHandler(tgapi.UpdateTypeEditedMessage, func(ctx *MessageContext, db NoData) error {
updateCalled = true
if ctx.Msg == nil {
t.Fatal("expected ctx.Msg in edited message handler")
@@ -927,11 +968,11 @@ func TestHandleEditedChannelPostStaysOutOfCommandFlow(t *testing.T) {
updateCalled := false
plugin := NewPlugin[NoData]("test")
plugin.NewCommand(func(ctx *MsgContext, db NoData) error {
plugin.Command("ping", func(ctx *MessageContext, db NoData) error {
commandCalled = true
return nil
}, "ping")
plugin.AddUpdateHandler(tgapi.UpdateTypeEditedChannelPost, func(ctx *MsgContext, db NoData) error {
})
plugin.AddUpdateHandler(tgapi.UpdateTypeEditedChannelPost, func(ctx *MessageContext, db NoData) error {
updateCalled = true
if ctx.Msg == nil {
t.Fatal("expected ctx.Msg in edited channel post handler")
@@ -966,7 +1007,7 @@ func TestHandleEditedChannelPostStaysOutOfCommandFlow(t *testing.T) {
func TestHandleCallbackPopulatesMessageTargets(t *testing.T) {
called := false
plugin := NewPlugin[NoData]("test")
plugin.NewPayload(func(ctx *MsgContext, db NoData) error {
plugin.Payload("approve", func(ctx *MessageContext, db NoData) error {
called = true
if ctx.CallbackQueryID != "cb-msg" {
t.Fatalf("unexpected CallbackQueryID: %q", ctx.CallbackQueryID)
@@ -990,7 +1031,7 @@ func TestHandleCallbackPopulatesMessageTargets(t *testing.T) {
t.Fatalf("unexpected callback args: got %v want %v", got, want)
}
return nil
}, "approve")
})
bot := &Bot[NoData]{
logger: sneklog.NewLogger(),
@@ -1025,7 +1066,7 @@ func TestHandleCallbackPopulatesMessageTargets(t *testing.T) {
func TestHandleCallbackPopulatesInlineTargets(t *testing.T) {
called := false
plugin := NewPlugin[NoData]("test")
plugin.NewPayload(func(ctx *MsgContext, db NoData) error {
plugin.Payload("inline.approve", func(ctx *MessageContext, db NoData) error {
called = true
if ctx.CallbackQueryID != "cb-inline" {
t.Fatalf("unexpected CallbackQueryID: %q", ctx.CallbackQueryID)
@@ -1049,7 +1090,7 @@ func TestHandleCallbackPopulatesInlineTargets(t *testing.T) {
t.Fatalf("unexpected callback args: got %v want %v", got, want)
}
return nil
}, "inline.approve")
})
bot := &Bot[NoData]{
logger: sneklog.NewLogger(),
@@ -1081,9 +1122,9 @@ func TestHandleCallbackPopulatesInlineTargets(t *testing.T) {
func TestHandleCallbackObserverEmitsPayloadEvents(t *testing.T) {
observer := &recordingObserver{}
plugin := NewPlugin[NoData]("test")
plugin.NewPayload(func(ctx *MsgContext, db NoData) error {
plugin.Payload("approve", func(ctx *MessageContext, db NoData) error {
return nil
}, "approve")
})
bot := &Bot[NoData]{
logger: sneklog.NewLogger(),
@@ -1132,9 +1173,9 @@ func TestHandleCallbackObserverEmitsPayloadErrors(t *testing.T) {
observer := &recordingObserver{}
plugin := NewPlugin[NoData]("test")
wantErr := AsInternalError(errors.New("boom"))
plugin.NewPayload(func(ctx *MsgContext, db NoData) error {
plugin.Payload("approve", func(ctx *MessageContext, db NoData) error {
return wantErr
}, "approve")
})
bot := &Bot[NoData]{
logger: sneklog.NewLogger(),
@@ -1179,6 +1220,161 @@ func TestHandleCallbackObserverEmitsPayloadErrors(t *testing.T) {
}
}
func TestParseCommandTable(t *testing.T) {
bot := &Bot[NoData]{prefixes: []string{"/", "!"}}
tests := []struct {
name string
text string
wantPrefix string
wantCmd string
wantArgs string
}{
{name: "plain text", text: "hello", wantPrefix: "", wantCmd: "", wantArgs: ""},
{name: "command no args", text: "/start", wantPrefix: "/", wantCmd: "start", wantArgs: ""},
{name: "command with args", text: "/ban 42 reason", wantPrefix: "/", wantCmd: "ban", wantArgs: "42 reason"},
{name: "alternate prefix", text: "!ping", wantPrefix: "!", wantCmd: "ping", wantArgs: ""},
{name: "leading space after prefix", text: "/ start now", wantPrefix: "/", wantCmd: "start", wantArgs: "now"},
{name: "command with botname", text: "/start@mybot extra", wantPrefix: "/", wantCmd: "start@mybot", wantArgs: "extra"},
{name: "trailing whitespace", text: "/start ", wantPrefix: "/", wantCmd: "start", wantArgs: ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
prefix, cmd, args := bot.parseCommand(tt.text)
if prefix != tt.wantPrefix {
t.Fatalf("unexpected prefix: got %q want %q", prefix, tt.wantPrefix)
}
if cmd != tt.wantCmd {
t.Fatalf("unexpected cmd: got %q want %q", cmd, tt.wantCmd)
}
if args != tt.wantArgs {
t.Fatalf("unexpected args: got %q want %q", args, tt.wantArgs)
}
})
}
}
func TestHandleMessageStripsBotUsernameSuffix(t *testing.T) {
tests := []struct {
name string
botUsername string
text string
wantCalled bool
}{
{name: "matching botname", botUsername: "mybot", text: "/start@mybot hello", wantCalled: true},
{name: "matching botname no args", botUsername: "mybot", text: "/start@mybot", wantCalled: true},
{name: "other botname", botUsername: "mybot", text: "/start@otherbot hello", wantCalled: false},
{name: "no botname", botUsername: "mybot", text: "/start hello", wantCalled: true},
{name: "bot has no username", botUsername: "", text: "/start@mybot hello", wantCalled: false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
called := false
plugin := NewPlugin[NoData]("test")
plugin.Command("start", func(ctx *MessageContext, db NoData) error {
called = true
return nil
})
bot := &Bot[NoData]{
logger: sneklog.NewLogger(),
prefixes: []string{"/"},
username: tt.botUsername,
plugins: []Plugin[NoData]{clonePlugin(plugin)},
}
bot.handle(context.Background(), &tgapi.Update{
UpdateID: 200,
Type: tgapi.UpdateTypeMessage,
Message: &tgapi.Message{
MessageID: 1,
Text: tt.text,
From: &tgapi.User{ID: 1},
Chat: &tgapi.Chat{ID: 42, Type: tgapi.ChatTypePrivate},
},
})
if called != tt.wantCalled {
t.Fatalf("unexpected handler invocation: got called=%v want %v", called, tt.wantCalled)
}
})
}
}
func TestHandlePanicEmitsErrorEvent(t *testing.T) {
tests := []struct {
name string
panicWith any
matchErr func(error) bool
}{
{
name: "error value",
panicWith: errors.New("boom"),
matchErr: func(err error) bool {
return err != nil && err.Error() == "boom"
},
},
{
name: "string value",
panicWith: "kaboom",
matchErr: func(err error) bool {
return err != nil && err.Error() == "kaboom"
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
observer := &recordingObserver{}
plugin := NewPlugin[NoData]("test")
plugin.Command("boom", func(ctx *MessageContext, db NoData) error {
panic(tt.panicWith)
})
bot := &Bot[NoData]{
logger: sneklog.NewLogger(),
prefixes: []string{"/"},
plugins: []Plugin[NoData]{clonePlugin(plugin)},
observer: observer,
}
bot.handle(context.Background(), &tgapi.Update{
UpdateID: 100,
Type: tgapi.UpdateTypeMessage,
Message: &tgapi.Message{
MessageID: 1,
Text: "/boom",
From: &tgapi.User{ID: 1},
Chat: &tgapi.Chat{ID: 42, Type: tgapi.ChatTypePrivate},
},
})
panicEvent := (*ErrorEvent)(nil)
for i := range observer.errors {
ev := observer.errors[i]
if ev.Plugin == "" && ev.HandlerKind == "" && ev.UpdateID == 100 {
panicEvent = &ev
break
}
}
if panicEvent == nil {
t.Fatalf("expected ErrorEvent from panic recovery, got events: %#v", observer.errors)
}
if panicEvent.UpdateType != tgapi.UpdateTypeMessage {
t.Fatalf("unexpected UpdateType: %q", panicEvent.UpdateType)
}
if panicEvent.UserFacing {
t.Fatal("panic ErrorEvent must not be marked user-facing")
}
if !tt.matchErr(panicEvent.Err) {
t.Fatalf("unexpected panic Err: %v", panicEvent.Err)
}
})
}
}
func TestHandleCallbackObserverEmitsDecodeErrors(t *testing.T) {
observer := &recordingObserver{}
bot := &Bot[NoData]{
@@ -1195,7 +1391,7 @@ func TestHandleCallbackObserverEmitsDecodeErrors(t *testing.T) {
Data: "{not-json",
From: tgapi.User{ID: 7},
},
}, &MsgContext{
}, &MessageContext{
Update: tgapi.Update{
UpdateID: 34,
Type: tgapi.UpdateTypeCallbackQuery,
+117 -39
View File
@@ -16,9 +16,9 @@ const (
ButtonStylePrimary tgapi.KeyboardButtonStyle = "primary"
)
// InlineKbButtonBuilder is a fluent builder for creating a single inline keyboard button.
// InlineKeyboardButtonBuilder is a fluent builder for creating a single inline keyboard button.
//
// Use NewInlineKbButton() to start, then chain methods to configure:
// Use NewInlineKeyboardButton() to start, then chain methods to configure:
// - SetIconCustomEmojiID() — adds a custom emoji icon
// - SetStyle() — sets visual style (danger/success/primary)
// - SetURL() — makes button open a URL
@@ -26,42 +26,51 @@ const (
//
// Call build() to produce the final tgapi.InlineKeyboardButton.
// Builder methods are immutable — each returns a copy.
type InlineKbButtonBuilder struct {
text string
iconCustomEmojiID string
style tgapi.KeyboardButtonStyle
url string
callbackData string
type InlineKeyboardButtonBuilder struct {
text string
emojiID string
style tgapi.KeyboardButtonStyle
url string
data string
payloadType BotPayloadType
}
// NewInlineKbButton creates a new button builder with the given display text.
// NewInlineKeyboardButton creates a new button builder with the given display text.
// The button will have no URL, no style, and no callback data by default.
func NewInlineKbButton(text string) InlineKbButtonBuilder {
return InlineKbButtonBuilder{text: text}
func NewInlineKeyboardButton(text string) InlineKeyboardButtonBuilder {
return InlineKeyboardButtonBuilder{text: text}
}
// SetIconCustomEmojiID sets a custom emoji ID to display as the button's icon.
// This is a Telegram Bot API feature for custom emoji icons.
func (b InlineKbButtonBuilder) SetIconCustomEmojiID(id string) InlineKbButtonBuilder {
b.iconCustomEmojiID = id
func (b InlineKeyboardButtonBuilder) SetIconCustomEmojiID(id string) InlineKeyboardButtonBuilder {
b.emojiID = id
return b
}
// SetStyle sets the visual style of the button.
// Valid values: ButtonStyleDanger, ButtonStyleSuccess, ButtonStylePrimary.
// If not set, the button uses the default style.
func (b InlineKbButtonBuilder) SetStyle(style tgapi.KeyboardButtonStyle) InlineKbButtonBuilder {
func (b InlineKeyboardButtonBuilder) SetStyle(style tgapi.KeyboardButtonStyle) InlineKeyboardButtonBuilder {
b.style = style
return b
}
// SetURL sets a URL that will be opened when the button is pressed.
// If both URL and CallbackData are set, Telegram will prioritize URL.
func (b InlineKbButtonBuilder) SetURL(url string) InlineKbButtonBuilder {
func (b InlineKeyboardButtonBuilder) SetURL(url string) InlineKeyboardButtonBuilder {
b.url = url
return b
}
// SetPayloadType sets the encoding used by SetCallbackData.
func (b InlineKeyboardButtonBuilder) SetPayloadType(t BotPayloadType) InlineKeyboardButtonBuilder {
b.payloadType = t
return b
}
// SetCallbackDataJSON sets a structured callback payload that will be sent to the bot
// when the button is pressed. The command and arguments are serialized as JSON.
//
@@ -69,27 +78,56 @@ func (b InlineKbButtonBuilder) SetURL(url string) InlineKbButtonBuilder {
// are safely serialized, but complex structs may not serialize usefully.
//
// Example: SetCallbackDataJSON("delete_user", 123, "confirm") → {"cmd":"delete_user","args":["123","confirm"]}.
func (b InlineKbButtonBuilder) SetCallbackDataJSON(cmd string, args ...any) InlineKbButtonBuilder {
b.callbackData = NewCallbackData(cmd, args...).ToJSON()
func (b InlineKeyboardButtonBuilder) SetCallbackDataJSON(cmd string, args ...any) InlineKeyboardButtonBuilder {
b.data = NewCallbackData(cmd, args...).ToJSON()
return b
}
// SetCallbackDataBase64 sets a structured callback payload encoded as Base64.
// This can be useful when the JSON payload exceeds Telegram's callback data length limit.
// Args are converted to strings using fmt.Sprint.
func (b InlineKbButtonBuilder) SetCallbackDataBase64(cmd string, args ...any) InlineKbButtonBuilder {
b.callbackData = NewCallbackData(cmd, args...).ToBase64()
func (b InlineKeyboardButtonBuilder) SetCallbackDataBase64(cmd string, args ...any) InlineKeyboardButtonBuilder {
b.data = NewCallbackData(cmd, args...).ToBase64()
return b
}
// Internal helper that converts the builder state into a Telegram button.
func (b InlineKbButtonBuilder) build() tgapi.InlineKeyboardButton {
// SetCallbackDataCompact sets a structured callback payload encoded as compact text.
func (b InlineKeyboardButtonBuilder) SetCallbackDataCompact(cmd string, args ...any) InlineKeyboardButtonBuilder {
b.data = NewCallbackData(cmd, args...).ToCompact()
return b
}
// SetCallbackDataCompactBase64 sets a compact callback payload encoded as Base64.
func (b InlineKeyboardButtonBuilder) SetCallbackDataCompactBase64(cmd string, args ...any) InlineKeyboardButtonBuilder {
b.data = NewCallbackData(cmd, args...).ToCompactBase64()
return b
}
// SetCallbackData sets a structured callback payload using the configured payload type.
// The default payload type is JSON.
func (b InlineKeyboardButtonBuilder) SetCallbackData(cmd string, args ...any) InlineKeyboardButtonBuilder {
switch b.payloadType {
case BotPayloadJSON:
b.data = NewCallbackData(cmd, args...).ToJSON()
case BotPayloadBase64:
b.data = NewCallbackData(cmd, args...).ToBase64()
case BotPayloadCompact:
b.data = NewCallbackData(cmd, args...).ToCompact()
case BotPayloadCompactBase64:
b.data = NewCallbackData(cmd, args...).ToCompactBase64()
default:
b.data = NewCallbackData(cmd, args...).ToJSON()
}
return b
}
func (b InlineKeyboardButtonBuilder) build() tgapi.InlineKeyboardButton {
return tgapi.InlineKeyboardButton{
Text: b.text,
URL: b.url,
Style: b.style,
IconCustomEmojiID: b.iconCustomEmojiID,
CallbackData: b.callbackData,
IconCustomEmojiID: b.emojiID,
CallbackData: b.data,
}
}
@@ -123,6 +161,16 @@ func NewInlineKeyboardBase64(maxRow int) *InlineKeyboard {
return NewInlineKeyboard(BotPayloadBase64, maxRow)
}
// NewInlineKeyboardCompact creates a keyboard builder using compact callback payloads.
func NewInlineKeyboardCompact(maxRow int) *InlineKeyboard {
return NewInlineKeyboard(BotPayloadCompact, maxRow)
}
// NewInlineKeyboardCompactBase64 creates a keyboard builder using Base64-encoded compact payloads.
func NewInlineKeyboardCompactBase64(maxRow int) *InlineKeyboard {
return NewInlineKeyboard(BotPayloadCompactBase64, maxRow)
}
// NewInlineKeyboard creates a new keyboard builder with the specified payload encoding
// type and maximum number of buttons per row.
//
@@ -154,7 +202,9 @@ func (in *InlineKeyboard) SetMaxRow(maxRow int) *InlineKeyboard {
return in
}
// Internal helper that appends a button and auto-flushes a full row.
// GetMaxRow returns the maximum number of buttons per row.
func (in *InlineKeyboard) GetMaxRow() int { return in.maxRow }
func (in *InlineKeyboard) append(button tgapi.InlineKeyboardButton) *InlineKeyboard {
if in.CurrentLine.Len() == in.maxRow {
in.AddLine()
@@ -177,7 +227,7 @@ func (in *InlineKeyboard) AddURLButtonStyle(text string, style tgapi.KeyboardBut
// AddCallbackButton adds a button that sends a structured callback payload to the bot.
// The command and args are serialized according to the current payloadType.
func (in *InlineKeyboard) AddCallbackButton(text string, cmd string, args ...any) *InlineKeyboard {
func (in *InlineKeyboard) AddCallbackButton(text, cmd string, args ...any) *InlineKeyboard {
return in.append(tgapi.InlineKeyboardButton{
Text: text,
CallbackData: NewCallbackData(cmd, args...).Encode(in.payloadType),
@@ -194,9 +244,9 @@ func (in *InlineKeyboard) AddCallbackButtonStyle(text string, style tgapi.Keyboa
})
}
// AddButton adds a button pre-configured via InlineKbButtonBuilder.
// AddButton adds a button pre-configured via InlineKeyboardButtonBuilder.
// This is the most flexible way to create buttons with custom emoji, style, URL, and callback.
func (in *InlineKeyboard) AddButton(b InlineKbButtonBuilder) *InlineKeyboard {
func (in *InlineKeyboard) AddButton(b InlineKeyboardButtonBuilder) *InlineKeyboard {
return in.append(b.build())
}
@@ -253,18 +303,18 @@ func NewCallbackData(command string, args ...any) CallbackData {
}
}
// All To* encoders return an empty string when serialization fails. Telegram
// rejects empty callback_data, so an empty result surfaces a real bug rather
// than masking it with a stub payload that silently routes to no handler.
// Build CallbackData from primitives (string, []string) only — the encoders
// have no failure modes for that input.
// ToJSON serializes the CallbackData to a JSON string.
//
// If serialization fails (e.g., due to unmarshalable fields), returns a fallback
// JSON object: {"cmd":""} to prevent breaking Telegram's API.
//
// This fallback ensures the bot receives a valid JSON payload even if internal
// errors occur — avoiding "invalid callback_data" errors from Telegram.
// Returns an empty string if serialization fails.
func (d CallbackData) ToJSON() string {
data, err := encodeJSONPayload(d)
if err != nil {
// Fallback: return minimal valid JSON to avoid Telegram API rejection
return `{"cmd":""}`
return ""
}
return data
}
@@ -272,15 +322,39 @@ func (d CallbackData) ToJSON() string {
// ToBase64 serializes the CallbackData to a JSON string and then encodes it as Base64.
// Returns an empty string if serialization or encoding fails.
func (d CallbackData) ToBase64() string {
s, err := encodeBase64Payload(d)
data, err := encodeBase64Payload(d)
if err != nil {
return ``
return ""
}
return s
return data
}
// ToCompact serializes the CallbackData to a compact delimited string.
// Returns an empty string if serialization fails.
//
// The compact format coalesces "no args" with "single empty arg" — both
// produce "cmd|" and decode back to nil args. Use ToJSON or ToBase64 when
// that distinction must be preserved.
func (d CallbackData) ToCompact() string {
data, err := encodeCompactPayload(d)
if err != nil {
return ""
}
return data
}
// ToCompactBase64 serializes the CallbackData to compact text and then encodes it as Base64.
// Returns an empty string if serialization or encoding fails.
func (d CallbackData) ToCompactBase64() string {
data, err := encodeCompactBase64Payload(d)
if err != nil {
return ""
}
return data
}
// Encode serializes the CallbackData according to the specified payload type.
// Supported types: BotPayloadJSON and BotPayloadBase64.
// Supported types: BotPayloadJSON, BotPayloadBase64, BotPayloadCompact, and BotPayloadCompactBase64.
// For unknown types, returns an empty string.
func (d CallbackData) Encode(t BotPayloadType) string {
switch t {
@@ -288,6 +362,10 @@ func (d CallbackData) Encode(t BotPayloadType) string {
return d.ToBase64()
case BotPayloadJSON:
return d.ToJSON()
case BotPayloadCompact:
return d.ToCompact()
case BotPayloadCompactBase64:
return d.ToCompactBase64()
}
return ""
}
+129 -1
View File
@@ -31,7 +31,7 @@ func TestInlineKeyboardWrapsRowsAndEncodesJSONPayloads(t *testing.T) {
func TestInlineKeyboardBuilderPreservesConfiguredButtonFields(t *testing.T) {
kb := NewInlineKeyboardBase64(3).
AddButton(
NewInlineKbButton("Docs").
NewInlineKeyboardButton("Docs").
SetStyle(ButtonStylePrimary).
SetURL("https://example.test"),
)
@@ -45,6 +45,34 @@ func TestInlineKeyboardBuilderPreservesConfiguredButtonFields(t *testing.T) {
}
}
func TestInlineKeyboardButtonBuilderSetCallbackDataDefaultsToJSON(t *testing.T) {
kb := NewInlineKeyboardBase64(1).
AddButton(NewInlineKeyboardButton("A").SetCallbackData("cmd", 1, "two"))
button := kb.Get().InlineKeyboard[0][0]
if !strings.Contains(button.CallbackData, `"cmd":"cmd"`) {
t.Fatalf("expected JSON callback payload, got %q", button.CallbackData)
}
}
func TestInlineKeyboardButtonBuilderSetCallbackDataUsesConfiguredPayloadType(t *testing.T) {
kb := NewInlineKeyboardJSON(1).
AddButton(NewInlineKeyboardButton("A").
SetPayloadType(BotPayloadBase64).
SetCallbackData("cmd", 1, "two"),
)
got, _, err := decodePayload(BotPayloadJSON, kb.Get().InlineKeyboard[0][0].CallbackData, false)
if err != nil {
t.Fatalf("decodePayload returned error: %v", err)
}
want := CallbackData{Command: "cmd", Args: []string{"1", "two"}}
if !reflect.DeepEqual(got, want) {
t.Fatalf("unexpected payload: got %#v want %#v", got, want)
}
}
func TestInlineKeyboardGetPayloadTypeReturnsLocalOverride(t *testing.T) {
kb := NewInlineKeyboardJSON(2)
if got := kb.GetPayloadType(); got != BotPayloadJSON {
@@ -86,6 +114,106 @@ func TestDecodePayloadAcceptsJSONKeyboardPayloadWhenBotPrefersBase64(t *testing.
}
}
func TestDecodePayloadAcceptsCompactKeyboardPayloadWhenBotPrefersJSON(t *testing.T) {
kb := NewInlineKeyboardCompact(1).
AddCallbackButton("A", "cmd", 1, "two")
got, decodedType, err := decodePayload(BotPayloadJSON, kb.Get().InlineKeyboard[0][0].CallbackData, false)
if err != nil {
t.Fatalf("decodePayload returned error: %v", err)
}
if decodedType != BotPayloadCompact {
t.Fatalf("unexpected decoded payload type: got %q want %q", decodedType, BotPayloadCompact)
}
want := CallbackData{Command: "cmd", Args: []string{"1", "two"}}
if !reflect.DeepEqual(got, want) {
t.Fatalf("unexpected payload: got %#v want %#v", got, want)
}
}
func TestDecodePayloadAcceptsCompactBase64KeyboardPayloadWhenBotPrefersJSON(t *testing.T) {
kb := NewInlineKeyboardCompactBase64(1).
AddCallbackButton("A", "cmd", 1, "two")
got, decodedType, err := decodePayload(BotPayloadJSON, kb.Get().InlineKeyboard[0][0].CallbackData, false)
if err != nil {
t.Fatalf("decodePayload returned error: %v", err)
}
if decodedType != BotPayloadCompactBase64 {
t.Fatalf("unexpected decoded payload type: got %q want %q", decodedType, BotPayloadCompactBase64)
}
want := CallbackData{Command: "cmd", Args: []string{"1", "two"}}
if !reflect.DeepEqual(got, want) {
t.Fatalf("unexpected payload: got %#v want %#v", got, want)
}
}
// TestCompactPayloadRoundTripsWithSeparatorChars guards the compact-encoding
// escape fix. Args containing the , | or \ separator bytes previously corrupted
// on decode; now they must round-trip exactly.
//
// Note: the compact format coalesces "no args" with "single empty arg" — both
// emit "cmd|" and decode to nil args. Use other encodings if that distinction
// matters.
func TestCompactPayloadRoundTripsWithSeparatorChars(t *testing.T) {
tests := []struct {
name string
data CallbackData
}{
{name: "plain", data: CallbackData{Command: "cmd", Args: []string{"one", "two"}}},
{name: "no args", data: CallbackData{Command: "cmd"}},
{name: "comma in arg", data: CallbackData{Command: "cmd", Args: []string{"a,b", "c"}}},
{name: "pipe in arg", data: CallbackData{Command: "cmd", Args: []string{"a|b", "c"}}},
{name: "backslash in arg", data: CallbackData{Command: "cmd", Args: []string{`a\b`, "c"}}},
{name: "all specials in arg", data: CallbackData{Command: "cmd", Args: []string{`a,b|c\d`}}},
{name: "specials in command", data: CallbackData{Command: "a|b,c", Args: []string{"x"}}},
{name: "two empty args", data: CallbackData{Command: "cmd", Args: []string{"", ""}}},
{name: "utf8 args", data: CallbackData{Command: "cmd", Args: []string{"привет", "мир"}}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
encoded, err := encodeCompactPayload(tt.data)
if err != nil {
t.Fatalf("encodeCompactPayload returned error: %v", err)
}
got, err := decodeCompactPayload(encoded)
if err != nil {
t.Fatalf("decodeCompactPayload returned error: %v", err)
}
if got.Command != tt.data.Command {
t.Fatalf("command mismatch: got %q want %q (encoded=%q)", got.Command, tt.data.Command, encoded)
}
if len(got.Args) != len(tt.data.Args) {
t.Fatalf("args length mismatch: got %v want %v (encoded=%q)", got.Args, tt.data.Args, encoded)
}
for i := range tt.data.Args {
if got.Args[i] != tt.data.Args[i] {
t.Fatalf("arg %d mismatch: got %q want %q (encoded=%q)", i, got.Args[i], tt.data.Args[i], encoded)
}
}
})
}
}
func TestCompactPayloadDecodeRejectsMissingSeparator(t *testing.T) {
if _, err := decodeCompactPayload("noseparator"); err == nil {
t.Fatal("expected error decoding payload without separator")
}
}
func TestDecodePayloadStrictRejectsCompactMismatchedType(t *testing.T) {
kb := NewInlineKeyboardCompact(1).
AddCallbackButton("A", "cmd", 1)
_, _, err := decodePayload(BotPayloadJSON, kb.Get().InlineKeyboard[0][0].CallbackData, true)
if !errors.Is(err, ErrPayloadTypeMismatch) {
t.Fatalf("expected ErrPayloadTypeMismatch, got %v", err)
}
}
func TestDecodePayloadStrictRejectsMismatchedType(t *testing.T) {
kb := NewInlineKeyboardBase64(1).
AddCallbackButton("A", "cmd", 1)
+12 -7
View File
@@ -22,15 +22,15 @@ import (
//
// Behavior:
// 1. Uses the bot's current update offset (via GetUpdateOffset)
// 2. Requests updates with 30-second timeout
// 2. Requests updates with the timeout configured via PollTimeout
// 3. Filters updates by types specified in bot.GetUpdateTypes()
// 4. Logs raw update JSON if RequestLogger is configured
// 5. Automatically updates the offset to the last received update ID + 1
// 6. Returns all received updates (empty slice if none)
//
// Note: This is a blocking call that waits up to 30 seconds for new updates,
// unless ctx is canceled earlier. For non-blocking behavior, consider using
// webhooks instead.
// Note: This is a blocking call that waits up to the configured PollTimeout
// for new updates, unless ctx is canceled earlier. For non-blocking behavior,
// consider using webhooks instead.
//
// Example:
//
@@ -43,15 +43,17 @@ import (
// }
func (bot *Bot[T]) Updates(ctx context.Context) ([]tgapi.Update, error) {
offset := bot.GetUpdateOffset()
timeout := bot.pollTimeout
params := tgapi.UpdateParams{
Offset: new(offset),
Timeout: new(30),
Timeout: new(timeout),
AllowedUpdates: bot.GetUpdateTypes(),
}
zero := make([]tgapi.Update, 0)
updates, err := bot.api.GetUpdatesWithContext(ctx, params)
if err != nil {
return nil, err
return zero, err
}
if bot.requestLogger != nil {
@@ -66,7 +68,10 @@ func (bot *Bot[T]) Updates(ctx context.Context) ([]tgapi.Update, error) {
if len(updates) > 0 {
bot.SetUpdateOffset(updates[len(updates)-1].UpdateID + 1)
}
return updates, err
if updates == nil {
return zero, nil
}
return updates, nil
}
// UpdatesIter fetches updates once and yields each update in order.
+128 -97
View File
@@ -14,10 +14,10 @@ import (
"git.scuroneko.dev/scuroneko/sneklog/v2"
)
// MsgContext holds the normalized per-update context passed to command, payload,
// MessageContext holds the normalized per-update context passed to command, payload,
// scene, middleware, and generic update handlers.
//
// MsgContext is populated from the current Telegram update before handler routing.
// MessageContext is populated from the current Telegram update before handler routing.
// Not every field is guaranteed for every update kind. In particular:
// - Update is always present.
// - Msg is populated only for update kinds that carry a Telegram message object.
@@ -27,10 +27,10 @@ import (
// - CallbackQueryID, CallbackMsgID, and InlineMsgID are populated only for
// callback query handling when the corresponding callback targets exist.
//
// Helper methods on MsgContext may require a message-backed context. For example,
// Helper methods on MessageContext may require a message-backed context. For example,
// reply helpers need Msg, while inline callback edit helpers can work through
// InlineMsgID when there is no chat message.
type MsgContext struct {
type MessageContext struct {
API *tgapi.API
Update tgapi.Update
@@ -81,21 +81,21 @@ type MsgContext struct {
payloadType BotPayloadType
sceneRuntime sceneRuntime
observer Observer
botID int64
ctx context.Context
}
// AnswerMessage represents a message sent or edited via MsgContext.
// AnswerMessage represents a message sent or edited via MessageContext.
// It holds metadata to allow further editing or deletion.
type AnswerMessage struct {
MessageID int
Text string
IsMedia bool
ctx *MsgContext // internal back-reference
ctx *MessageContext // internal back-reference
}
// Internal helper for text edits with optional keyboard and parse mode.
func (ctx *MsgContext) edit(messageID int, text string, keyboard *InlineKeyboard, parseMode tgapi.ParseMode) *AnswerMessage {
func (ctx *MessageContext) edit(messageID int, text string, keyboard *InlineKeyboard, parseMode tgapi.ParseMode) *AnswerMessage {
if err := validateMessageText(text); err != nil {
ctx.Logger.Errorln(err)
return nil
@@ -139,14 +139,13 @@ func (m *AnswerMessage) Edit(text string) *AnswerMessage {
// EditMarkdown replaces the text of the message using MarkdownV2 formatting.
//
// ⚠️ WARNING: User input must be escaped with laniakea.EscapeMarkdownV2() before passing here.
// ⚠️ WARNING: User input must be escaped with tgfmt.EscapeMarkdownV2() before passing here.
// Unescaped input may cause Telegram API errors or broken formatting.
func (m *AnswerMessage) EditMarkdown(text string) *AnswerMessage {
return m.ctx.edit(m.MessageID, text, nil, tgapi.ParseMDV2)
return m.ctx.edit(m.MessageID, text, nil, tgapi.ParseMarkdownV2)
}
// Internal helper for editing callback-linked messages.
func (ctx *MsgContext) editCallback(text string, keyboard *InlineKeyboard, parseMode tgapi.ParseMode) *AnswerMessage {
func (ctx *MessageContext) editCallback(text string, keyboard *InlineKeyboard, parseMode tgapi.ParseMode) *AnswerMessage {
if ctx.CallbackMsgID == 0 && ctx.InlineMsgID == "" {
ctx.Logger.Errorln(ErrCallbackMessageMissing)
return nil
@@ -155,31 +154,30 @@ func (ctx *MsgContext) editCallback(text string, keyboard *InlineKeyboard, parse
}
// EditCallback edits the callback message using plain text (ParseNone).
func (ctx *MsgContext) EditCallback(text string, keyboard *InlineKeyboard) *AnswerMessage {
func (ctx *MessageContext) EditCallback(text string, keyboard *InlineKeyboard) *AnswerMessage {
return ctx.editCallback(text, keyboard, tgapi.ParseNone)
}
// EditCallbackMarkdown edits the callback message using MarkdownV2.
//
// ⚠️ WARNING: User input must be escaped with laniakea.EscapeMarkdownV2() before passing here.
func (ctx *MsgContext) EditCallbackMarkdown(text string, keyboard *InlineKeyboard) *AnswerMessage {
return ctx.editCallback(text, keyboard, tgapi.ParseMDV2)
// ⚠️ WARNING: User input must be escaped with tgfmt.EscapeMarkdownV2() before passing here.
func (ctx *MessageContext) EditCallbackMarkdown(text string, keyboard *InlineKeyboard) *AnswerMessage {
return ctx.editCallback(text, keyboard, tgapi.ParseMarkdownV2)
}
// EditCallbackf formats a string using fmt.Sprintf and edits the callback message with plain text.
func (ctx *MsgContext) EditCallbackf(format string, keyboard *InlineKeyboard, args ...any) *AnswerMessage {
func (ctx *MessageContext) EditCallbackf(format string, keyboard *InlineKeyboard, args ...any) *AnswerMessage {
return ctx.editCallback(fmt.Sprintf(format, args...), keyboard, tgapi.ParseNone)
}
// EditCallbackfMarkdown formats a string using fmt.Sprintf and edits the callback message with MarkdownV2.
//
// ⚠️ WARNING: User input must be escaped with laniakea.EscapeMarkdownV2() before passing here.
func (ctx *MsgContext) EditCallbackfMarkdown(format string, keyboard *InlineKeyboard, args ...any) *AnswerMessage {
return ctx.editCallback(fmt.Sprintf(format, args...), keyboard, tgapi.ParseMDV2)
// ⚠️ WARNING: User input must be escaped with tgfmt.EscapeMarkdownV2() before passing here.
func (ctx *MessageContext) EditCallbackfMarkdown(format string, keyboard *InlineKeyboard, args ...any) *AnswerMessage {
return ctx.editCallback(fmt.Sprintf(format, args...), keyboard, tgapi.ParseMarkdownV2)
}
// Internal helper for media-caption edits.
func (ctx *MsgContext) editPhotoText(messageID int, text string, kb *InlineKeyboard, parseMode tgapi.ParseMode) *AnswerMessage {
func (ctx *MessageContext) editPhotoText(messageID int, text string, kb *InlineKeyboard, parseMode tgapi.ParseMode) *AnswerMessage {
if err := validateCaptionText(text); err != nil {
ctx.Logger.Errorln(err)
return nil
@@ -223,9 +221,9 @@ func (m *AnswerMessage) EditCaption(text string) *AnswerMessage {
// EditCaptionMarkdown edits the caption of a media message using MarkdownV2.
//
// ⚠️ WARNING: User input must be escaped with laniakea.EscapeMarkdownV2() before passing here.
// ⚠️ WARNING: User input must be escaped with tgfmt.EscapeMarkdownV2() before passing here.
func (m *AnswerMessage) EditCaptionMarkdown(text string) *AnswerMessage {
return m.ctx.editPhotoText(m.MessageID, text, nil, tgapi.ParseMDV2)
return m.ctx.editPhotoText(m.MessageID, text, nil, tgapi.ParseMarkdownV2)
}
// EditCaptionKeyboard edits the caption of a media message with a new inline keyboard (plain text).
@@ -235,13 +233,12 @@ func (m *AnswerMessage) EditCaptionKeyboard(text string, kb *InlineKeyboard) *An
// EditCaptionKeyboardMarkdown edits the caption of a media message with a new inline keyboard using MarkdownV2.
//
// ⚠️ WARNING: User input must be escaped with laniakea.EscapeMarkdownV2() before passing here.
// ⚠️ WARNING: User input must be escaped with tgfmt.EscapeMarkdownV2() before passing here.
func (m *AnswerMessage) EditCaptionKeyboardMarkdown(text string, kb *InlineKeyboard) *AnswerMessage {
return m.ctx.editPhotoText(m.MessageID, text, kb, tgapi.ParseMDV2)
return m.ctx.editPhotoText(m.MessageID, text, kb, tgapi.ParseMarkdownV2)
}
// Internal helper for message replies with optional keyboard and parse mode.
func (ctx *MsgContext) answer(text string, keyboard *InlineKeyboard, parseMode tgapi.ParseMode) *AnswerMessage {
func (ctx *MessageContext) answer(text string, keyboard *InlineKeyboard, parseMode tgapi.ParseMode) *AnswerMessage {
if ctx.Msg == nil {
ctx.Logger.Errorln(ErrMessageContextNil)
return nil
@@ -276,7 +273,7 @@ func (ctx *MsgContext) answer(text string, keyboard *InlineKeyboard, parseMode t
}
// Answer sends a plain text message (ParseNone).
func (ctx *MsgContext) Answer(text string) *AnswerMessage {
func (ctx *MessageContext) Answer(text string) *AnswerMessage {
return ctx.answer(text, nil, tgapi.ParseNone)
}
@@ -284,54 +281,54 @@ func (ctx *MsgContext) Answer(text string) *AnswerMessage {
//
// The text is split into Telegram-safe chunks. Returned messages preserve send
// order. If a chunk fails to send, already-sent messages are returned.
func (ctx *MsgContext) AnswerLong(text string) []*AnswerMessage {
func (ctx *MessageContext) AnswerLong(text string) []*AnswerMessage {
return ctx.answerLong(text, nil, tgapi.ParseNone)
}
// AnswerMarkdown sends a message using MarkdownV2 formatting.
//
// ⚠️ WARNING: User input must be escaped with laniakea.EscapeMarkdownV2() before passing here.
func (ctx *MsgContext) AnswerMarkdown(text string) *AnswerMessage {
return ctx.answer(text, nil, tgapi.ParseMDV2)
// ⚠️ WARNING: User input must be escaped with tgfmt.EscapeMarkdownV2() before passing here.
func (ctx *MessageContext) AnswerMarkdown(text string) *AnswerMessage {
return ctx.answer(text, nil, tgapi.ParseMarkdownV2)
}
// Answerf formats a string using fmt.Sprintf and sends it as a plain text message.
func (ctx *MsgContext) Answerf(template string, args ...any) *AnswerMessage {
func (ctx *MessageContext) Answerf(template string, args ...any) *AnswerMessage {
return ctx.answer(fmt.Sprintf(template, args...), nil, tgapi.ParseNone)
}
// AnswerLongf formats a string using fmt.Sprintf and sends it as one or more plain-text messages.
func (ctx *MsgContext) AnswerLongf(template string, args ...any) []*AnswerMessage {
func (ctx *MessageContext) AnswerLongf(template string, args ...any) []*AnswerMessage {
return ctx.answerLong(fmt.Sprintf(template, args...), nil, tgapi.ParseNone)
}
// AnswerfMarkdown formats a string using fmt.Sprintf and sends it using MarkdownV2.
//
// ⚠️ WARNING: User input must be escaped with laniakea.EscapeMarkdownV2() before passing here.
func (ctx *MsgContext) AnswerfMarkdown(template string, args ...any) *AnswerMessage {
return ctx.answer(fmt.Sprintf(template, args...), nil, tgapi.ParseMDV2)
// ⚠️ WARNING: User input must be escaped with tgfmt.EscapeMarkdownV2() before passing here.
func (ctx *MessageContext) AnswerfMarkdown(template string, args ...any) *AnswerMessage {
return ctx.answer(fmt.Sprintf(template, args...), nil, tgapi.ParseMarkdownV2)
}
// Keyboard sends a message with an inline keyboard (plain text).
func (ctx *MsgContext) Keyboard(text string, kb *InlineKeyboard) *AnswerMessage {
func (ctx *MessageContext) Keyboard(text string, kb *InlineKeyboard) *AnswerMessage {
return ctx.answer(text, kb, tgapi.ParseNone)
}
// KeyboardLong sends long plain text split across multiple messages.
//
// The inline keyboard is attached only to the final chunk.
func (ctx *MsgContext) KeyboardLong(text string, kb *InlineKeyboard) []*AnswerMessage {
func (ctx *MessageContext) KeyboardLong(text string, kb *InlineKeyboard) []*AnswerMessage {
return ctx.answerLong(text, kb, tgapi.ParseNone)
}
// KeyboardMarkdown sends a message with an inline keyboard using MarkdownV2.
//
// ⚠️ WARNING: User input must be escaped with laniakea.EscapeMarkdownV2() before passing here.
func (ctx *MsgContext) KeyboardMarkdown(text string, keyboard *InlineKeyboard) *AnswerMessage {
return ctx.answer(text, keyboard, tgapi.ParseMDV2)
// ⚠️ WARNING: User input must be escaped with tgfmt.EscapeMarkdownV2() before passing here.
func (ctx *MessageContext) KeyboardMarkdown(text string, keyboard *InlineKeyboard) *AnswerMessage {
return ctx.answer(text, keyboard, tgapi.ParseMarkdownV2)
}
func (ctx *MsgContext) answerLong(text string, keyboard *InlineKeyboard, parseMode tgapi.ParseMode) []*AnswerMessage {
func (ctx *MessageContext) answerLong(text string, keyboard *InlineKeyboard, parseMode tgapi.ParseMode) []*AnswerMessage {
if parseMode != tgapi.ParseNone {
ctx.Logger.Errorln(ErrMessageSplitImpossible)
return nil
@@ -370,8 +367,7 @@ func (ctx *MsgContext) answerLong(text string, keyboard *InlineKeyboard, parseMo
return messages
}
// Internal helper for photo replies with optional caption and keyboard.
func (ctx *MsgContext) answerPhoto(photoID, text string, kb *InlineKeyboard, parseMode tgapi.ParseMode) *AnswerMessage {
func (ctx *MessageContext) answerPhoto(photoID, text string, kb *InlineKeyboard, parseMode tgapi.ParseMode) *AnswerMessage {
if ctx.Msg == nil {
ctx.Logger.Errorln(ErrMessageContextNil)
return nil
@@ -407,43 +403,42 @@ func (ctx *MsgContext) answerPhoto(photoID, text string, kb *InlineKeyboard, par
}
// AnswerPhoto sends a photo with plain text caption.
func (ctx *MsgContext) AnswerPhoto(photoID, text string) *AnswerMessage {
func (ctx *MessageContext) AnswerPhoto(photoID, text string) *AnswerMessage {
return ctx.answerPhoto(photoID, text, nil, tgapi.ParseNone)
}
// AnswerPhotoMarkdown sends a photo with MarkdownV2 caption.
//
// ⚠️ WARNING: User input must be escaped with laniakea.EscapeMarkdownV2() before passing here.
func (ctx *MsgContext) AnswerPhotoMarkdown(photoID, text string) *AnswerMessage {
return ctx.answerPhoto(photoID, text, nil, tgapi.ParseMDV2)
// ⚠️ WARNING: User input must be escaped with tgfmt.EscapeMarkdownV2() before passing here.
func (ctx *MessageContext) AnswerPhotoMarkdown(photoID, text string) *AnswerMessage {
return ctx.answerPhoto(photoID, text, nil, tgapi.ParseMarkdownV2)
}
// AnswerPhotoKeyboard sends a photo with caption and inline keyboard (plain text).
func (ctx *MsgContext) AnswerPhotoKeyboard(photoID, text string, kb *InlineKeyboard) *AnswerMessage {
func (ctx *MessageContext) AnswerPhotoKeyboard(photoID, text string, kb *InlineKeyboard) *AnswerMessage {
return ctx.answerPhoto(photoID, text, kb, tgapi.ParseNone)
}
// AnswerPhotoKeyboardMarkdown sends a photo with caption and inline keyboard using MarkdownV2.
//
// ⚠️ WARNING: User input must be escaped with laniakea.EscapeMarkdownV2() before passing here.
func (ctx *MsgContext) AnswerPhotoKeyboardMarkdown(photoID, text string, kb *InlineKeyboard) *AnswerMessage {
return ctx.answerPhoto(photoID, text, kb, tgapi.ParseMDV2)
// ⚠️ WARNING: User input must be escaped with tgfmt.EscapeMarkdownV2() before passing here.
func (ctx *MessageContext) AnswerPhotoKeyboardMarkdown(photoID, text string, kb *InlineKeyboard) *AnswerMessage {
return ctx.answerPhoto(photoID, text, kb, tgapi.ParseMarkdownV2)
}
// AnswerPhotof formats a string and sends it as a photo caption (plain text).
func (ctx *MsgContext) AnswerPhotof(photoID, template string, args ...any) *AnswerMessage {
func (ctx *MessageContext) AnswerPhotof(photoID, template string, args ...any) *AnswerMessage {
return ctx.answerPhoto(photoID, fmt.Sprintf(template, args...), nil, tgapi.ParseNone)
}
// AnswerPhotofMarkdown formats a string and sends it as a photo caption using MarkdownV2.
//
// ⚠️ WARNING: User input must be escaped with laniakea.EscapeMarkdownV2() before passing here.
func (ctx *MsgContext) AnswerPhotofMarkdown(photoID, template string, args ...any) *AnswerMessage {
return ctx.answerPhoto(photoID, fmt.Sprintf(template, args...), nil, tgapi.ParseMDV2)
// ⚠️ WARNING: User input must be escaped with tgfmt.EscapeMarkdownV2() before passing here.
func (ctx *MessageContext) AnswerPhotofMarkdown(photoID, template string, args ...any) *AnswerMessage {
return ctx.answerPhoto(photoID, fmt.Sprintf(template, args...), nil, tgapi.ParseMarkdownV2)
}
// Internal helper that deletes a message by ID.
func (ctx *MsgContext) delete(messageID int) {
func (ctx *MessageContext) delete(messageID int) {
if messageID == 0 {
ctx.Logger.Errorln(ErrMessageIDZero)
return
@@ -465,7 +460,7 @@ func (ctx *MsgContext) delete(messageID int) {
func (m *AnswerMessage) Delete() { m.ctx.delete(m.MessageID) }
// CallbackDelete deletes the message that triggered the callback query.
func (ctx *MsgContext) CallbackDelete() {
func (ctx *MessageContext) CallbackDelete() {
if ctx.CallbackMsgID == 0 {
ctx.Logger.Errorln(ErrCallbackMessageMissing)
return
@@ -473,8 +468,7 @@ func (ctx *MsgContext) CallbackDelete() {
ctx.delete(ctx.CallbackMsgID)
}
// Internal helper that answers a callback query with optional text, alert, or URL.
func (ctx *MsgContext) answerCallbackQuery(url, text string, showAlert bool) {
func (ctx *MessageContext) answerCallbackQuery(url, text string, showAlert bool) {
if len(ctx.CallbackQueryID) == 0 {
return
}
@@ -487,22 +481,22 @@ func (ctx *MsgContext) answerCallbackQuery(url, text string, showAlert bool) {
}
}
// AnswerCbQuery answers the callback query with no text or alert.
func (ctx *MsgContext) AnswerCbQuery() { ctx.answerCallbackQuery("", "", false) }
// AnswerCallback answers the callback query with no text or alert.
func (ctx *MessageContext) AnswerCallback() { ctx.answerCallbackQuery("", "", false) }
// AnswerCbQueryText answers the callback query with a text notification.
func (ctx *MsgContext) AnswerCbQueryText(text string) { ctx.answerCallbackQuery("", text, false) }
// AnswerCallbackText answers the callback query with a text notification.
func (ctx *MessageContext) AnswerCallbackText(text string) { ctx.answerCallbackQuery("", text, false) }
// AnswerCbQueryAlert answers the callback query with a user-visible alert.
func (ctx *MsgContext) AnswerCbQueryAlert(text string) { ctx.answerCallbackQuery("", text, true) }
// AnswerCallbackAlert answers the callback query with a user-visible alert.
func (ctx *MessageContext) AnswerCallbackAlert(text string) { ctx.answerCallbackQuery("", text, true) }
// AnswerCbQueryURL answers the callback query with a URL redirect.
func (ctx *MsgContext) AnswerCbQueryURL(u string) { ctx.answerCallbackQuery(u, "", false) }
// AnswerCallbackURL answers the callback query with a URL redirect.
func (ctx *MessageContext) AnswerCallbackURL(u string) { ctx.answerCallbackQuery(u, "", false) }
// SendAction sends a chat action (typing, uploading_photo, etc.) to indicate bot activity.
func (ctx *MsgContext) SendAction(action tgapi.ChatActionType) {
func (ctx *MessageContext) SendAction(action tgapi.ChatActionType) {
if ctx.Msg == nil {
ctx.Logger.Errorln("Can't send action without chat message context")
ctx.Logger.Errorln(ErrMessageContextNil)
return
}
params := tgapi.SendChatAction{
@@ -517,13 +511,12 @@ func (ctx *MsgContext) SendAction(action tgapi.ChatActionType) {
}
}
// Internal helper that formats, sends, and logs an error.
func (ctx *MsgContext) error(err error) {
func (ctx *MessageContext) error(err error) {
if err == nil {
return
}
ctx.Logger.Errorln(err)
if IsInternalError(err) {
if !IsUserError(err) {
return
}
text := fmt.Sprintf(ctx.errorTemplate, err.Error())
@@ -535,10 +528,15 @@ func (ctx *MsgContext) error(err error) {
}
}
// Error is an alias for error().
func (ctx *MsgContext) Error(err error) { ctx.error(err) }
// Error routes err through the centralized handler error path.
//
// The error is logged via ctx.Logger. When IsUserError(err) is true, the
// formatted error template is delivered to the user — through an answer
// to the active callback query when one exists, otherwise as a chat reply.
// Internal errors are logged but not surfaced to the user.
func (ctx *MessageContext) Error(err error) { ctx.error(err) }
func (ctx *MsgContext) newDraft(parseMode tgapi.ParseMode) *Draft {
func (ctx *MessageContext) newDraft(parseMode tgapi.ParseMode) *Draft {
if ctx.Msg == nil {
ctx.Logger.Errorln(ErrMessageContextNil)
return nil
@@ -567,20 +565,20 @@ func (ctx *MsgContext) newDraft(parseMode tgapi.ParseMode) *Draft {
// NewDraft creates a new message draft associated with the current chat.
// Uses the API limiter to avoid rate limiting.
func (ctx *MsgContext) NewDraft() *Draft {
func (ctx *MessageContext) NewDraft() *Draft {
return ctx.newDraft(tgapi.ParseNone)
}
// NewDraftMarkdown creates a new message draft associated with the current chat,
// with Markdown V2 parse mode enabled.
// Uses the API limiter to avoid rate limiting.
func (ctx *MsgContext) NewDraftMarkdown() *Draft {
return ctx.newDraft(tgapi.ParseMDV2)
func (ctx *MessageContext) NewDraftMarkdown() *Draft {
return ctx.newDraft(tgapi.ParseMarkdownV2)
}
// Translate looks up a key in the current user's language.
// Falls back to the bot's default language if user's language is unknown or unsupported.
func (ctx *MsgContext) Translate(key string) string {
func (ctx *MessageContext) Translate(key string) string {
if ctx.From == nil {
return key
}
@@ -590,10 +588,15 @@ func (ctx *MsgContext) Translate(key string) string {
// NewInlineKeyboard creates a new keyboard builder with the context's payload
// encoding type and the specified maximum number of buttons per row.
func (ctx *MsgContext) NewInlineKeyboard(maxRow int) *InlineKeyboard {
func (ctx *MessageContext) NewInlineKeyboard(maxRow int) *InlineKeyboard {
return NewInlineKeyboard(ctx.payloadType, maxRow)
}
// NewInlineKeyboardButton creates a button builder using the context payload encoding.
func (ctx *MessageContext) NewInlineKeyboardButton(text string) InlineKeyboardButtonBuilder {
return NewInlineKeyboardButton(text).SetPayloadType(ctx.payloadType)
}
func bindPositional(args []string, dst any) error {
v := reflect.ValueOf(dst)
if v.Kind() != reflect.Pointer || v.IsNil() {
@@ -679,19 +682,19 @@ func bindPositional(args []string, dst any) error {
// are provided than fields, the remaining fields keep their zero values. If the
// final bindable field is a string, it receives the remaining arguments joined
// with spaces.
func (ctx *MsgContext) BindArgs(dst any) error {
func (ctx *MessageContext) BindArgs(dst any) error {
return bindPositional(ctx.Args, dst)
}
// Context returns the request-scoped context associated with the current update.
func (ctx *MsgContext) Context() context.Context {
func (ctx *MessageContext) Context() context.Context {
if ctx.ctx == nil {
return context.Background()
}
return ctx.ctx
}
func (ctx *MsgContext) emitPolicyChecked(event PolicyCheckedEvent) {
func (ctx *MessageContext) emitPolicyChecked(event PolicyCheckedEvent) {
if ctx == nil || ctx.observer == nil {
return
}
@@ -708,7 +711,7 @@ func (ctx *MsgContext) emitPolicyChecked(event PolicyCheckedEvent) {
}
// EnterScene enters the named scene at its configured entry step.
func (ctx *MsgContext) EnterScene(name string) error {
func (ctx *MessageContext) EnterScene(name string) error {
if ctx.sceneRuntime == nil {
return ErrSceneRuntimeNil
}
@@ -718,7 +721,7 @@ func (ctx *MsgContext) EnterScene(name string) error {
return ErrSceneNotFound
}
key, ok := ctx.sceneRuntime.buildSceneKey(scene.Scope, ctx)
key, ok := buildSceneKey(scene.Scope, ctx)
if !ok {
return ErrCantFindSession
}
@@ -738,7 +741,7 @@ func (ctx *MsgContext) EnterScene(name string) error {
}
// EnterSceneStep enters the named scene at a specific step.
func (ctx *MsgContext) EnterSceneStep(name, step string) error {
func (ctx *MessageContext) EnterSceneStep(name, step string) error {
if ctx.sceneRuntime == nil {
return ErrSceneRuntimeNil
}
@@ -751,21 +754,18 @@ func (ctx *MsgContext) EnterSceneStep(name, step string) error {
return ErrSceneStepNotFound
}
key, ok := ctx.sceneRuntime.buildSceneKey(scene.Scope, ctx)
key, ok := buildSceneKey(scene.Scope, ctx)
if !ok {
return ErrCantFindSession
}
session := SceneSession{
Scene: scene.Name,
Step: step,
}
session := SceneSession{Scene: scene.Name, Step: step}
return ctx.sceneRuntime.setSession(key, session)
}
// ExitScene leaves the currently active scene for this context.
func (ctx *MsgContext) ExitScene() error {
func (ctx *MessageContext) ExitScene() error {
if ctx.sceneRuntime == nil {
return ErrSceneRuntimeNil
}
@@ -783,10 +783,41 @@ func (ctx *MsgContext) ExitScene() error {
return ErrSceneNotFound
}
key, ok := ctx.sceneRuntime.buildSceneKey(scene.Scope, ctx)
key, ok := buildSceneKey(scene.Scope, ctx)
if !ok {
return ErrCantFindSession
}
return ctx.sceneRuntime.deleteSession(key)
}
// IsCallback reports whether the context belongs to a callback query.
func (ctx *MessageContext) IsCallback() bool {
return ctx.CallbackQueryID != "" || ctx.CallbackMsgID > 0 || ctx.InlineMsgID != ""
}
// HasPhoto reports whether the current message contains a photo payload.
func (ctx *MessageContext) HasPhoto() bool {
return ctx.Msg != nil && ctx.Msg.Photo.Len() > 0
}
func (ctx *MessageContext) upsertKeyboard(text string, keyboard *InlineKeyboard, parseMode tgapi.ParseMode) *AnswerMessage {
if ctx.IsCallback() {
if ctx.HasPhoto() {
ctx.CallbackDelete()
return ctx.answer(text, keyboard, parseMode)
}
return ctx.editCallback(text, keyboard, parseMode)
}
return ctx.answer(text, keyboard, parseMode)
}
// UpsertKeyboard edits a callback message or sends a new plain-text message with a keyboard.
func (ctx *MessageContext) UpsertKeyboard(text string, keyboard *InlineKeyboard) *AnswerMessage {
return ctx.upsertKeyboard(text, keyboard, tgapi.ParseNone)
}
// UpsertKeyboardMarkdown edits a callback message or sends a new MarkdownV2 message with a keyboard.
func (ctx *MessageContext) UpsertKeyboardMarkdown(text string, keyboard *InlineKeyboard) *AnswerMessage {
return ctx.upsertKeyboard(text, keyboard, tgapi.ParseMarkdownV2)
}
+147 -15
View File
@@ -44,7 +44,7 @@ func TestAnswerPhotoIncludesDirectMessagesTopicID(t *testing.T) {
}
}()
ctx := &MsgContext{
ctx := &MessageContext{
API: api,
Msg: &tgapi.Message{
Chat: &tgapi.Chat{ID: 42, Type: tgapi.ChatTypePrivate},
@@ -56,6 +56,7 @@ func TestAnswerPhotoIncludesDirectMessagesTopicID(t *testing.T) {
answer := ctx.AnswerPhoto("photo-id", "caption")
if answer == nil {
t.Fatal("expected answer message")
return
}
if answer.MessageID != 9 {
t.Fatalf("unexpected message id: %d", answer.MessageID)
@@ -73,7 +74,7 @@ func TestBindArgsBindsScalarFields(t *testing.T) {
Name string
}
ctx := &MsgContext{Args: []string{"42", "true", "3.5", "Ada", "Lovelace"}}
ctx := &MessageContext{Args: []string{"42", "true", "3.5", "Ada", "Lovelace"}}
var got input
if err := ctx.BindArgs(&got); err != nil {
@@ -91,6 +92,23 @@ func TestBindArgsBindsScalarFields(t *testing.T) {
}
}
func TestNewInlineKeyboardButtonUsesContextPayloadType(t *testing.T) {
ctx := &MessageContext{payloadType: BotPayloadBase64}
kb := NewInlineKeyboardJSON(1).
AddButton(ctx.NewInlineKeyboardButton("A").SetCallbackData("cmd", 1, "two"))
got, _, err := decodePayload(BotPayloadJSON, kb.Get().InlineKeyboard[0][0].CallbackData, false)
if err != nil {
t.Fatalf("decodePayload returned error: %v", err)
}
want := CallbackData{Command: "cmd", Args: []string{"1", "two"}}
if !reflect.DeepEqual(got, want) {
t.Fatalf("unexpected payload: got %#v want %#v", got, want)
}
}
func TestBindArgsLeavesTrailingFieldsZeroWhenArgsRunOut(t *testing.T) {
type input struct {
ID int
@@ -98,7 +116,7 @@ func TestBindArgsLeavesTrailingFieldsZeroWhenArgsRunOut(t *testing.T) {
Admin bool
}
ctx := &MsgContext{Args: []string{"7"}}
ctx := &MessageContext{Args: []string{"7"}}
var got input
if err := ctx.BindArgs(&got); err != nil {
@@ -117,7 +135,7 @@ func TestBindArgsLeavesTrailingFieldsZeroWhenArgsRunOut(t *testing.T) {
}
func TestBindArgsRejectsInvalidTargets(t *testing.T) {
ctx := &MsgContext{Args: []string{"1"}}
ctx := &MessageContext{Args: []string{"1"}}
if err := ctx.BindArgs(nil); !errors.Is(err, ErrBindArgsTargetNotPointer) {
t.Fatalf("expected ErrBindArgsTargetNotPointer for nil target, got %v", err)
@@ -134,7 +152,7 @@ func TestBindArgsReportsConversionFailures(t *testing.T) {
ID int
}
ctx := &MsgContext{Args: []string{"oops"}}
ctx := &MessageContext{Args: []string{"oops"}}
var got input
err := ctx.BindArgs(&got)
@@ -154,7 +172,7 @@ func TestBindArgsRejectsUnsupportedFieldTypes(t *testing.T) {
Tags []string
}
ctx := &MsgContext{Args: []string{"tag"}}
ctx := &MessageContext{Args: []string{"tag"}}
var got input
err := ctx.BindArgs(&got)
@@ -166,7 +184,37 @@ func TestBindArgsRejectsUnsupportedFieldTypes(t *testing.T) {
}
}
func TestErrorDefaultRemainsUserVisibleForMessageFlow(t *testing.T) {
func TestErrorDefaultStaysInternalForMessageFlow(t *testing.T) {
client := &http.Client{
Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
t.Fatal("unexpected HTTP request for unclassified error")
return nil, nil
}),
}
api := tgapi.NewAPI(
tgapi.NewAPIOpts("token").
SetAPIURL("https://example.test").
SetHTTPClient(client),
)
defer func() {
if err := api.Close(); err != nil {
t.Fatalf("Close returned error: %v", err)
}
}()
ctx := &MessageContext{
API: api,
Msg: &tgapi.Message{Chat: &tgapi.Chat{ID: 42, Type: tgapi.ChatTypePrivate}},
Logger: sneklog.NewLogger(),
errorTemplate: "Error: %s",
}
// Unclassified errors must not leak to the user. Only AsUserError replies.
ctx.error(errors.New("boom"))
}
func TestErrorUserVisibleAnswersForMessageFlow(t *testing.T) {
var requests int
var gotBody map[string]any
@@ -199,14 +247,14 @@ func TestErrorDefaultRemainsUserVisibleForMessageFlow(t *testing.T) {
}
}()
ctx := &MsgContext{
ctx := &MessageContext{
API: api,
Msg: &tgapi.Message{Chat: &tgapi.Chat{ID: 42, Type: tgapi.ChatTypePrivate}},
Logger: sneklog.NewLogger(),
errorTemplate: "Error: %s",
}
ctx.error(errors.New("boom"))
ctx.error(AsUserError(errors.New("boom")))
if requests != 1 {
t.Fatalf("expected one user-facing error reply, got %d requests", requests)
@@ -235,7 +283,7 @@ func TestErrorInternalSkipsUserReplyForMessageFlow(t *testing.T) {
}
}()
ctx := &MsgContext{
ctx := &MessageContext{
API: api,
Msg: &tgapi.Message{Chat: &tgapi.Chat{ID: 42, Type: tgapi.ChatTypePrivate}},
Logger: sneklog.NewLogger(),
@@ -264,7 +312,7 @@ func TestErrorInternalSkipsCallbackAnswer(t *testing.T) {
}
}()
ctx := &MsgContext{
ctx := &MessageContext{
API: api,
Logger: sneklog.NewLogger(),
errorTemplate: "%s",
@@ -307,7 +355,7 @@ func TestErrorUserVisibleAnswersCallback(t *testing.T) {
}
}()
ctx := &MsgContext{
ctx := &MessageContext{
API: api,
Logger: sneklog.NewLogger(),
errorTemplate: "Oops: %s",
@@ -324,8 +372,92 @@ func TestErrorUserVisibleAnswersCallback(t *testing.T) {
}
}
func TestIsCallbackIncludesInlineCallbackTargets(t *testing.T) {
tests := []struct {
name string
ctx MessageContext
want bool
}{
{name: "callback query id", ctx: MessageContext{CallbackQueryID: "cb-1"}, want: true},
{name: "callback message id", ctx: MessageContext{CallbackMsgID: 12}, want: true},
{name: "inline message id", ctx: MessageContext{InlineMsgID: "inline-1"}, want: true},
{name: "not callback", ctx: MessageContext{}, want: false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.ctx.IsCallback(); got != tt.want {
t.Fatalf("IsCallback() = %v, want %v", got, tt.want)
}
})
}
}
func TestUpsertKeyboardEditsInlineCallback(t *testing.T) {
var requests int
var gotPath string
var gotBody map[string]any
client := &http.Client{
Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
requests++
gotPath = req.URL.Path
body, err := io.ReadAll(req.Body)
if err != nil {
t.Fatalf("failed to read request body: %v", err)
}
if err := json.Unmarshal(body, &gotBody); err != nil {
t.Fatalf("failed to decode request body: %v", err)
}
return &http.Response{
StatusCode: http.StatusOK,
Header: http.Header{"Content-Type": []string{"application/json"}},
Body: io.NopCloser(strings.NewReader(`{"ok":true,"result":true}`)),
}, nil
}),
}
api := tgapi.NewAPI(
tgapi.NewAPIOpts("token").
SetAPIURL("https://example.test").
SetHTTPClient(client),
)
defer func() {
if err := api.Close(); err != nil {
t.Fatalf("Close returned error: %v", err)
}
}()
ctx := &MessageContext{
API: api,
InlineMsgID: "inline-1",
Logger: sneklog.NewLogger(),
}
kb := NewInlineKeyboardJSON(1).AddCallbackButton("A", "cmd")
answer := ctx.UpsertKeyboard("updated", kb)
if answer == nil {
t.Fatal("expected answer message")
}
if requests != 1 {
t.Fatalf("expected one edit request, got %d", requests)
}
if gotPath != "/bottoken/editMessageText" {
t.Fatalf("unexpected request path: %s", gotPath)
}
if got := gotBody["inline_message_id"]; got != "inline-1" {
t.Fatalf("unexpected inline_message_id: %v", got)
}
if got := gotBody["text"]; got != "updated" {
t.Fatalf("unexpected text: %v", got)
}
if _, ok := gotBody["reply_markup"]; !ok {
t.Fatal("expected reply_markup in edit request")
}
}
func TestAnswerRejectsEmptyMessage(t *testing.T) {
ctx := &MsgContext{
ctx := &MessageContext{
Msg: &tgapi.Message{Chat: &tgapi.Chat{ID: 42, Type: tgapi.ChatTypePrivate}},
Logger: sneklog.NewLogger(),
}
@@ -354,7 +486,7 @@ func TestAnswerRejectsLongMessageWithoutSendingRequest(t *testing.T) {
}
}()
ctx := &MsgContext{
ctx := &MessageContext{
API: api,
Msg: &tgapi.Message{Chat: &tgapi.Chat{ID: 42, Type: tgapi.ChatTypePrivate}},
Logger: sneklog.NewLogger(),
@@ -438,7 +570,7 @@ func TestAnswerLongSplitsRequestsAndAttachesKeyboardToLastChunk(t *testing.T) {
}
}()
ctx := &MsgContext{
ctx := &MessageContext{
API: api,
Msg: &tgapi.Message{Chat: &tgapi.Chat{ID: 42, Type: tgapi.ChatTypePrivate}},
Logger: sneklog.NewLogger(),
+9 -8
View File
@@ -7,7 +7,7 @@ import (
"git.scuroneko.dev/scuroneko/laniakea/tgapi"
)
func (bot *Bot[T]) handleMessage(update *tgapi.Update, ctx *MsgContext) bool {
func (bot *Bot[T]) handleMessage(update *tgapi.Update, ctx *MessageContext) bool {
text, ok := messageText(update)
if !ok {
return false
@@ -22,18 +22,19 @@ func (bot *Bot[T]) handleMessage(update *tgapi.Update, ctx *MsgContext) bool {
if strings.Contains(cmd, "@") {
botUsername := bot.username
if botUsername != "" && strings.HasSuffix(cmd, "@"+botUsername) {
cmd = cmd[:len(cmd)-len("@"+botUsername)] // убираем @botname
cmd = cmd[:len(cmd)-len("@"+botUsername)] // remove @botname
}
}
// Ищем команду по точному совпадению
for _, plugin := range bot.plugins {
if _, exists := plugin.commands[cmd]; exists {
ctx.Text = args
ctx.Args = strings.Fields(args) // Убирает лишние пробелы
ctx.Args = strings.Fields(args)
ctx.Logger = plugin.logger
if plugin.logger != nil {
ctx.Logger = plugin.logger
if ctx.Logger == nil {
ctx.Logger = bot.logger
}
if !plugin.executeMiddlewares(ctx, bot.appData) {
return false
@@ -90,7 +91,7 @@ func (bot *Bot[T]) handleMessage(update *tgapi.Update, ctx *MsgContext) bool {
return bot.handleFallback(update, ctx)
}
func (bot *Bot[T]) handleFallback(update *tgapi.Update, ctx *MsgContext) bool {
func (bot *Bot[T]) handleFallback(update *tgapi.Update, ctx *MessageContext) bool {
text, ok := messageText(update)
if !ok {
return false
@@ -180,7 +181,7 @@ func messageText(update *tgapi.Update) (string, bool) {
return text, true
}
func (bot *Bot[T]) handleCallback(update *tgapi.Update, ctx *MsgContext) bool {
func (bot *Bot[T]) handleCallback(update *tgapi.Update, ctx *MessageContext) bool {
data, err := bot.decodePayload(update.CallbackQuery.Data)
if err != nil {
bot.logger.Errorln(err)
+4 -4
View File
@@ -145,8 +145,8 @@ func (ErrorEvent) isEvent() {}
// Observer receives best-effort runtime instrumentation events.
type Observer interface {
OnReceiveUpdate(ctx context.Context, event UpdateReceivedEvent)
OnHandledUpdate(ctx context.Context, event UpdateHandledEvent)
OnUpdateReceived(ctx context.Context, event UpdateReceivedEvent)
OnUpdateHandled(ctx context.Context, event UpdateHandledEvent)
OnHandlerStarted(ctx context.Context, event HandlerStartedEvent)
OnHandlerFinished(ctx context.Context, event HandlerFinishedEvent)
OnSceneTransition(ctx context.Context, event SceneTransitionEvent)
@@ -167,9 +167,9 @@ func (bot *Bot[T]) safeEmitEvent(ctx context.Context, event Event) {
}()
switch e := event.(type) {
case UpdateReceivedEvent:
bot.observer.OnReceiveUpdate(ctx, e)
bot.observer.OnUpdateReceived(ctx, e)
case UpdateHandledEvent:
bot.observer.OnHandledUpdate(ctx, e)
bot.observer.OnUpdateHandled(ctx, e)
case HandlerStartedEvent:
bot.observer.OnHandlerStarted(ctx, e)
case HandlerFinishedEvent:
+77 -178
View File
@@ -2,7 +2,6 @@ package laniakea
import (
"errors"
"regexp"
"git.scuroneko.dev/scuroneko/extypes"
"git.scuroneko.dev/scuroneko/laniakea/tgapi"
@@ -10,152 +9,6 @@ import (
"git.scuroneko.dev/scuroneko/sneklog/v2"
)
// CommandValueType defines the expected type of command argument.
type CommandValueType string
const (
// CommandValueStringType expects any non-empty string.
CommandValueStringType CommandValueType = "string"
// CommandValueIntType expects a decimal integer (digits only).
CommandValueIntType CommandValueType = "int"
// CommandValueBoolType expects a exact "true" or "false".
CommandValueBoolType CommandValueType = "bool"
// CommandValueAnyType accepts any input without validation.
CommandValueAnyType CommandValueType = "any"
)
var (
// CommandRegexInt matches one or more digits.
CommandRegexInt = regexp.MustCompile(`^\d+$`)
// CommandRegexString matches any non-empty string.
CommandRegexString = regexp.MustCompile(`^.+$`)
// CommandRegexBool matches true or false.
CommandRegexBool = regexp.MustCompile(`^(true|false)$`)
)
// ErrCmdArgCountMismatch is returned when the number of provided arguments
// is less than the number of required arguments.
var ErrCmdArgCountMismatch = errors.New("command arg count mismatch")
// ErrCmdArgRegexpMismatch is returned when an argument fails regex validation.
var ErrCmdArgRegexpMismatch = errors.New("command arg regexp mismatch")
var (
errCommandNotFound = errors.New("command not found")
errPayloadNotFound = errors.New("payload not found")
)
// CommandArg defines a single argument for a command, including type, regex,
// and whether it is required.
type CommandArg struct {
valueType CommandValueType // Type of expected value
text string // Human-readable description (not used in validation)
regex *regexp.Regexp // Regex used to validate input
required bool // Whether this argument must be provided
}
// NewCommandArg creates a new CommandArg with the given text and type.
// Uses a default regex based on the type (string or int).
// For CommandValueAnyType, no validation is performed.
func NewCommandArg(text string) CommandArg {
return CommandArg{CommandValueAnyType, text, CommandRegexString, false}
}
// SetValueType sets expected value type and switches built-in validation regexp.
func (c CommandArg) SetValueType(t CommandValueType) CommandArg {
regex := CommandRegexString
switch t {
case CommandValueIntType:
regex = CommandRegexInt
case CommandValueBoolType:
regex = CommandRegexBool
case CommandValueAnyType:
regex = nil // Skip validation
}
c.valueType = t
c.regex = regex
return c
}
// SetRequired marks this argument as required.
// Returns the receiver for method chaining.
func (c CommandArg) SetRequired() CommandArg {
c.required = true
return c
}
// CommandExecutor is the function type that executes a command.
// It receives the message context and injected application data.
// Returning a non-nil error routes it through the bot's error handler.
type CommandExecutor[T AppData] func(ctx *MsgContext, dbContext T) error
// Command represents a bot command with arguments, description, and executor.
// Can be registered in a Plugin and optionally skipped from auto-generation.
type Command[T AppData] struct {
command string // The command trigger (e.g., "/start")
description string // Human-readable description for help
exec CommandExecutor[T] // Function to execute when command is triggered
args extypes.Slice[CommandArg] // List of expected arguments
middlewares extypes.Slice[Middleware[T]] // Optional middleware chain
skipAutoCmd bool // If true, this command won't be auto-added to help menus
}
// NewCommand creates a new Command with the given executor, command string, and arguments.
// The command string should not include the leading slash (e.g., "start", not "/start").
func NewCommand[T any](exec CommandExecutor[T], command string, args ...CommandArg) *Command[T] {
return &Command[T]{command, "", exec, args, make(extypes.Slice[Middleware[T]], 0), false}
}
// NewPayload creates a new Command with the given executor, command payload string, and arguments.
// The command string can contain any symbols, but it is recommended to use only "_", "-", ".", a-z, A-Z, and 0-9.
func NewPayload[T any](exec CommandExecutor[T], command string, args ...CommandArg) *Command[T] {
return &Command[T]{command, "", exec, args, make(extypes.Slice[Middleware[T]], 0), false}
}
// Use adds a middleware to the command's execution chain.
// Middlewares are executed in the order they are added.
func (c *Command[T]) Use(m Middleware[T]) *Command[T] {
c.middlewares = c.middlewares.Push(m)
return c
}
// SetDescription sets the human-readable description of the command.
func (c *Command[T]) SetDescription(desc string) *Command[T] {
c.description = desc
return c
}
// SkipCommandAutoGen marks this command to be excluded from auto-generated help menus.
func (c *Command[T]) SkipCommandAutoGen() *Command[T] {
c.skipAutoCmd = true
return c
}
// Internal helper that validates provided command arguments.
func (c *Command[T]) validateArgs(args []string) error {
for i := range c.args.Len() {
if i >= len(args) && c.args.Get(i).required {
return ErrCmdArgCountMismatch
}
}
// Validate each argument against its regex
for i, arg := range args {
if i >= c.args.Len() {
// Extra arguments beyond defined args are ignored
break
}
cmdArg := c.args.Get(i)
if cmdArg.regex == nil {
continue // Skip validation for CommandValueAnyType
}
if !cmdArg.regex.MatchString(arg) {
return ErrCmdArgRegexpMismatch
}
}
return nil
}
// Plugin represents a collection of commands and payloads (e.g., callback handlers),
// with shared middleware and configuration.
//
@@ -170,6 +23,7 @@ type Plugin[T AppData] struct {
middlewares extypes.Slice[Middleware[T]] // Shared middlewares for all commands/payloads
skipAutoCmd bool // If true, all commands in this plugin are excluded from auto-help
logger *sneklog.Logger
loggerOwned bool // true when the logger was created by the bot during registration; only owned loggers are closed by Close
messageFallback CommandExecutor[T]
handlers map[tgapi.UpdateType]CommandExecutor[T]
@@ -192,7 +46,6 @@ func NewPlugin[T AppData](name string) *Plugin[T] {
}
// AddCommand registers a command in the plugin.
// The command's .command field is used as the key.
func (p *Plugin[T]) AddCommand(command *Command[T]) *Plugin[T] {
if command == nil {
if p.logger != nil {
@@ -200,14 +53,17 @@ func (p *Plugin[T]) AddCommand(command *Command[T]) *Plugin[T] {
}
return p
}
if _, exists := p.commands[command.command]; exists && p.logger != nil {
p.logger.Warnf("command '%s' already registered in plugin '%s'; overwriting", command.command, p.name)
}
p.commands[command.command] = command
return p
}
// NewCommand creates and immediately adds a new command to the plugin.
// Command creates and immediately adds a new command to the plugin.
// Returns the created command for further configuration.
func (p *Plugin[T]) NewCommand(exec CommandExecutor[T], command string, args ...CommandArg) *Command[T] {
cmd := NewCommand(exec, command, args...)
func (p *Plugin[T]) Command(command string, exec CommandExecutor[T], args ...CommandArg) *Command[T] {
cmd := NewCommand(command, exec, args...)
p.AddCommand(cmd)
return cmd
}
@@ -221,35 +77,70 @@ func (p *Plugin[T]) AddPayload(command *Command[T]) *Plugin[T] {
}
return p
}
if _, exists := p.payloads[command.command]; exists && p.logger != nil {
p.logger.Warnf("payload '%s' is already registered in plugin '%s'; overwriting", command.command, p.name)
}
p.payloads[command.command] = command
return p
}
// NewPayload creates and immediately adds a new payload command to the plugin.
// Payload creates and immediately adds a new payload command to the plugin.
// Returns the created payload command for further configuration.
func (p *Plugin[T]) NewPayload(exec CommandExecutor[T], command string, args ...CommandArg) *Command[T] {
cmd := NewPayload(exec, command, args...)
func (p *Plugin[T]) Payload(command string, exec CommandExecutor[T], args ...CommandArg) *Command[T] {
cmd := NewCommand(command, exec, args...)
p.AddPayload(cmd)
return cmd
}
// Scene creates, registers, and returns a new scene owned by the plugin.
func (p *Plugin[T]) Scene(name string) *Scene[T] {
scene := NewScene[T](name)
scene.setPluginName(p.name)
p.AddScene(scene)
return scene
}
// AddScene registers a multi-step scene in the plugin.
func (p *Plugin[T]) AddScene(scene *Scene[T]) *Plugin[T] {
if scene == nil {
return p
}
scene.PluginName = p.name
scene.setPluginName(p.name)
p.scenes[scene.Name] = scene
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.scenes[scene.name] = scene
return p
}
// NewScene creates, registers, and returns a new scene owned by the plugin.
func (p *Plugin[T]) NewScene(name string) *Scene[T] {
scene := NewScene[T](name)
scene.setPluginName(p.name)
p.AddScene(scene)
return scene
// CommandGroup configures and registers a prefixed command group.
func (p *Plugin[T]) CommandGroup(prefix string, groupFunc func(group *CommandGroup[T])) *Plugin[T] {
if groupFunc == nil {
return p
}
group := NewCommandGroup[T](prefix)
groupFunc(group)
if len(group.commands) == 0 {
return p
}
for _, cmd := range group.Build() {
p.AddCommand(cmd)
}
return p
}
// AddCommandGroup registers every command built by group.
func (p *Plugin[T]) AddCommandGroup(group *CommandGroup[T]) *Plugin[T] {
if group == nil {
return p
}
if len(group.commands) == 0 {
return p
}
for _, cmd := range group.Build() {
p.AddCommand(cmd)
}
return p
}
// UsePolicy registers a Policy as plugin middleware for all plugin handlers.
@@ -265,11 +156,11 @@ func (p *Plugin[T]) AddUpdateHandler(t tgapi.UpdateType, handler CommandExecutor
case tgapi.UpdateTypeMessage, tgapi.UpdateTypeChannelPost, tgapi.UpdateTypeCallbackQuery:
if p.logger == nil {
logger := utils.CreateLogger(p.name, utils.GetLoggerLevel(), utils.LogFormatText, nil)
logger.Warnf("%s can't be registred through AddUpdateHandler. Use AddPayload/NewPayload or AddCommand/NewCommand", t)
logger.Warnf("%s can't be registered through AddUpdateHandler. Use AddPayload/Payload or AddCommand/Command", t)
_ = logger.Close()
return p
}
p.logger.Warnf("%s can't be registred through AddUpdateHandler. Use AddPayload/NewPayload or AddCommand/NewCommand", t)
p.logger.Warnf("%s can't be registered through AddUpdateHandler. Use AddPayload/Payload or AddCommand/Command", t)
return p
}
p.handlers[t] = handler
@@ -326,9 +217,13 @@ func (p *Plugin[T]) SetMessageFallback(handler CommandExecutor[T]) *Plugin[T] {
// Close releases plugin-owned resources such as its logger and optional
// OnClose callback.
//
// Only loggers created by the bot during registration are closed. A logger
// supplied via SetLogger remains the caller's responsibility — the framework
// never closes a logger it does not own.
func (p *Plugin[T]) Close() error {
var e []error
if p.logger != nil {
if p.logger != nil && p.loggerOwned {
if err := p.logger.Close(); err != nil {
e = append(e, err)
}
@@ -341,8 +236,7 @@ func (p *Plugin[T]) Close() error {
return errors.Join(e...)
}
// Internal helper that validates and executes a command handler.
func (p *Plugin[T]) executeCmd(cmd string, ctx *MsgContext, db T) error {
func (p *Plugin[T]) executeCmd(cmd string, ctx *MessageContext, db T) error {
command, exists := p.commands[cmd]
if !exists {
return AsInternalError(errCommandNotFound)
@@ -363,8 +257,7 @@ func (p *Plugin[T]) executeCmd(cmd string, ctx *MsgContext, db T) error {
return command.exec(ctx, db)
}
// Internal helper that validates and executes a payload handler.
func (p *Plugin[T]) executePayload(payload string, ctx *MsgContext, db T) error {
func (p *Plugin[T]) executePayload(payload string, ctx *MessageContext, db T) error {
command, exists := p.payloads[payload]
if !exists {
return AsInternalError(errPayloadNotFound)
@@ -385,8 +278,7 @@ func (p *Plugin[T]) executePayload(payload string, ctx *MsgContext, db T) error
return command.exec(ctx, db)
}
// Internal helper that runs plugin middlewares in order.
func (p *Plugin[T]) executeMiddlewares(ctx *MsgContext, db T) bool {
func (p *Plugin[T]) executeMiddlewares(ctx *MessageContext, db T) bool {
for _, m := range p.middlewares {
if !m.Execute(ctx, db) {
return false
@@ -398,14 +290,14 @@ func (p *Plugin[T]) executeMiddlewares(ctx *MsgContext, db T) bool {
// MiddlewareExecutor is the function type for middleware logic.
// Returns true to continue execution, false to block it.
// If async, return value is ignored.
type MiddlewareExecutor[T AppData] func(ctx *MsgContext, db T) bool
type MiddlewareExecutor[T AppData] func(ctx *MessageContext, db T) bool
// Middleware represents a reusable execution interceptor.
// Can be synchronous (blocking) or asynchronous (non-blocking).
type Middleware[T AppData] struct {
name string // Human-readable name for logging/debugging
executor MiddlewareExecutor[T] // Function to execute
order int // Optional sort order (not used yet)
order int // Sort order for bot-level middleware ordering
async bool // If true, runs in goroutine and doesn't block
}
@@ -430,12 +322,19 @@ func (m Middleware[T]) SetAsync(async bool) Middleware[T] {
// Execute runs the middleware.
// If async, runs in a goroutine and returns true immediately.
// Otherwise, returns the result of the executor.
func (m Middleware[T]) Execute(ctx *MsgContext, db T) bool {
//
// Async note: the goroutine receives a shallow copy of MessageContext, so
// scalar fields (FromID, ChatID, CallbackQueryID, ...) remain a stable
// snapshot. Pointer and slice fields (Msg, From, Chat, API, Logger, Args)
// continue to share storage with the synchronous flow. Async middleware
// must treat those fields as read-only — mutating them races the sync chain
// that mutates the same context concurrently.
func (m Middleware[T]) Execute(ctx *MessageContext, db T) bool {
if m.async {
ctx := *ctx // copy context to avoid race condition
go func(ctx MsgContext) {
ctxCopy := *ctx
go func(ctx MessageContext) {
m.executor(&ctx, db)
}(ctx)
}(ctxCopy)
return true
}
return m.executor(ctx, db)
+81 -3
View File
@@ -6,7 +6,7 @@ import (
)
func TestValidateArgsRequiresFullMatch(t *testing.T) {
intCmd := NewCommand(func(ctx *MsgContext, db NoData) error { return nil }, "int", NewCommandArg("n").SetValueType(CommandValueIntType).SetRequired())
intCmd := NewCommand("int", func(ctx *MessageContext, db NoData) error { return nil }, NewCommandArg("n").SetValueType(CommandValueInt).SetRequired())
if err := intCmd.validateArgs([]string{"123"}); err != nil {
t.Fatalf("expected valid integer argument, got %v", err)
}
@@ -14,7 +14,7 @@ func TestValidateArgsRequiresFullMatch(t *testing.T) {
t.Fatalf("expected ErrCmdArgRegexpMismatch for partial int match, got %v", err)
}
boolCmd := NewCommand(func(ctx *MsgContext, db NoData) error { return nil }, "bool", NewCommandArg("flag").SetValueType(CommandValueBoolType).SetRequired())
boolCmd := NewCommand("bool", func(ctx *MessageContext, db NoData) error { return nil }, NewCommandArg("flag").SetValueType(CommandValueBool).SetRequired())
if err := boolCmd.validateArgs([]string{"false"}); err != nil {
t.Fatalf("expected valid bool argument, got %v", err)
}
@@ -25,8 +25,8 @@ func TestValidateArgsRequiresFullMatch(t *testing.T) {
func TestValidateArgsEnforcesRequiredArgIndex(t *testing.T) {
cmd := NewCommand(
func(ctx *MsgContext, db NoData) error { return nil },
"mixed",
func(ctx *MessageContext, db NoData) error { return nil },
NewCommandArg("optional"),
NewCommandArg("required").SetRequired(),
)
@@ -38,3 +38,81 @@ func TestValidateArgsEnforcesRequiredArgIndex(t *testing.T) {
t.Fatalf("expected both args to validate, got %v", err)
}
}
func TestCommandGroupBuildsPrefixedCommandsWithoutMutatingOriginal(t *testing.T) {
groupMiddleware := NewMiddleware("group", func(ctx *MessageContext, db NoData) bool { return true })
commandMiddleware := NewMiddleware("command", func(ctx *MessageContext, db NoData) bool { return true })
cmd := NewCommand("ban", func(ctx *MessageContext, db NoData) error { return nil }).
SetDescription("Ban user").
Use(commandMiddleware)
group := NewCommandGroup[NoData]("admin_").
Use(groupMiddleware).
AddCommand(cmd)
built := group.Build()
if len(built) != 1 {
t.Fatalf("expected one command, got %d", len(built))
}
grouped := built[0]
if grouped.command != "admin_ban" {
t.Fatalf("expected prefixed command name, got %q", grouped.command)
}
if grouped.description != "Ban user" {
t.Fatalf("expected description to be copied, got %q", grouped.description)
}
if cmd.command != "ban" {
t.Fatalf("expected original command name to stay unchanged, got %q", cmd.command)
}
if len(cmd.middlewares) != 1 || cmd.middlewares[0].name != "command" {
t.Fatalf("expected original command middleware to stay unchanged, got %#v", cmd.middlewares)
}
if len(grouped.middlewares) != 2 {
t.Fatalf("expected group and command middleware, got %d", len(grouped.middlewares))
}
if grouped.middlewares[0].name != "group" || grouped.middlewares[1].name != "command" {
t.Fatalf("expected group middleware before command middleware, got %q then %q", grouped.middlewares[0].name, grouped.middlewares[1].name)
}
}
func TestCommandGroupBuildIsRepeatable(t *testing.T) {
group := NewCommandGroup[NoData]("admin").
Use(NewMiddleware("group", func(ctx *MessageContext, db NoData) bool { return true })).
AddCommand(NewCommand("ban", func(ctx *MessageContext, db NoData) error { return nil }).
Use(NewMiddleware("command", func(ctx *MessageContext, db NoData) bool { return true })))
first := group.Build()
second := group.Build()
if len(first) != 1 || len(second) != 1 {
t.Fatalf("expected one command from each build, got %d and %d", len(first), len(second))
}
if len(first[0].middlewares) != 2 {
t.Fatalf("expected first build to have two middlewares, got %d", len(first[0].middlewares))
}
if len(second[0].middlewares) != 2 {
t.Fatalf("expected second build to have two middlewares, got %d", len(second[0].middlewares))
}
if first[0] == second[0] {
t.Fatal("expected repeated Build calls to return distinct command copies")
}
}
func TestPluginCommandGroupRegistersBuiltCommands(t *testing.T) {
plugin := NewPlugin[NoData]("admin")
plugin.CommandGroup("admin_", func(group *CommandGroup[NoData]) {
group.AddCommand(NewCommand("ban", func(ctx *MessageContext, db NoData) error { return nil }))
})
if _, ok := plugin.commands["admin_ban"]; !ok {
t.Fatal("expected plugin to register prefixed command")
}
if _, ok := plugin.commands["ban"]; ok {
t.Fatal("expected plugin not to register unprefixed command")
}
plugin.CommandGroup("ignored", nil)
plugin.AddCommandGroup(nil)
}
+15 -18
View File
@@ -8,11 +8,11 @@ import (
)
// Policy defines a reusable authorization rule for the current update context.
type Policy[T AppData] func(ctx *MsgContext, data T) error
type Policy[T AppData] func(ctx *MessageContext, data T) error
// RequirePolicy adapts a Policy into a blocking middleware.
func RequirePolicy[T AppData](name string, p Policy[T]) Middleware[T] {
return NewMiddleware(name, func(ctx *MsgContext, data T) bool {
return NewMiddleware(name, func(ctx *MessageContext, data T) bool {
if err := p(ctx, data); err != nil {
ctx.emitPolicyChecked(PolicyCheckedEvent{
Name: name,
@@ -37,7 +37,7 @@ func RequirePolicy[T AppData](name string, p Policy[T]) Middleware[T] {
// AllPolicies composes policies that all must succeed.
func AllPolicies[T AppData](policies ...Policy[T]) Policy[T] {
return func(ctx *MsgContext, data T) error {
return func(ctx *MessageContext, data T) error {
for _, p := range policies {
if err := p(ctx, data); err != nil {
return err
@@ -49,7 +49,7 @@ func AllPolicies[T AppData](policies ...Policy[T]) Policy[T] {
// AnyPolicy composes policies where at least one must succeed.
func AnyPolicy[T AppData](policies ...Policy[T]) Policy[T] {
return func(ctx *MsgContext, data T) error {
return func(ctx *MessageContext, data T) error {
var firstDeny error
var internalErr error
for _, p := range policies {
@@ -79,7 +79,7 @@ func AnyPolicy[T AppData](policies ...Policy[T]) Policy[T] {
// NotPolicy inverts a policy deny result while preserving internal failures.
func NotPolicy[T AppData](policy Policy[T]) Policy[T] {
return func(ctx *MsgContext, data T) error {
return func(ctx *MessageContext, data T) error {
var err error
if err = policy(ctx, data); err == nil {
return AsUserError(errors.New("the action is not allowed due to policy violation"))
@@ -93,7 +93,7 @@ func NotPolicy[T AppData](policy Policy[T]) Policy[T] {
// RequirePrivateChat allows execution only in private chats.
func RequirePrivateChat[T AppData]() Policy[T] {
return func(ctx *MsgContext, data T) error {
return func(ctx *MessageContext, data T) error {
if ctx.Msg == nil || ctx.Msg.Chat == nil {
return AsInternalError(errors.New("private-chat policy requires message chat context"))
}
@@ -108,7 +108,7 @@ func RequirePrivateChat[T AppData]() Policy[T] {
// RequireGroupChat allows execution only in group or supergroup chats.
func RequireGroupChat[T AppData]() Policy[T] {
return func(ctx *MsgContext, data T) error {
return func(ctx *MessageContext, data T) error {
if ctx.Msg == nil || ctx.Msg.Chat == nil {
return AsInternalError(errors.New("group-chat policy requires message chat context"))
}
@@ -123,7 +123,7 @@ func RequireGroupChat[T AppData]() Policy[T] {
// RequireSupergroupChat allows execution only in supergroup chats.
func RequireSupergroupChat[T AppData]() Policy[T] {
return func(ctx *MsgContext, data T) error {
return func(ctx *MessageContext, data T) error {
if ctx.Msg == nil || ctx.Msg.Chat == nil {
return AsInternalError(errors.New("supergroup-chat policy requires message chat context"))
}
@@ -138,7 +138,7 @@ func RequireSupergroupChat[T AppData]() Policy[T] {
// RequireChatAdmin allows execution only for chat administrators or owners.
func RequireChatAdmin[T AppData]() Policy[T] {
return func(ctx *MsgContext, data T) error {
return func(ctx *MessageContext, data T) error {
if ctx.FromID == 0 || ctx.ChatID == 0 {
return AsInternalError(errors.New("chat-admin policy requires message chat context"))
}
@@ -161,7 +161,7 @@ func RequireChatAdmin[T AppData]() Policy[T] {
// RequireChatCreator allows execution only for the chat owner.
func RequireChatCreator[T AppData]() Policy[T] {
return func(ctx *MsgContext, data T) error {
return func(ctx *MessageContext, data T) error {
if ctx.FromID == 0 || ctx.ChatID == 0 {
return AsInternalError(errors.New("chat-creator policy requires message chat context"))
}
@@ -184,19 +184,16 @@ func RequireChatCreator[T AppData]() Policy[T] {
// RequireBotAdmin allows execution only when the bot is an admin in the chat.
func RequireBotAdmin[T AppData]() Policy[T] {
return func(ctx *MsgContext, data T) error {
return func(ctx *MessageContext, data T) error {
if ctx.ChatID == 0 {
return AsInternalError(errors.New("bot-admin policy requires message chat context"))
}
bot, err := ctx.API.GetMe()
if err != nil {
return AsInternalError(fmt.Errorf("failed to fetch bot info: %w", err))
if ctx.botID == 0 {
return AsInternalError(errors.New("bot ID is not set in context"))
}
member, err := ctx.API.GetChatMember(tgapi.GetChatMember{
ChatID: ctx.ChatID,
UserID: bot.ID,
ChatID: ctx.ChatID, UserID: ctx.botID,
})
if err != nil {
return AsInternalError(fmt.Errorf("failed to fetch bot member status: %w", err))
@@ -212,7 +209,7 @@ func RequireBotAdmin[T AppData]() Policy[T] {
// RequireCallbackFromUser allows execution only for callback queries sent by non-bot users.
func RequireCallbackFromUser[T AppData]() Policy[T] {
return func(ctx *MsgContext, data T) error {
return func(ctx *MessageContext, data T) error {
if ctx.Update.CallbackQuery == nil {
return AsInternalError(errors.New("callback-user policy requires callback query context"))
}
+26 -26
View File
@@ -46,14 +46,14 @@ func TestRequirePolicyStopsExecutionOnDeniedPolicy(t *testing.T) {
}
}()
ctx := &MsgContext{
ctx := &MessageContext{
API: api,
Msg: &tgapi.Message{Chat: &tgapi.Chat{ID: 42, Type: tgapi.ChatTypePrivate}},
Logger: sneklog.NewLogger(),
errorTemplate: "Error: %s",
}
mw := RequirePolicy("deny", func(ctx *MsgContext, data NoData) error {
mw := RequirePolicy("deny", func(ctx *MessageContext, data NoData) error {
return AsUserError(errors.New("blocked"))
})
@@ -69,7 +69,7 @@ func TestRequirePolicyStopsExecutionOnDeniedPolicy(t *testing.T) {
}
func TestRequirePrivateChatAllowsPrivateChat(t *testing.T) {
ctx := &MsgContext{
ctx := &MessageContext{
Msg: &tgapi.Message{
Chat: &tgapi.Chat{ID: 42, Type: tgapi.ChatTypePrivate},
},
@@ -82,7 +82,7 @@ func TestRequirePrivateChatAllowsPrivateChat(t *testing.T) {
}
func TestRequirePrivateChatDeniesNonPrivateChat(t *testing.T) {
ctx := &MsgContext{
ctx := &MessageContext{
Msg: &tgapi.Message{
Chat: &tgapi.Chat{ID: -100, Type: tgapi.ChatTypeSupergroup},
},
@@ -136,7 +136,7 @@ func TestRequireChatAdminUsesNormalizedIDs(t *testing.T) {
}
}()
ctx := &MsgContext{
ctx := &MessageContext{
API: api,
ChatID: -2001,
FromID: 55,
@@ -160,15 +160,15 @@ func TestRequireChatAdminUsesNormalizedIDs(t *testing.T) {
func TestAllPoliciesReturnsFirstError(t *testing.T) {
want := AsUserError(errors.New("blocked"))
policy := AllPolicies(
func(ctx *MsgContext, data NoData) error { return nil },
func(ctx *MsgContext, data NoData) error { return want },
func(ctx *MsgContext, data NoData) error {
func(ctx *MessageContext, data NoData) error { return nil },
func(ctx *MessageContext, data NoData) error { return want },
func(ctx *MessageContext, data NoData) error {
t.Fatal("unexpected evaluation after first failure")
return nil
},
)
err := policy(&MsgContext{Logger: sneklog.NewLogger()}, NoData{})
err := policy(&MessageContext{Logger: sneklog.NewLogger()}, NoData{})
if !errors.Is(err, want) {
t.Fatalf("expected first policy error, got %v", err)
}
@@ -176,11 +176,11 @@ func TestAllPoliciesReturnsFirstError(t *testing.T) {
func TestAnyPolicyAllowsLaterSuccessAfterInternalError(t *testing.T) {
policy := AnyPolicy(
func(ctx *MsgContext, data NoData) error { return AsInternalError(errors.New("temporary")) },
func(ctx *MsgContext, data NoData) error { return nil },
func(ctx *MessageContext, data NoData) error { return AsInternalError(errors.New("temporary")) },
func(ctx *MessageContext, data NoData) error { return nil },
)
if err := policy(&MsgContext{Logger: sneklog.NewLogger()}, NoData{}); err != nil {
if err := policy(&MessageContext{Logger: sneklog.NewLogger()}, NoData{}); err != nil {
t.Fatalf("expected later success to allow access, got %v", err)
}
}
@@ -188,11 +188,11 @@ func TestAnyPolicyAllowsLaterSuccessAfterInternalError(t *testing.T) {
func TestAnyPolicyReturnsInternalErrorWhenNonePass(t *testing.T) {
internal := AsInternalError(errors.New("temporary"))
policy := AnyPolicy(
func(ctx *MsgContext, data NoData) error { return AsUserError(errors.New("denied")) },
func(ctx *MsgContext, data NoData) error { return internal },
func(ctx *MessageContext, data NoData) error { return AsUserError(errors.New("denied")) },
func(ctx *MessageContext, data NoData) error { return internal },
)
err := policy(&MsgContext{Logger: sneklog.NewLogger()}, NoData{})
err := policy(&MessageContext{Logger: sneklog.NewLogger()}, NoData{})
if !errors.Is(err, internal) {
t.Fatalf("expected internal error, got %v", err)
}
@@ -201,29 +201,29 @@ func TestAnyPolicyReturnsInternalErrorWhenNonePass(t *testing.T) {
func TestAnyPolicyReturnsFirstDenyWhenNoPolicyPasses(t *testing.T) {
first := AsUserError(errors.New("first deny"))
policy := AnyPolicy(
func(ctx *MsgContext, data NoData) error { return first },
func(ctx *MsgContext, data NoData) error { return AsUserError(errors.New("second deny")) },
func(ctx *MessageContext, data NoData) error { return first },
func(ctx *MessageContext, data NoData) error { return AsUserError(errors.New("second deny")) },
)
err := policy(&MsgContext{Logger: sneklog.NewLogger()}, NoData{})
err := policy(&MessageContext{Logger: sneklog.NewLogger()}, NoData{})
if !errors.Is(err, first) {
t.Fatalf("expected first deny error, got %v", err)
}
}
func TestNotPolicyInvertsUserDenyButPreservesInternalErrors(t *testing.T) {
inverted := NotPolicy(func(ctx *MsgContext, data NoData) error {
inverted := NotPolicy(func(ctx *MessageContext, data NoData) error {
return AsUserError(errors.New("denied"))
})
if err := inverted(&MsgContext{Logger: sneklog.NewLogger()}, NoData{}); err != nil {
if err := inverted(&MessageContext{Logger: sneklog.NewLogger()}, NoData{}); err != nil {
t.Fatalf("expected inverted deny to succeed, got %v", err)
}
internal := AsInternalError(errors.New("temporary"))
preserve := NotPolicy(func(ctx *MsgContext, data NoData) error {
preserve := NotPolicy(func(ctx *MessageContext, data NoData) error {
return internal
})
err := preserve(&MsgContext{Logger: sneklog.NewLogger()}, NoData{})
err := preserve(&MessageContext{Logger: sneklog.NewLogger()}, NoData{})
if !errors.Is(err, internal) {
t.Fatalf("expected internal error to be preserved, got %v", err)
}
@@ -232,7 +232,7 @@ func TestNotPolicyInvertsUserDenyButPreservesInternalErrors(t *testing.T) {
func TestRequirePolicyEmitsObserverEvents(t *testing.T) {
t.Run("allow", func(t *testing.T) {
observer := &recordingObserver{}
ctx := &MsgContext{
ctx := &MessageContext{
Logger: sneklog.NewLogger(),
ctx: context.Background(),
observer: observer,
@@ -240,7 +240,7 @@ func TestRequirePolicyEmitsObserverEvents(t *testing.T) {
ChatID: 20,
}
mw := RequirePolicy("allow", func(ctx *MsgContext, data NoData) error {
mw := RequirePolicy("allow", func(ctx *MessageContext, data NoData) error {
return nil
})
@@ -257,14 +257,14 @@ func TestRequirePolicyEmitsObserverEvents(t *testing.T) {
t.Run("deny", func(t *testing.T) {
observer := &recordingObserver{}
ctx := &MsgContext{
ctx := &MessageContext{
Logger: sneklog.NewLogger(),
ctx: context.Background(),
observer: observer,
errorTemplate: "%s",
}
mw := RequirePolicy("deny", func(ctx *MsgContext, data NoData) error {
mw := RequirePolicy("deny", func(ctx *MessageContext, data NoData) error {
return AsInternalError(errors.New("blocked"))
})
+61 -75
View File
@@ -12,98 +12,78 @@ type RunnerFn[T AppData] func(*Bot[T]) error
// Runner represents a configurable background or one-time task to be
// executed by a Bot.
//
// Runners are configured using builder methods: Onetime(), Async(), Timeout().
// Once Execute() is called, the Runner should not be modified.
// Runners are configured using builder methods Async and Every. Once the
// bot's runtime has started executing the runner, it should not be modified.
//
// Execution semantics:
// - onetime=true, async=false: Run once synchronously (blocks).
// - onetime=true, async=true: Run once in a goroutine (non-blocking).
// - onetime=false, async=true: Run repeatedly in a goroutine with timeout.
// - onetime=false, async=false: Invalid configuration — ignored with warning.
// - every=0, async=true: Run once in a goroutine (non-blocking, default).
// - every=0, async=false: Run once synchronously (blocks runtime startup).
// - every>0, async=true: Run repeatedly in a goroutine with the given interval.
// - every>0, async=false: Invalid configuration — skipped with a warning.
type Runner[T AppData] struct {
name string // Human-readable name for logging
onetime bool // If true, runs once; if false, runs periodically
async bool // If true, runs in a goroutine; else, runs synchronously
timeout time.Duration // Duration to wait between periodic executions (ignored if onetime=true)
fn RunnerFn[T] // The function to execute
name string // Human-readable name for logging
async bool // If true, runs in a goroutine; else, runs synchronously
every time.Duration // Interval between periodic executions; zero means one-shot
fn RunnerFn[T] // The function to execute
}
// NewRunner creates a new Runner with the given name and function.
// By default, the Runner is configured as async=true (non-blocking).
//
// Builder methods (Onetime, Async, Timeout) can be chained to customize behavior.
// DO NOT call builder methods concurrently or after Execute().
// The default configuration is async=true and every=0, i.e. a one-shot
// goroutine that fires once when the bot runtime starts. Use Async and Every
// to customize this. Do not call builder methods concurrently or after the
// bot runtime has begun executing runners.
func NewRunner[T AppData](name string, fn RunnerFn[T]) Runner[T] {
return Runner[T]{
name: name,
fn: fn,
async: true, // Default: run asynchronously
timeout: 0, // Default: no timeout (ignored if onetime=true)
name: name,
fn: fn,
async: true,
every: 0,
}
}
// Onetime sets whether the runner executes once or repeatedly.
// If true, the runner runs only once.
// If false, the runner runs in a loop with the configured timeout.
func (r Runner[T]) Onetime(onetime bool) Runner[T] {
r.onetime = onetime
return r
}
// Async sets whether the runner executes synchronously or asynchronously.
// If true, the runner runs in a goroutine (non-blocking).
// If false, the runner blocks the caller during execution.
//
// Note: If onetime=false and async=false, the runner will be skipped with a warning.
// Note: periodic runners (Every > 0) require async=true and are skipped with
// a warning when async=false.
func (r Runner[T]) Async(async bool) Runner[T] {
r.async = async
return r
}
// Timeout sets the duration to wait between repeated executions for
// non-onetime runners.
// Every sets the interval between repeated executions of a periodic runner.
//
// If onetime=true, this value is ignored.
// If onetime=false and async=true, this timeout determines the sleep interval
// between loop iterations.
//
// A zero value (time.Duration(0)) is allowed but may trigger a warning
// if used with a background (non-onetime) async runner.
func (r Runner[T]) Timeout(timeout time.Duration) Runner[T] {
r.timeout = timeout
// A zero value (the default) keeps the runner one-shot. A positive value
// schedules the runner to fire repeatedly with the given interval and
// requires async=true; periodic sync runners are skipped with a warning.
func (r Runner[T]) Every(timeout time.Duration) Runner[T] {
r.every = timeout
return r
}
// ExecRunners executes all runners registered on the Bot with context-based lifecycle management.
//
// It logs warnings for misconfigured runners:
// - Sync, non-onetime runners are skipped (invalid configuration).
// - Background (non-onetime, async) runners without a timeout trigger a warning.
//
// Execution logic:
// - onetime + async: Runs once in a goroutine.
// - onetime + sync: Runs once synchronously; warns if slower than 2 seconds.
// - !onetime + async: Runs in a loop with timeout between iterations until ctx.Done().
// - !onetime + sync: Skipped with warning.
// Execution semantics by configuration:
// - every=0, async=true: Runs once in a goroutine (fire and forget).
// - every=0, async=false: Runs once synchronously; warns if slower than 2 seconds.
// - every>0, async=true: Runs in a loop with the configured interval until ctx.Done().
// - every>0, async=false: Skipped with a warning (invalid configuration).
//
// Background runners listen for ctx.Done() and gracefully shut down when the context is canceled.
//
// This method is typically called once during bot startup from RunWithContext or
// RunWebHookWithContext.
// RunWebhookWithContext.
func (bot *Bot[T]) ExecRunners(ctx context.Context) {
bot.logger.Infoln("Executing runners...")
for _, runner := range bot.runners {
// Validate configuration
if !runner.onetime && !runner.async {
bot.logger.Warnf("Runner %s not onetime, but sync — skipping\n", runner.name)
continue
}
if !runner.onetime && runner.async && runner.timeout == 0 {
bot.logger.Warnf("Background runner \"%s\" has no timeout — skipping\n", runner.name)
if runner.every > 0 && !runner.async {
bot.logger.Warnf("Runner %q is periodic but sync; skipping (use Async(true))\n", runner.name)
continue
}
if runner.onetime && runner.async {
if runner.every == 0 && runner.async {
// One-time async: fire and forget
bot.runnerOnceWG.Add(1)
go func(r Runner[T]) {
@@ -126,7 +106,7 @@ func (bot *Bot[T]) ExecRunners(ctx context.Context) {
bot.logger.Warnf("Runner %s failed: %s\n", r.name, err)
}
}(runner)
} else if runner.onetime && !runner.async {
} else if runner.every == 0 && !runner.async {
// One-time sync: block until done
t := time.Now()
err := runner.fn(bot)
@@ -149,39 +129,45 @@ func (bot *Bot[T]) ExecRunners(ctx context.Context) {
if elapsed > time.Second*2 {
bot.logger.Warnf("Runner %s too slow. Elapsed time %v >= 2s\n", runner.name, elapsed)
}
} else if !runner.onetime && runner.async {
} else if runner.every > 0 && runner.async {
// Background loop: periodic execution with graceful shutdown
bot.runnerBgWG.Add(1)
go func(r Runner[T]) {
defer bot.runnerBgWG.Done()
ticker := time.NewTicker(r.timeout)
ticker := time.NewTicker(r.every)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
startedAt := time.Now()
err := r.fn(bot)
bot.safeEmitEvent(ctx, RunnerFinishedEvent{
Name: r.name,
Duration: time.Since(startedAt),
Err: err,
}
// When both ctx.Done() and ticker.C are ready at the same
// time, Go's select picks one at random. Re-check ctx so a
// late tick after cancellation does not fire one extra
// invocation past shutdown.
if ctx.Err() != nil {
return
}
startedAt := time.Now()
err := r.fn(bot)
bot.safeEmitEvent(ctx, RunnerFinishedEvent{
Name: r.name,
Duration: time.Since(startedAt),
Err: err,
})
if err != nil {
bot.safeEmitEvent(ctx, ErrorEvent{
Plugin: "bot",
HandlerKind: HandlerRunnerKind,
HandlerName: r.name,
Err: err,
UserFacing: false,
})
if err != nil {
bot.safeEmitEvent(ctx, ErrorEvent{
Plugin: "bot",
HandlerKind: HandlerRunnerKind,
HandlerName: r.name,
Err: err,
UserFacing: false,
})
bot.logger.Warnf("Runner %s failed: %s\n", r.name, err)
}
bot.logger.Warnf("Runner %s failed: %s\n", r.name, err)
}
}
}(runner)
}
// Note: !onetime && !async is already skipped above
}
}
+4 -4
View File
@@ -14,7 +14,7 @@ type runnerObserver struct {
recordingObserver
}
func TestExecRunnersRunsOnetimeSyncRunner(t *testing.T) {
func TestExecRunnersRunsOnceSyncRunner(t *testing.T) {
var calls atomic.Int32
bot := &Bot[NoData]{
logger: sneklog.NewLogger(),
@@ -22,7 +22,7 @@ func TestExecRunnersRunsOnetimeSyncRunner(t *testing.T) {
NewRunner("sync-once", func(*Bot[NoData]) error {
calls.Add(1)
return nil
}).Onetime(true).Async(false),
}).Async(false),
},
}
@@ -46,7 +46,7 @@ func TestExecRunnersStopsBackgroundRunnerOnCancel(t *testing.T) {
triggered <- struct{}{}
}
return nil
}).Timeout(5 * time.Millisecond),
}).Every(5 * time.Millisecond),
},
}
@@ -76,7 +76,7 @@ func TestExecRunnersEmitObserverEvents(t *testing.T) {
runners: []Runner[NoData]{
NewRunner("sync-once", func(*Bot[NoData]) error {
return wantErr
}).Onetime(true).Async(false),
}).Async(false),
},
}
+39 -26
View File
@@ -2,6 +2,7 @@ package laniakea
import (
"encoding/json"
"maps"
"sync"
)
@@ -10,14 +11,10 @@ type SceneHandler[T any] func(ctx *SceneContext, db T) (SceneResult, error)
// Scene defines a multi-step conversational flow.
type Scene[T any] struct {
// Name identifies the scene in plugin registration and session state.
Name string
// Scope controls how active scene sessions are keyed and shared.
Scope SceneScope
// Entry names the first step used by MsgContext.EnterScene.
Entry string
// PluginName stores the owning plugin name for scene resolution.
PluginName string
name string
scope SceneScope
entry string
pluginName string
steps map[string]SceneHandler[T]
commands map[string]SceneHandler[T]
@@ -28,9 +25,9 @@ type Scene[T any] struct {
// NewScene creates a new scene with user-chat scope by default.
func NewScene[T any](name string) *Scene[T] {
return &Scene[T]{
Name: name,
Scope: SceneScopeUserChat,
Entry: "",
name: name,
scope: SceneScopeUserChat,
entry: "",
steps: make(map[string]SceneHandler[T]),
commands: make(map[string]SceneHandler[T]),
payloads: make(map[string]SceneHandler[T]),
@@ -40,18 +37,18 @@ func NewScene[T any](name string) *Scene[T] {
// SetScope changes how scene sessions are keyed and shared.
func (s *Scene[T]) SetScope(scope SceneScope) *Scene[T] {
s.Scope = scope
s.scope = scope
return s
}
// SetEntry sets the initial step entered by MsgContext.EnterScene.
// SetEntry sets the initial step entered by MessageContext.EnterScene.
func (s *Scene[T]) SetEntry(step string) *Scene[T] {
s.Entry = step
s.entry = step
return s
}
func (s *Scene[T]) setPluginName(name string) *Scene[T] {
s.PluginName = name
s.pluginName = name
return s
}
@@ -111,42 +108,59 @@ func (s *Scene[T]) executeMessage(ctx *SceneContext, db T) (SceneResult, bool, e
return result, true, err
}
func (s *Scene[T]) clone() *Scene[T] {
if s == nil {
return nil
}
cloned := *s
cloned.steps = make(map[string]SceneHandler[T], len(s.steps))
cloned.commands = make(map[string]SceneHandler[T], len(s.commands))
cloned.payloads = make(map[string]SceneHandler[T], len(s.payloads))
maps.Copy(cloned.steps, s.steps)
maps.Copy(cloned.commands, s.commands)
maps.Copy(cloned.payloads, s.payloads)
return &cloned
}
// SceneSession stores the active scene state for one session key.
type SceneSession struct {
// Scene is the registered scene name for the active session.
Scene string
// Step is the current step name inside the active scene.
Step string
// Data stores opaque session payload bytes, typically JSON.
Data []byte
// data stores opaque session payload bytes, typically JSON.
data []byte
}
// SetData stores arbitrary opaque session data.
func (s *SceneSession) SetData(data []byte) {
s.Data = data
s.data = data
}
// GetData returns the raw session data payload.
func (s *SceneSession) GetData() []byte {
return s.Data
return s.data
}
// HasData reports whether the session has a non-empty data payload.
func (s *SceneSession) HasData() bool {
return len(s.Data) > 0
return len(s.data) > 0
}
// ClearData removes any stored session data.
func (s *SceneSession) ClearData() {
s.Data = nil
s.data = nil
}
// BindData unmarshals the stored JSON payload into v.
func (s *SceneSession) BindData(v any) error {
if len(s.Data) == 0 {
if len(s.data) == 0 {
return nil
}
return json.Unmarshal(s.Data, v)
return json.Unmarshal(s.data, v)
}
// SaveData marshals v as JSON and stores it in the session.
@@ -155,7 +169,7 @@ func (s *SceneSession) SaveData(v any) error {
if err != nil {
return err
}
s.Data = data
s.data = data
return nil
}
@@ -242,8 +256,7 @@ type sceneRuntime interface {
getSession(key string) (SceneSession, error)
setSession(key string, session SceneSession) error
deleteSession(key string) error
buildSceneKey(scope SceneScope, ctx *MsgContext) (string, bool)
findSceneSession(ctx *MsgContext) (string, SceneSession, error)
findSceneSession(ctx *MessageContext) (string, SceneSession, error)
}
type sceneMeta struct {
+2 -2
View File
@@ -1,8 +1,8 @@
package laniakea
// SceneContext wraps MsgContext with scene session state for scene handlers.
// SceneContext wraps MessageContext with scene session state for scene handlers.
type SceneContext struct {
*MsgContext
*MessageContext
sess SceneSession
key string
}
+13 -13
View File
@@ -7,10 +7,10 @@ import (
"time"
)
func (bot *Bot[T]) tryHandleScene(ctx *MsgContext) (bool, error) {
func (bot *Bot[T]) tryHandleScene(ctx *MessageContext) (bool, error) {
key, session, err := bot.findSceneSession(ctx)
if err != nil {
if errors.Is(err, ErrCantFindSession) || errors.Is(err, ErrMessageNil) {
if errors.Is(err, ErrCantFindSession) {
return false, nil
}
return false, err
@@ -24,16 +24,16 @@ func (bot *Bot[T]) tryHandleScene(ctx *MsgContext) (bool, error) {
if !ok {
continue
}
if scene.PluginName != "" && scene.PluginName != plugin.name {
if scene.pluginName != "" && scene.pluginName != plugin.name {
continue
}
if !plugin.executeMiddlewares(ctx, bot.appData) {
return false, nil
}
sceneCtx := &SceneContext{
MsgContext: ctx,
sess: session,
key: key,
MessageContext: ctx,
sess: session,
key: key,
}
return bot.executeScene(sceneCtx, scene)
@@ -42,7 +42,7 @@ func (bot *Bot[T]) tryHandleScene(ctx *MsgContext) (bool, error) {
}
func (bot *Bot[T]) executeScene(ctx *SceneContext, scene *Scene[T]) (bool, error) {
if ctx.MsgContext == nil || ctx.sess.Scene == "" {
if ctx.MessageContext == nil || ctx.sess.Scene == "" {
return false, nil
}
@@ -176,7 +176,7 @@ func (bot *Bot[T]) emitSceneStarted(ctx *SceneContext, scene *Scene[T], kind Han
bot.safeEmitEvent(ctx.Context(), HandlerStartedEvent{
UpdateID: ctx.Update.UpdateID,
UpdateType: ctx.Update.Type,
Plugin: scene.PluginName,
Plugin: scene.pluginName,
HandlerKind: kind,
HandlerName: name,
FromID: ctx.FromID,
@@ -188,7 +188,7 @@ func (bot *Bot[T]) emitSceneFinished(ctx *SceneContext, scene *Scene[T], kind Ha
bot.safeEmitEvent(ctx.Context(), HandlerFinishedEvent{
UpdateID: ctx.Update.UpdateID,
UpdateType: ctx.Update.Type,
Plugin: scene.PluginName,
Plugin: scene.pluginName,
HandlerKind: kind,
HandlerName: name,
FromID: ctx.FromID,
@@ -203,7 +203,7 @@ func (bot *Bot[T]) emitSceneError(ctx *SceneContext, scene *Scene[T], kind Handl
bot.safeEmitEvent(ctx.Context(), ErrorEvent{
UpdateID: ctx.Update.UpdateID,
UpdateType: ctx.Update.Type,
Plugin: scene.PluginName,
Plugin: scene.pluginName,
HandlerKind: kind,
HandlerName: name,
FromID: ctx.FromID,
@@ -229,8 +229,8 @@ func (bot *Bot[T]) emitSceneTransition(ctx *SceneContext, scene *Scene[T], from
}
bot.safeEmitEvent(ctx.Context(), SceneTransitionEvent{
Plugin: scene.PluginName,
Scene: scene.Name,
Plugin: scene.pluginName,
Scene: scene.name,
From: from,
To: to,
Action: result.Action,
@@ -269,7 +269,7 @@ func (bot *Bot[T]) applySceneResult(scene *Scene[T], ctx *SceneContext, result S
return false, nil
}
}
func buildSceneKey(scope SceneScope, ctx *MsgContext) (string, bool) {
func buildSceneKey(scope SceneScope, ctx *MessageContext) (string, bool) {
if ctx == nil {
return "", false
}
+49 -49
View File
@@ -36,8 +36,8 @@ func TestPluginAddSceneRegistersScene(t *testing.T) {
if got, ok := plugin.scenes["signup"]; !ok || got != scene {
t.Fatalf("scene was not registered in plugin: ok=%v got=%p want=%p", ok, got, scene)
}
if scene.PluginName != "wizard" {
t.Fatalf("unexpected plugin name on scene: got %q want %q", scene.PluginName, "wizard")
if scene.pluginName != "wizard" {
t.Fatalf("unexpected plugin name on scene: got %q want %q", scene.pluginName, "wizard")
}
}
@@ -45,7 +45,7 @@ func TestBotAddPluginsPreservesScenesAndHandlesThem(t *testing.T) {
called := false
plugin := NewPlugin[NoData]("wizard")
plugin.NewScene("signup").
plugin.Scene("signup").
SetEntry("start").
OnStep("start", func(ctx *SceneContext, db NoData) (SceneResult, error) {
called = true
@@ -71,7 +71,7 @@ func TestBotAddPluginsPreservesScenesAndHandlesThem(t *testing.T) {
t.Fatalf("unexpected scene entry: got %q want %q", sceneMeta.Entry, "start")
}
enterCtx := &MsgContext{
enterCtx := &MessageContext{
Msg: &tgapi.Message{Chat: &tgapi.Chat{ID: 100, Type: tgapi.ChatTypePrivate}},
FromID: 42,
sceneRuntime: bot,
@@ -95,7 +95,7 @@ func TestBotAddPluginsPreservesScenesAndHandlesThem(t *testing.T) {
t.Fatal("expected scene step handler to be called")
}
lookupCtx := &MsgContext{
lookupCtx := &MessageContext{
Msg: &tgapi.Message{Chat: &tgapi.Chat{ID: 100, Type: tgapi.ChatTypePrivate}},
FromID: 42,
}
@@ -108,7 +108,7 @@ func TestBuildSceneKeyRejectsMissingContextFields(t *testing.T) {
tests := []struct {
name string
scope SceneScope
ctx *MsgContext
ctx *MessageContext
}{
{
name: "nil context",
@@ -118,17 +118,17 @@ func TestBuildSceneKeyRejectsMissingContextFields(t *testing.T) {
{
name: "missing message for chat scope",
scope: SceneScopeChat,
ctx: &MsgContext{},
ctx: &MessageContext{},
},
{
name: "missing from id for user scope",
scope: SceneScopeUser,
ctx: &MsgContext{},
ctx: &MessageContext{},
},
{
name: "missing from id for user chat scope",
scope: SceneScopeUserChat,
ctx: &MsgContext{
ctx: &MessageContext{
Msg: &tgapi.Message{Chat: &tgapi.Chat{ID: 100, Type: tgapi.ChatTypePrivate}},
},
},
@@ -146,7 +146,7 @@ func TestBuildSceneKeyRejectsMissingContextFields(t *testing.T) {
func TestEnterSceneRejectsMissingEntryConfiguration(t *testing.T) {
t.Run("empty entry", func(t *testing.T) {
plugin := NewPlugin[NoData]("wizard")
plugin.NewScene("signup")
plugin.Scene("signup")
bot := &Bot[NoData]{
logger: sneklog.NewLogger(),
@@ -155,7 +155,7 @@ func TestEnterSceneRejectsMissingEntryConfiguration(t *testing.T) {
}
bot.AddPlugins(plugin)
ctx := &MsgContext{
ctx := &MessageContext{
Msg: &tgapi.Message{Chat: &tgapi.Chat{ID: 100, Type: tgapi.ChatTypePrivate}},
FromID: 42,
sceneRuntime: bot,
@@ -169,7 +169,7 @@ func TestEnterSceneRejectsMissingEntryConfiguration(t *testing.T) {
t.Run("missing entry step", func(t *testing.T) {
plugin := NewPlugin[NoData]("wizard")
plugin.NewScene("signup").SetEntry("start")
plugin.Scene("signup").SetEntry("start")
bot := &Bot[NoData]{
logger: sneklog.NewLogger(),
@@ -178,7 +178,7 @@ func TestEnterSceneRejectsMissingEntryConfiguration(t *testing.T) {
}
bot.AddPlugins(plugin)
ctx := &MsgContext{
ctx := &MessageContext{
Msg: &tgapi.Message{Chat: &tgapi.Chat{ID: 100, Type: tgapi.ChatTypePrivate}},
FromID: 42,
sceneRuntime: bot,
@@ -192,7 +192,7 @@ func TestEnterSceneRejectsMissingEntryConfiguration(t *testing.T) {
}
func TestSceneContextMethodsRequireRuntime(t *testing.T) {
ctx := &MsgContext{}
ctx := &MessageContext{}
if err := ctx.EnterScene("signup"); !errors.Is(err, ErrSceneRuntimeNil) {
t.Fatalf("expected ErrSceneRuntimeNil from EnterScene, got %v", err)
@@ -210,7 +210,7 @@ func TestSceneCommandHandlerRunsBeforeStep(t *testing.T) {
stepCalled := false
plugin := NewPlugin[NoData]("wizard")
plugin.NewScene("signup").
plugin.Scene("signup").
SetEntry("start").
OnStep("start", func(ctx *SceneContext, db NoData) (SceneResult, error) {
stepCalled = true
@@ -238,7 +238,7 @@ func TestSceneCommandHandlerRunsBeforeStep(t *testing.T) {
}
bot.AddPlugins(plugin)
enterCtx := &MsgContext{
enterCtx := &MessageContext{
Msg: &tgapi.Message{Chat: &tgapi.Chat{ID: 100, Type: tgapi.ChatTypePrivate}},
FromID: 42,
sceneRuntime: bot,
@@ -269,7 +269,7 @@ func TestSceneCommandHandlerRunsBeforeStep(t *testing.T) {
func TestSceneCommandObserverEmitsLifecycleEvents(t *testing.T) {
observer := &recordingObserver{}
plugin := NewPlugin[NoData]("wizard")
plugin.NewScene("signup").
plugin.Scene("signup").
SetEntry("start").
OnStep("start", func(ctx *SceneContext, db NoData) (SceneResult, error) {
return ctx.Stay(), nil
@@ -287,7 +287,7 @@ func TestSceneCommandObserverEmitsLifecycleEvents(t *testing.T) {
}
bot.AddPlugins(plugin)
enterCtx := &MsgContext{
enterCtx := &MessageContext{
Msg: &tgapi.Message{Chat: &tgapi.Chat{ID: 100, Type: tgapi.ChatTypePrivate}},
FromID: 42,
sceneRuntime: bot,
@@ -324,7 +324,7 @@ func TestSceneCommandObserverEmitsLifecycleEvents(t *testing.T) {
func TestSceneStepObserverEmitsLifecycleEvents(t *testing.T) {
observer := &recordingObserver{}
plugin := NewPlugin[NoData]("wizard")
plugin.NewScene("signup").
plugin.Scene("signup").
SetEntry("start").
OnStep("start", func(ctx *SceneContext, db NoData) (SceneResult, error) {
return ctx.Stay(), nil
@@ -339,7 +339,7 @@ func TestSceneStepObserverEmitsLifecycleEvents(t *testing.T) {
}
bot.AddPlugins(plugin)
enterCtx := &MsgContext{
enterCtx := &MessageContext{
Msg: &tgapi.Message{Chat: &tgapi.Chat{ID: 100, Type: tgapi.ChatTypePrivate}},
FromID: 42,
sceneRuntime: bot,
@@ -376,7 +376,7 @@ func TestSceneStepObserverEmitsLifecycleEvents(t *testing.T) {
func TestSceneMessageObserverEmitsLifecycleEvents(t *testing.T) {
observer := &recordingObserver{}
plugin := NewPlugin[NoData]("wizard")
scene := plugin.NewScene("signup").
scene := plugin.Scene("signup").
SetEntry("start").
OnStep("start", func(ctx *SceneContext, db NoData) (SceneResult, error) {
return ctx.Stay(), nil
@@ -394,14 +394,14 @@ func TestSceneMessageObserverEmitsLifecycleEvents(t *testing.T) {
}
bot.AddPlugins(plugin)
key, ok := buildSceneKey(SceneScopeUserChat, &MsgContext{
key, ok := buildSceneKey(SceneScopeUserChat, &MessageContext{
Msg: &tgapi.Message{Chat: &tgapi.Chat{ID: 100, Type: tgapi.ChatTypePrivate}},
FromID: 42,
})
if !ok {
t.Fatal("expected scene key to be built")
}
if err := bot.sessionStore.Set(key, SceneSession{Scene: scene.Name}); err != nil {
if err := bot.sessionStore.Set(key, SceneSession{Scene: scene.name}); err != nil {
t.Fatalf("failed to seed scene session: %v", err)
}
@@ -435,7 +435,7 @@ func TestScenePayloadHandlerRunsBeforeStep(t *testing.T) {
stepCalled := false
plugin := NewPlugin[NoData]("wizard")
plugin.NewScene("signup").
plugin.Scene("signup").
SetEntry("start").
OnStep("start", func(ctx *SceneContext, db NoData) (SceneResult, error) {
stepCalled = true
@@ -460,7 +460,7 @@ func TestScenePayloadHandlerRunsBeforeStep(t *testing.T) {
}
bot.AddPlugins(plugin)
enterCtx := &MsgContext{
enterCtx := &MessageContext{
Msg: &tgapi.Message{Chat: &tgapi.Chat{ID: 100, Type: tgapi.ChatTypePrivate}},
FromID: 42,
sceneRuntime: bot,
@@ -499,7 +499,7 @@ func TestScenePayloadHandlerRunsBeforeStep(t *testing.T) {
func TestScenePayloadObserverEmitsLifecycleEvents(t *testing.T) {
observer := &recordingObserver{}
plugin := NewPlugin[NoData]("wizard")
plugin.NewScene("signup").
plugin.Scene("signup").
SetEntry("start").
OnStep("start", func(ctx *SceneContext, db NoData) (SceneResult, error) {
return ctx.Stay(), nil
@@ -517,7 +517,7 @@ func TestScenePayloadObserverEmitsLifecycleEvents(t *testing.T) {
}
bot.AddPlugins(plugin)
enterCtx := &MsgContext{
enterCtx := &MessageContext{
Msg: &tgapi.Message{Chat: &tgapi.Chat{ID: 100, Type: tgapi.ChatTypePrivate}},
FromID: 42,
sceneRuntime: bot,
@@ -563,8 +563,8 @@ func TestSceneUnmatchedPayloadFallsThroughWithoutRunningStep(t *testing.T) {
stepCalled := false
plugin := NewPlugin[NoData]("wizard")
plugin.NewPayload(func(ctx *MsgContext, db NoData) error { return nil }, "ping")
plugin.NewScene("signup").
plugin.Payload("ping", func(ctx *MessageContext, db NoData) error { return nil })
plugin.Scene("signup").
SetEntry("start").
OnStep("start", func(ctx *SceneContext, db NoData) (SceneResult, error) {
stepCalled = true
@@ -579,7 +579,7 @@ func TestSceneUnmatchedPayloadFallsThroughWithoutRunningStep(t *testing.T) {
}
bot.AddPlugins(plugin)
enterCtx := &MsgContext{
enterCtx := &MessageContext{
Msg: &tgapi.Message{Chat: &tgapi.Chat{ID: 100, Type: tgapi.ChatTypePrivate}},
FromID: 42,
sceneRuntime: bot,
@@ -588,7 +588,7 @@ func TestSceneUnmatchedPayloadFallsThroughWithoutRunningStep(t *testing.T) {
t.Fatalf("EnterScene returned error: %v", err)
}
key, ok := buildSceneKey(SceneScopeUserChat, &MsgContext{
key, ok := buildSceneKey(SceneScopeUserChat, &MessageContext{
Msg: &tgapi.Message{Chat: &tgapi.Chat{ID: 100, Type: tgapi.ChatTypePrivate}},
FromID: 42,
})
@@ -632,11 +632,11 @@ func TestScenePassDoesNotPersistSessionData(t *testing.T) {
commandCalled := false
plugin := NewPlugin[NoData]("wizard")
plugin.NewCommand(func(ctx *MsgContext, db NoData) error {
plugin.Command("ping", func(ctx *MessageContext, db NoData) error {
commandCalled = true
return nil
}, "ping")
plugin.NewScene("signup").
})
plugin.Scene("signup").
SetEntry("start").
OnStep("start", func(ctx *SceneContext, db NoData) (SceneResult, error) {
if err := ctx.SaveData(struct {
@@ -655,7 +655,7 @@ func TestScenePassDoesNotPersistSessionData(t *testing.T) {
}
bot.AddPlugins(plugin)
enterCtx := &MsgContext{
enterCtx := &MessageContext{
Msg: &tgapi.Message{Chat: &tgapi.Chat{ID: 100, Type: tgapi.ChatTypePrivate}},
FromID: 42,
sceneRuntime: bot,
@@ -664,7 +664,7 @@ func TestScenePassDoesNotPersistSessionData(t *testing.T) {
t.Fatalf("EnterScene returned error: %v", err)
}
key, ok := buildSceneKey(SceneScopeUserChat, &MsgContext{
key, ok := buildSceneKey(SceneScopeUserChat, &MessageContext{
Msg: &tgapi.Message{Chat: &tgapi.Chat{ID: 100, Type: tgapi.ChatTypePrivate}},
FromID: 42,
})
@@ -712,16 +712,16 @@ func TestSceneUnmatchedCommandFallsThroughWithoutRunningStep(t *testing.T) {
stepCalled := false
plugin := NewPlugin[NoData]("wizard")
plugin.NewScene("signup").
plugin.Scene("signup").
SetEntry("start").
OnStep("start", func(ctx *SceneContext, db NoData) (SceneResult, error) {
stepCalled = true
return ctx.Stay(), nil
})
plugin.NewCommand(func(ctx *MsgContext, db NoData) error {
plugin.Command("ping", func(ctx *MessageContext, db NoData) error {
commandCalled = true
return nil
}, "ping")
})
bot := &Bot[NoData]{
logger: sneklog.NewLogger(),
@@ -731,7 +731,7 @@ func TestSceneUnmatchedCommandFallsThroughWithoutRunningStep(t *testing.T) {
}
bot.AddPlugins(plugin)
enterCtx := &MsgContext{
enterCtx := &MessageContext{
Msg: &tgapi.Message{Chat: &tgapi.Chat{ID: 100, Type: tgapi.ChatTypePrivate}},
FromID: 42,
sceneRuntime: bot,
@@ -740,7 +740,7 @@ func TestSceneUnmatchedCommandFallsThroughWithoutRunningStep(t *testing.T) {
t.Fatalf("EnterScene returned error: %v", err)
}
key, ok := buildSceneKey(SceneScopeUserChat, &MsgContext{
key, ok := buildSceneKey(SceneScopeUserChat, &MessageContext{
Msg: &tgapi.Message{Chat: &tgapi.Chat{ID: 100, Type: tgapi.ChatTypePrivate}},
FromID: 42,
})
@@ -779,7 +779,7 @@ func TestSceneMessageFallbackRunsWhenNoCommandOrStepMatch(t *testing.T) {
fallbackCalled := false
plugin := NewPlugin[NoData]("wizard")
plugin.NewScene("signup").
plugin.Scene("signup").
SetEntry("start").
OnStep("start", func(ctx *SceneContext, db NoData) (SceneResult, error) {
return ctx.Stay(), nil
@@ -800,7 +800,7 @@ func TestSceneMessageFallbackRunsWhenNoCommandOrStepMatch(t *testing.T) {
}
bot.AddPlugins(plugin)
enterCtx := &MsgContext{
enterCtx := &MessageContext{
Msg: &tgapi.Message{Chat: &tgapi.Chat{ID: 100, Type: tgapi.ChatTypePrivate}},
FromID: 42,
sceneRuntime: bot,
@@ -809,7 +809,7 @@ func TestSceneMessageFallbackRunsWhenNoCommandOrStepMatch(t *testing.T) {
t.Fatalf("EnterScene returned error: %v", err)
}
key, ok := buildSceneKey(SceneScopeUserChat, &MsgContext{
key, ok := buildSceneKey(SceneScopeUserChat, &MessageContext{
Msg: &tgapi.Message{Chat: &tgapi.Chat{ID: 100, Type: tgapi.ChatTypePrivate}},
FromID: 42,
})
@@ -847,7 +847,7 @@ func TestFindSceneSessionSupportsUserScopeWithoutMessage(t *testing.T) {
t.Fatalf("Set returned error: %v", err)
}
key, session, err := bot.findSceneSession(&MsgContext{FromID: 42})
key, session, err := bot.findSceneSession(&MessageContext{FromID: 42})
if err != nil {
t.Fatalf("findSceneSession returned error: %v", err)
}
@@ -870,7 +870,7 @@ func TestSceneStoreErrorsPropagate(t *testing.T) {
sceneScopePriority: []SceneScope{SceneScopeUser},
}
_, _, err := bot.findSceneSession(&MsgContext{FromID: 42})
_, _, err := bot.findSceneSession(&MessageContext{FromID: 42})
if !errors.Is(err, getErr) {
t.Fatalf("expected getErr, got %v", err)
}
@@ -887,9 +887,9 @@ func TestSceneStoreErrorsPropagate(t *testing.T) {
}
_, err := bot.applySceneResult(scene, &SceneContext{
MsgContext: &MsgContext{},
sess: SceneSession{Scene: "signup", Step: "start"},
key: "user_id:42:chat_id:100",
MessageContext: &MessageContext{},
sess: SceneSession{Scene: "signup", Step: "start"},
key: "user_id:42:chat_id:100",
}, SceneResult{Action: SceneActionStay})
if !errors.Is(err, setErr) {
t.Fatalf("expected setErr, got %v", err)
+17 -5
View File
@@ -64,10 +64,13 @@ func (opts *APIOpts) SetAPIURL(apiURL string) *APIOpts {
return opts
}
// SetLogFormat sets the output format used by API-managed loggers.
func (opts *APIOpts) SetLogFormat(format utils.LogFormat) *APIOpts {
opts.logFormat = format
return opts
}
// SetLogFormatter sets the formatter used by API-managed logger writers.
func (opts *APIOpts) SetLogFormatter(formatter *sneklog.Formatter) *APIOpts {
opts.logFormatter = formatter
return opts
@@ -80,10 +83,10 @@ func (opts *APIOpts) SetLimiter(limiter *utils.RateLimiter) *APIOpts {
return opts
}
// SetLimiterDrop enables "drop mode" for rate limiting.
// SetDropRateLimitOverflow enables "drop mode" for rate limiting.
// If true, requests exceeding limits return ErrDropOverflow immediately.
// If false, requests block until capacity is available.
func (opts *APIOpts) SetLimiterDrop(b bool) *APIOpts {
func (opts *APIOpts) SetDropRateLimitOverflow(b bool) *APIOpts {
opts.dropOverflowLimit = b
return opts
}
@@ -119,6 +122,7 @@ func NewAPI(opts *APIOpts) *API {
"API", utils.GetLoggerLevel(),
opts.logFormat, opts.logFormatter,
)
logger.AddReplacer(opts.token, "<TOKEN>")
client := opts.client
if client == nil {
@@ -252,6 +256,12 @@ func (r TelegramRequest[R, P]) doRequest(ctx context.Context, api *API) (R, erro
}
if !response.Ok {
responseErr := &ResponseError{
Code: response.ErrorCode,
Description: response.Description,
Parameters: response.Parameters,
}
// Handle rate limiting (429)
if response.ErrorCode == 429 && response.Parameters != nil && response.Parameters.RetryAfter != nil {
after := *response.Parameters.RetryAfter
@@ -266,6 +276,10 @@ func (r TelegramRequest[R, P]) doRequest(ctx context.Context, api *API) (R, erro
}
}
if r.method == "getUpdates" {
return zero, responseErr
}
// Wait and retry
select {
case <-ctx.Done():
@@ -276,7 +290,7 @@ func (r TelegramRequest[R, P]) doRequest(ctx context.Context, api *API) (R, erro
}
// Other API errors
return zero, fmt.Errorf("[%d] %s", response.ErrorCode, response.Description)
return zero, responseErr
}
return response.Result, nil
@@ -315,13 +329,11 @@ func (r TelegramRequest[R, P]) Do(api *API) (R, error) {
return r.DoWithContext(context.Background(), api)
}
// Internal helper that reads and caps a Telegram response body.
func readBody(body io.ReadCloser) ([]byte, error) {
reader := io.LimitReader(body, 10<<20) // 10 MB
return io.ReadAll(reader)
}
// Internal helper that parses a typed Telegram API response body.
func parseBody[R any](data []byte) (TelegramResponse[R], error) {
var resp TelegramResponse[R]
err := json.Unmarshal(data, &resp)
+70
View File
@@ -3,6 +3,7 @@ package tgapi
import "context"
// SendPhoto holds parameters for the sendPhoto method.
// Since: Bot API 1.0
// See https://core.telegram.org/bots/api#sendphoto
type SendPhoto struct {
BusinessConnectionID string `json:"business_connection_id,omitempty"`
@@ -28,6 +29,7 @@ type SendPhoto struct {
}
// SendPhoto sends a photo.
// Since: Bot API 1.0
// See https://core.telegram.org/bots/api#sendphoto
func (api *API) SendPhoto(params SendPhoto) (Message, error) {
req := NewRequestWithChatID[Message]("sendPhoto", params, params.ChatID)
@@ -35,6 +37,7 @@ func (api *API) SendPhoto(params SendPhoto) (Message, error) {
}
// SendPhotoWithContext is the context-aware variant of SendPhoto.
// Since: Bot API 1.0
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#sendphoto
func (api *API) SendPhotoWithContext(ctx context.Context, params SendPhoto) (Message, error) {
@@ -43,6 +46,7 @@ func (api *API) SendPhotoWithContext(ctx context.Context, params SendPhoto) (Mes
}
// SendAudio holds parameters for the sendAudio method.
// Since: Bot API 1.2
// See https://core.telegram.org/bots/api#sendaudio
type SendAudio struct {
BusinessConnectionID string `json:"business_connection_id,omitempty"`
@@ -70,6 +74,7 @@ type SendAudio struct {
}
// SendAudio sends an audio file.
// Since: Bot API 1.2
// See https://core.telegram.org/bots/api#sendaudio
func (api *API) SendAudio(params SendAudio) (Message, error) {
req := NewRequestWithChatID[Message]("sendAudio", params, params.ChatID)
@@ -77,6 +82,7 @@ func (api *API) SendAudio(params SendAudio) (Message, error) {
}
// SendAudioWithContext is the context-aware variant of SendAudio.
// Since: Bot API 1.2
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#sendaudio
func (api *API) SendAudioWithContext(ctx context.Context, params SendAudio) (Message, error) {
@@ -85,6 +91,7 @@ func (api *API) SendAudioWithContext(ctx context.Context, params SendAudio) (Mes
}
// SendDocument holds parameters for the sendDocument method.
// Since: Bot API 1.0
// See https://core.telegram.org/bots/api#senddocument
type SendDocument struct {
BusinessConnectionID string `json:"business_connection_id,omitempty"`
@@ -110,6 +117,7 @@ type SendDocument struct {
}
// SendDocument sends a document.
// Since: Bot API 1.0
// See https://core.telegram.org/bots/api#senddocument
func (api *API) SendDocument(params SendDocument) (Message, error) {
req := NewRequestWithChatID[Message]("sendDocument", params, params.ChatID)
@@ -117,6 +125,7 @@ func (api *API) SendDocument(params SendDocument) (Message, error) {
}
// SendDocumentWithContext is the context-aware variant of SendDocument.
// Since: Bot API 1.0
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#senddocument
func (api *API) SendDocumentWithContext(ctx context.Context, params SendDocument) (Message, error) {
@@ -125,6 +134,7 @@ func (api *API) SendDocumentWithContext(ctx context.Context, params SendDocument
}
// SendVideo holds parameters for the sendVideo method.
// Since: Bot API 1.0
// See https://core.telegram.org/bots/api#sendvideo
type SendVideo struct {
BusinessConnectionID string `json:"business_connection_id,omitempty"`
@@ -158,6 +168,7 @@ type SendVideo struct {
}
// SendVideo sends a video.
// Since: Bot API 1.0
// See https://core.telegram.org/bots/api#sendvideo
func (api *API) SendVideo(params SendVideo) (Message, error) {
req := NewRequestWithChatID[Message]("sendVideo", params, params.ChatID)
@@ -165,6 +176,7 @@ func (api *API) SendVideo(params SendVideo) (Message, error) {
}
// SendVideoWithContext is the context-aware variant of SendVideo.
// Since: Bot API 1.0
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#sendvideo
func (api *API) SendVideoWithContext(ctx context.Context, params SendVideo) (Message, error) {
@@ -173,6 +185,7 @@ func (api *API) SendVideoWithContext(ctx context.Context, params SendVideo) (Mes
}
// SendAnimation holds parameters for the sendAnimation method.
// Since: Bot API 4.0
// See https://core.telegram.org/bots/api#sendanimation
type SendAnimation struct {
BusinessConnectionID string `json:"business_connection_id,omitempty"`
@@ -202,6 +215,7 @@ type SendAnimation struct {
}
// SendAnimation sends an animation file (GIF or H.264/MPEG-4 AVC video without sound).
// Since: Bot API 4.0
// See https://core.telegram.org/bots/api#sendanimation
func (api *API) SendAnimation(params SendAnimation) (Message, error) {
req := NewRequestWithChatID[Message]("sendAnimation", params, params.ChatID)
@@ -209,6 +223,7 @@ func (api *API) SendAnimation(params SendAnimation) (Message, error) {
}
// SendAnimationWithContext is the context-aware variant of SendAnimation.
// Since: Bot API 4.0
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#sendanimation
func (api *API) SendAnimationWithContext(ctx context.Context, params SendAnimation) (Message, error) {
@@ -217,6 +232,7 @@ func (api *API) SendAnimationWithContext(ctx context.Context, params SendAnimati
}
// SendVoice holds parameters for the sendVoice method.
// Since: Bot API 1.2
// See https://core.telegram.org/bots/api#sendvoice
type SendVoice struct {
BusinessConnectionID string `json:"business_connection_id,omitempty"`
@@ -240,6 +256,7 @@ type SendVoice struct {
}
// SendVoice sends a voice note.
// Since: Bot API 1.2
// See https://core.telegram.org/bots/api#sendvoice
func (api *API) SendVoice(params SendVoice) (Message, error) {
req := NewRequestWithChatID[Message]("sendVoice", params, params.ChatID)
@@ -247,6 +264,7 @@ func (api *API) SendVoice(params SendVoice) (Message, error) {
}
// SendVoiceWithContext is the context-aware variant of SendVoice.
// Since: Bot API 1.2
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#sendvoice
func (api *API) SendVoiceWithContext(ctx context.Context, params SendVoice) (Message, error) {
@@ -255,6 +273,7 @@ func (api *API) SendVoiceWithContext(ctx context.Context, params SendVoice) (Mes
}
// SendVideoNote holds parameters for the sendVideoNote method.
// Since: Bot API 3.0
// See https://core.telegram.org/bots/api#sendvideonote
type SendVideoNote struct {
BusinessConnectionID string `json:"business_connection_id,omitempty"`
@@ -277,6 +296,7 @@ type SendVideoNote struct {
}
// SendVideoNote sends a video note (rounded video message).
// Since: Bot API 3.0
// See https://core.telegram.org/bots/api#sendvideonote
func (api *API) SendVideoNote(params SendVideoNote) (Message, error) {
req := NewRequestWithChatID[Message]("sendVideoNote", params, params.ChatID)
@@ -284,6 +304,7 @@ func (api *API) SendVideoNote(params SendVideoNote) (Message, error) {
}
// SendVideoNoteWithContext is the context-aware variant of SendVideoNote.
// Since: Bot API 3.0
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#sendvideonote
func (api *API) SendVideoNoteWithContext(ctx context.Context, params SendVideoNote) (Message, error) {
@@ -292,6 +313,7 @@ func (api *API) SendVideoNoteWithContext(ctx context.Context, params SendVideoNo
}
// SendPaidMedia holds parameters for the sendPaidMedia method.
// Since: Bot API 7.6
// See https://core.telegram.org/bots/api#sendpaidmedia
type SendPaidMedia struct {
BusinessConnectionID string `json:"business_connection_id,omitempty"`
@@ -316,6 +338,7 @@ type SendPaidMedia struct {
}
// SendPaidMedia sends paid media.
// Since: Bot API 7.6
// See https://core.telegram.org/bots/api#sendpaidmedia
func (api *API) SendPaidMedia(params SendPaidMedia) (Message, error) {
req := NewRequestWithChatID[Message]("sendPaidMedia", params, params.ChatID)
@@ -323,6 +346,7 @@ func (api *API) SendPaidMedia(params SendPaidMedia) (Message, error) {
}
// SendPaidMediaWithContext is the context-aware variant of SendPaidMedia.
// Since: Bot API 7.6
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#sendpaidmedia
func (api *API) SendPaidMediaWithContext(ctx context.Context, params SendPaidMedia) (Message, error) {
@@ -331,6 +355,7 @@ func (api *API) SendPaidMediaWithContext(ctx context.Context, params SendPaidMed
}
// SendMediaGroup holds parameters for the sendMediaGroup method.
// Since: Bot API 3.5
// See https://core.telegram.org/bots/api#sendmediagroup
type SendMediaGroup struct {
BusinessConnectionID string `json:"business_connection_id,omitempty"`
@@ -347,6 +372,7 @@ type SendMediaGroup struct {
}
// SendMediaGroup sends a group of photos, videos, documents or audios as an album.
// Since: Bot API 3.5
// See https://core.telegram.org/bots/api#sendmediagroup
func (api *API) SendMediaGroup(params SendMediaGroup) ([]Message, error) {
req := NewRequestWithChatID[[]Message]("sendMediaGroup", params, params.ChatID)
@@ -354,9 +380,53 @@ func (api *API) SendMediaGroup(params SendMediaGroup) ([]Message, error) {
}
// SendMediaGroupWithContext is the context-aware variant of SendMediaGroup.
// Since: Bot API 3.5
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#sendmediagroup
func (api *API) SendMediaGroupWithContext(ctx context.Context, params SendMediaGroup) ([]Message, error) {
req := NewRequestWithChatID[[]Message]("sendMediaGroup", params, params.ChatID)
return req.DoWithContext(ctx, api)
}
// SendLivePhoto holds parameters for the sendLivePhoto method.
// Since: Bot API 10.0
// See https://core.telegram.org/bots/api#sendlivephoto
type SendLivePhoto struct {
BusinessConnectionID string `json:"business_connection_id,omitempty"`
ChatID int64 `json:"chat_id"`
MessageThreadID int `json:"message_thread_id,omitempty"`
DirectMessagesTopicID int `json:"direct_messages_topic_id,omitempty"`
LivePhoto string `json:"live_photo"`
Caption string `json:"caption,omitempty"`
ParseMode ParseMode `json:"parse_mode,omitempty"`
CaptionEntities []MessageEntity `json:"caption_entities,omitempty"`
ShowCaptionAboveMedia bool `json:"show_caption_above_media,omitempty"`
HasSpoiler bool `json:"has_spoiler,omitempty"`
DisableNotification bool `json:"disable_notification,omitempty"`
ProtectContent bool `json:"protect_content,omitempty"`
AllowPaidBroadcast bool `json:"allow_paid_broadcast,omitempty"`
MessageEffectID string `json:"message_effect_id,omitempty"`
SuggestedPostParameters *SuggestedPostParameters `json:"suggested_post_parameters,omitempty"`
ReplyParameters *ReplyParameters `json:"reply_parameters,omitempty"`
ReplyMarkup *ReplyMarkup `json:"reply_markup,omitempty"`
}
// SendLivePhoto sends a live photo.
// Since: Bot API 10.0
// See https://core.telegram.org/bots/api#sendlivephoto
func (api *API) SendLivePhoto(params SendLivePhoto) (Message, error) {
req := NewRequestWithChatID[Message]("sendLivePhoto", params, params.ChatID)
return req.Do(api)
}
// SendLivePhotoWithContext is the context-aware variant of SendLivePhoto.
// Since: Bot API 10.0
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#sendlivephoto
func (api *API) SendLivePhotoWithContext(ctx context.Context, params SendLivePhoto) (Message, error) {
req := NewRequestWithChatID[Message]("sendLivePhoto", params, params.ChatID)
return req.DoWithContext(ctx, api)
}
+151 -36
View File
@@ -1,5 +1,7 @@
package tgapi
// Animation represents an animation file (GIF or H.264/MPEG-4 AVC without sound).
// Since: Bot API 4.0
type Animation struct {
FileID string `json:"file_id"`
FileUniqueID string `json:"file_unique_id"`
@@ -14,6 +16,7 @@ type Animation struct {
}
// Audio represents an audio file to be treated as music by the Telegram clients.
// Since: Bot API 1.2
// See https://core.telegram.org/bots/api#audio
type Audio struct {
FileID string `json:"file_id"`
@@ -22,12 +25,14 @@ type Audio struct {
Performer string `json:"performer,omitempty"`
Title string `json:"title,omitempty"`
FileName string `json:"file_name,omitempty"`
FileName string `json:"file_name,omitempty"` // Since: Bot API 5.0
MimeType string `json:"mime_type,omitempty"`
FileSize int64 `json:"file_size,omitempty"`
Thumbnail *PhotoSize `json:"thumbnail,omitempty"`
}
// Document represents a general file (as opposed to photos, voice messages and audio files).
// Since: Bot API 1.0
type Document struct {
FileID string `json:"file_id"`
FileUniqueID string `json:"file_unique_id"`
@@ -38,11 +43,14 @@ type Document struct {
}
// Story represents a story.
// Since: Bot API 6.8
type Story struct {
Chat Chat `json:"chat"`
ID int `json:"id"`
}
// Video represents a video file.
// Since: Bot API 1.0
type Video struct {
FileID string `json:"file_id"`
FileUniqueID string `json:"file_unique_id"`
@@ -51,15 +59,16 @@ type Video struct {
Duration int `json:"duration"`
Thumbnail *PhotoSize `json:"thumbnail,omitempty"`
Cover []PhotoSize `json:"cover,omitempty"`
StartTimestamp int64 `json:"start_timestamp"`
Qualities []VideoQuality `json:"qualities,omitempty"`
Cover []PhotoSize `json:"cover,omitempty"` // Since: Bot API 8.3
StartTimestamp int64 `json:"start_timestamp"` // Since: Bot API 8.3
Qualities []VideoQuality `json:"qualities,omitempty"` // Since: Bot API 9.4
FileName string `json:"file_name,omitempty"`
MimeType string `json:"mime_type,omitempty"`
FileSize int64 `json:"file_size,omitempty"`
}
// VideoQuality describes an alternative quality for a video.
// Since: Bot API 9.4
// See https://core.telegram.org/bots/api#videoquality
type VideoQuality struct {
FileID string `json:"file_id"`
@@ -70,6 +79,8 @@ type VideoQuality struct {
FileSize int64 `json:"file_size,omitempty"`
}
// VideoNote represents a video message.
// Since: Bot API 3.0
type VideoNote struct {
FileID string `json:"file_id"`
FileUniqueID string `json:"file_unique_id"`
@@ -79,6 +90,8 @@ type VideoNote struct {
FileSize int64 `json:"file_size,omitempty"`
}
// Voice represents a voice note.
// Since: Bot API 1.2
type Voice struct {
FileID string `json:"file_id"`
FileUniqueID string `json:"file_unique_id"`
@@ -87,18 +100,26 @@ type Voice struct {
FileSize int `json:"file_size,omitempty"`
}
// PaidMediaInfo describes paid media.
// Since: Bot API 7.6
type PaidMediaInfo struct {
StarCount int `json:"star_count"`
PaidMedia []PaidMedia `json:"paid_media"`
}
// PaidMediaType represents the type of paid media.
// Since: Bot API 7.6
type PaidMediaType string
const (
PaidMediaPreviewType PaidMediaType = "preview"
PaidMediaPhotoType PaidMediaType = "photo"
PaidMediaVideoType PaidMediaType = "video"
PaidMediaPreviewType PaidMediaType = "preview"
PaidMediaPhotoType PaidMediaType = "photo"
PaidMediaVideoType PaidMediaType = "video"
PaidMediaLivePhotoType PaidMediaType = "live_photo" // Since: Bot API 10.0
)
// PaidMedia describes paid media content.
// Since: Bot API 7.6
type PaidMedia struct {
Type PaidMediaType `json:"type,omitempty"`
@@ -108,9 +129,12 @@ type PaidMedia struct {
Photo []PhotoSize `json:"photo,omitempty"`
Video *Video `json:"video,omitempty"`
Video *Video `json:"video,omitempty"`
LivePhoto *LivePhoto `json:"live_photo,omitempty"` // Since: Bot API 10.0
}
// Contact represents a phone contact.
// Since: Bot API 1.0
type Contact struct {
PhoneNumber string `json:"phone_number"`
FirstName string `json:"first_name"`
@@ -119,32 +143,56 @@ type Contact struct {
Vcard string `json:"vcard,omitempty"`
}
// Dice represents an animated emoji with a random value.
// Since: Bot API 4.7
type Dice struct {
Emoji string `json:"emoji"`
Value int `json:"value"`
}
// PollOption contains information about one answer option in a poll.
// Since: Bot API 4.2
// See https://core.telegram.org/bots/api#polloption
type PollOption struct {
PersistentID string `json:"persistent_id"`
PersistentID string `json:"persistent_id"` // Since: Bot API 9.6
Text string `json:"text"`
TextEntities []MessageEntity `json:"text_entities"`
Media *PollMedia `json:"media,omitempty"` // Since: Bot API 10.0
VoterCount int `json:"voter_count"`
AddedByUser *User `json:"added_by_user,omitempty"`
AddedByChat *Chat `json:"added_by_chat,omitempty"`
AdditionDate int `json:"addition_date,omitempty"`
AddedByUser *User `json:"added_by_user,omitempty"` // Since: Bot API 9.6
AddedByChat *Chat `json:"added_by_chat,omitempty"` // Since: Bot API 9.6
AdditionDate int `json:"addition_date,omitempty"` // Since: Bot API 9.6
}
// InputPollOptionMedia describes the media to attach to a poll option.
// Since: Bot API 10.0
// See https://core.telegram.org/bots/api#inputpolloptionmedia
type InputPollOptionMedia struct {
Type string `json:"type"`
Media string `json:"media"`
}
// InputPollOption contains information about one answer option in a poll to be sent.
// Since: Bot API 7.3
// See https://core.telegram.org/bots/api#inputpolloption
type InputPollOption struct {
Text string `json:"text"`
TextParseMode ParseMode `json:"text_parse_mode,omitempty"`
TextEntities []MessageEntity `json:"text_entities,omitempty"`
Text string `json:"text"`
TextParseMode ParseMode `json:"text_parse_mode,omitempty"`
TextEntities []MessageEntity `json:"text_entities,omitempty"`
Media *InputPollOptionMedia `json:"media,omitempty"` // Since: Bot API 10.0
}
// InputPollMedia describes the media to attach to a poll or its explanation.
// Since: Bot API 10.0
// See https://core.telegram.org/bots/api#inputpollmedia
type InputPollMedia struct {
Type string `json:"type"`
Media string `json:"media"`
}
// PollOptionAdded describes a service message about a poll option being added.
// Since: Bot API 9.6
type PollOptionAdded struct {
PollMessage *InaccessibleMessage `json:"poll_message,omitempty"`
OptionPersistentID string `json:"option_persistent_id"`
@@ -152,6 +200,8 @@ type PollOptionAdded struct {
OptionTextEntities []MessageEntity `json:"option_text_entities,omitempty"`
}
// PollOptionDeleted describes a service message about a poll option being deleted.
// Since: Bot API 9.6
type PollOptionDeleted struct {
PollMessage *InaccessibleMessage `json:"poll_message,omitempty"`
OptionPersistentID string `json:"option_persistent_id"`
@@ -170,38 +220,60 @@ const (
)
// PollAnswer represents an answer of a user in a poll.
// Since: Bot API 4.6
// See https://core.telegram.org/bots/api#pollanswer
type PollAnswer struct {
PollID string `json:"poll_id"`
VoterChat Chat `json:"voter_chat"`
VoterChat Chat `json:"voter_chat"` // Since: Bot API 6.8
User User `json:"user"`
OptionIDs []int `json:"option_ids"`
OptionPersistentIDs []string `json:"option_persistent_ids"`
OptionPersistentIDs []string `json:"option_persistent_ids"` // Since: Bot API 9.6
}
// Poll contains information about a poll.
// Since: Bot API 4.2
// See https://core.telegram.org/bots/api#poll
type Poll struct {
ID string `json:"id"`
Question string `json:"question"`
QuestionEntities []MessageEntity `json:"question_entities"`
QuestionEntities []MessageEntity `json:"question_entities"` // Since: Bot API 7.3
Options []PollOption `json:"options"`
TotalVoterCount int `json:"total_voter_count"`
IsClosed bool `json:"is_closed"`
IsAnonymous bool `json:"is_anonymous"`
IsClosed bool `json:"is_closed,omitempty"`
IsAnonymous bool `json:"is_anonymous,omitempty"`
Type PollType `json:"type"`
AllowsMultipleAnswers bool `json:"allows_multiple_answers"`
AllowsRevoting bool `json:"allows_revoting"`
CorrectOptionIDs []int `json:"correct_option_ids,omitempty"`
Explanation string `json:"explanation,omitempty"`
ExplanationEntities []MessageEntity `json:"explanation_entities,omitempty"`
OpenPeriod int `json:"open_period,omitempty"`
CloseDate int `json:"close_date,omitempty"`
Description string `json:"description,omitempty"`
DescriptionEntities []MessageEntity `json:"description_entities,omitempty"`
AllowsMultipleAnswers bool `json:"allows_multiple_answers,omitempty"` // Since: Bot API 4.6
AllowsRevoting bool `json:"allows_revoting,omitempty"` // Since: Bot API 9.6
MembersOnly bool `json:"members_only,omitempty"` // Since: Bot API 10.0
CountryCodes []string `json:"country_codes,omitempty"` // Since: Bot API 10.0
CorrectOptionIDs []int `json:"correct_option_ids,omitempty"` // Since: Bot API 9.6
Explanation string `json:"explanation,omitempty"` // Since: Bot API 4.8
ExplanationEntities []MessageEntity `json:"explanation_entities,omitempty"` // Since: Bot API 4.8
ExplanationMedia *PollMedia `json:"explanation_media,omitempty"` // Since: Bot API 10.0
OpenPeriod int `json:"open_period,omitempty"` // Since: Bot API 4.8
CloseDate int `json:"close_date,omitempty"` // Since: Bot API 4.8
Description string `json:"description,omitempty"` // Since: Bot API 9.6
DescriptionEntities []MessageEntity `json:"description_entities,omitempty"` // Since: Bot API 9.6
Media *PollMedia `json:"media,omitempty"` // Since: Bot API 10.0
}
// PollMedia represents media attached to a poll.
// Since: Bot API 10.0
type PollMedia struct {
Animation *Animation `json:"animation,omitempty"`
Audio *Audio `json:"audio,omitempty"`
Document *Document `json:"document,omitempty"`
LivePhoto *LivePhoto `json:"live_photo,omitempty"`
Location *Location `json:"location,omitempty"`
Photo []PhotoSize `json:"photo,omitempty"`
Sticker *Sticker `json:"sticker,omitempty"`
Venue *Venue `json:"venue,omitempty"`
Video *Video `json:"video,omitempty"`
}
// ChecklistTask represents a single task in a checklist.
// Since: Bot API 9.1
type ChecklistTask struct {
ID int `json:"id"`
Text string `json:"text"`
@@ -211,6 +283,8 @@ type ChecklistTask struct {
CompletionDate int `json:"completion_date,omitempty"`
}
// Checklist represents a checklist.
// Since: Bot API 9.1
type Checklist struct {
Title string `json:"title"`
TitleEntities []MessageEntity `json:"title_entities,omitempty"`
@@ -220,6 +294,7 @@ type Checklist struct {
}
// InputChecklistTask describes a task in a checklist.
// Since: Bot API 9.1
type InputChecklistTask struct {
ID int `json:"id"`
Text string `json:"text"`
@@ -228,6 +303,7 @@ type InputChecklistTask struct {
}
// InputChecklist represents a checklist to be sent.
// Since: Bot API 9.1
type InputChecklist struct {
Title string `json:"title"`
ParseMode ParseMode `json:"parse_mode,omitempty"`
@@ -237,12 +313,16 @@ type InputChecklist struct {
OtherCanMarkTasksAsDone bool `json:"other_can_mark_tasks_as_done,omitempty"`
}
// ChecklistTaskDone describes a service message about checklist tasks being marked as done.
// Since: Bot API 9.1
type ChecklistTaskDone struct {
ChecklistMessage *Message `json:"checklist_message,omitempty"`
MarkedAsDoneTaskIDs []int `json:"marked_as_done_task_ids,omitempty"`
MarkedAsNotDoneTaskIDs []int `json:"marked_as_not_done_task_ids,omitempty"`
}
// ChecklistTasksAdded describes a service message about new checklist tasks being added.
// Since: Bot API 9.1
type ChecklistTasksAdded struct {
ChecklistMessage *Message `json:"checklist_message,omitempty"`
Tasks []ChecklistTask `json:"tasks"`
@@ -262,10 +342,16 @@ const (
InputMediaTypeVideo InputMediaType = "video"
// InputMediaTypeAudio is an audio file.
InputMediaTypeAudio InputMediaType = "audio"
InputMediaTypeSticker InputMediaType = "sticker"
InputMediaTypeLocation InputMediaType = "location"
InputMediaTypeVenue InputMediaType = "venue"
InputMediaTypeLivePhoto InputMediaType = "live_photo" // Since: Bot API 10.0
)
// InputMedia represents the content of a media message to be sent.
// It is a union type described in https://core.telegram.org/bots/api#inputmedia.
// Since: Bot API 4.0
// See https://core.telegram.org/bots/api#inputmedia
type InputMedia struct {
Type InputMediaType `json:"type"`
Media string `json:"media"`
@@ -273,11 +359,11 @@ type InputMedia struct {
Caption *string `json:"caption,omitempty"`
ParseMode *ParseMode `json:"parse_mode,omitempty"`
CaptionEntities []MessageEntity `json:"caption_entities,omitempty"`
ShowCaptionAboveMedia *bool `json:"show_caption_above_media,omitempty"`
HasSpoiler *bool `json:"has_spoiler,omitempty"`
ShowCaptionAboveMedia *bool `json:"show_caption_above_media,omitempty"` // Since: Bot API 7.4
HasSpoiler *bool `json:"has_spoiler,omitempty"` // Since: Bot API 6.4
Cover *string `json:"cover"`
StartTimestamp *int `json:"start_timestamp"`
Cover *string `json:"cover"` // Since: Bot API 8.3
StartTimestamp *int `json:"start_timestamp"` // Since: Bot API 8.3
Width *int `json:"width,omitempty"`
Height *int `json:"height,omitempty"`
Duration *int `json:"duration,omitempty"`
@@ -285,6 +371,18 @@ type InputMedia struct {
Performer *string `json:"performer,omitempty"`
Title *string `json:"title,omitempty"`
Emoji *string `json:"emoji,omitempty"`
Latitude *float64 `json:"latitude,omitempty"`
Longitude *float64 `json:"longitude,omitempty"`
Address *string `json:"address,omitempty"`
FoursquareID *string `json:"foursquare_id,omitempty"`
FoursquareType *string `json:"foursquare_type,omitempty"`
GooglePlaceID *string `json:"google_place_id,omitempty"`
GooglePlaceType *string `json:"google_place_type,omitempty"`
HorizontalAccuracy *float64 `json:"horizontal_accuracy,omitempty"`
}
// InputPaidMediaType represents the type of paid media.
@@ -295,16 +393,19 @@ const (
InputPaidMediaTypeVideo InputPaidMediaType = "video"
// InputPaidMediaTypePhoto represents a paid photo.
InputPaidMediaTypePhoto InputPaidMediaType = "photo"
// InputPaidMediaTypeLivePhoto represents a paid live photo.
InputPaidMediaTypeLivePhoto InputPaidMediaType = "live_photo" // Since: Bot API 10.0
)
// InputPaidMedia describes the paid media to be sent.
// Since: Bot API 7.6
// See https://core.telegram.org/bots/api#inputpaidmedia
type InputPaidMedia struct {
Type InputPaidMediaType `json:"type"`
Media string `json:"media"`
Cover *string `json:"cover,omitempty"`
StartTimestamp *int64 `json:"start_timestamp,omitempty"`
Cover *string `json:"cover,omitempty"` // Since: Bot API 8.3
StartTimestamp *int64 `json:"start_timestamp,omitempty"` // Since: Bot API 8.3
Width *int `json:"width,omitempty"`
Height *int `json:"height,omitempty"`
Duration *int `json:"duration,omitempty"`
@@ -312,6 +413,7 @@ type InputPaidMedia struct {
}
// PhotoSize represents one size of a photo or a file/sticker thumbnail.
// Since: Bot API 1.0
// See https://core.telegram.org/bots/api#photosize
type PhotoSize struct {
FileID string `json:"file_id"`
@@ -320,3 +422,16 @@ type PhotoSize struct {
Height int `json:"height"`
FileSize int64 `json:"file_size,omitempty"`
}
// LivePhoto represents a live photo (a photo with a short video attached).
// Since: Bot API 10.0
type LivePhoto struct {
Photo []PhotoSize `json:"photo,omitempty"`
FileID string `json:"file_id"`
FileUniqueID string `json:"file_unique_id"`
Width int `json:"width"`
Height int `json:"height"`
Duration int `json:"duration"`
MIMEType string `json:"mime_type,omitempty"`
FileSize int64 `json:"file_size,omitempty"`
}
+102
View File
@@ -3,6 +3,7 @@ package tgapi
import "context"
// SetMyCommands holds parameters for the setMyCommands method.
// Since: Bot API 4.7
// See https://core.telegram.org/bots/api#setmycommands
type SetMyCommands struct {
Commands []BotCommand `json:"commands"`
@@ -11,6 +12,7 @@ type SetMyCommands struct {
}
// SetMyCommands changes the list of the bot's commands.
// Since: Bot API 4.7
// Returns true on success.
// See https://core.telegram.org/bots/api#setmycommands
func (api *API) SetMyCommands(params SetMyCommands) (bool, error) {
@@ -19,6 +21,7 @@ func (api *API) SetMyCommands(params SetMyCommands) (bool, error) {
}
// SetMyCommandsWithContext is the context-aware variant of SetMyCommands.
// Since: Bot API 4.7
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#setmycommands
func (api *API) SetMyCommandsWithContext(ctx context.Context, params SetMyCommands) (bool, error) {
@@ -27,6 +30,7 @@ func (api *API) SetMyCommandsWithContext(ctx context.Context, params SetMyComman
}
// DeleteMyCommands holds parameters for the deleteMyCommands method.
// Since: Bot API 5.3
// See https://core.telegram.org/bots/api#deletemycommands
type DeleteMyCommands struct {
Scope *BotCommandScope `json:"scope,omitempty"`
@@ -34,6 +38,7 @@ type DeleteMyCommands struct {
}
// DeleteMyCommands deletes the list of the bot's commands for the given scope and user language.
// Since: Bot API 5.3
// Returns true on success.
// See https://core.telegram.org/bots/api#deletemycommands
func (api *API) DeleteMyCommands(params DeleteMyCommands) (bool, error) {
@@ -42,6 +47,7 @@ func (api *API) DeleteMyCommands(params DeleteMyCommands) (bool, error) {
}
// DeleteMyCommandsWithContext is the context-aware variant of DeleteMyCommands.
// Since: Bot API 5.3
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#deletemycommands
func (api *API) DeleteMyCommandsWithContext(ctx context.Context, params DeleteMyCommands) (bool, error) {
@@ -50,6 +56,7 @@ func (api *API) DeleteMyCommandsWithContext(ctx context.Context, params DeleteMy
}
// GetMyCommands holds parameters for the getMyCommands method.
// Since: Bot API 4.7
// See https://core.telegram.org/bots/api#getmycommands
type GetMyCommands struct {
Scope *BotCommandScope `json:"scope,omitempty"`
@@ -57,6 +64,7 @@ type GetMyCommands struct {
}
// GetMyCommands returns the current list of the bot's commands for the given scope and user language.
// Since: Bot API 4.7
// See https://core.telegram.org/bots/api#getmycommands
func (api *API) GetMyCommands(params GetMyCommands) ([]BotCommand, error) {
req := NewRequest[[]BotCommand]("getMyCommands", params)
@@ -64,6 +72,7 @@ func (api *API) GetMyCommands(params GetMyCommands) ([]BotCommand, error) {
}
// GetMyCommandsWithContext is the context-aware variant of GetMyCommands.
// Since: Bot API 4.7
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#getmycommands
func (api *API) GetMyCommandsWithContext(ctx context.Context, params GetMyCommands) ([]BotCommand, error) {
@@ -72,6 +81,7 @@ func (api *API) GetMyCommandsWithContext(ctx context.Context, params GetMyComman
}
// SetMyName holds parameters for the setMyName method.
// Since: Bot API 6.7
// See https://core.telegram.org/bots/api#setmyname
type SetMyName struct {
Name string `json:"name"`
@@ -79,6 +89,7 @@ type SetMyName struct {
}
// SetMyName changes the bot's name.
// Since: Bot API 6.7
// Returns true on success.
// See https://core.telegram.org/bots/api#setmyname
func (api *API) SetMyName(params SetMyName) (bool, error) {
@@ -87,6 +98,7 @@ func (api *API) SetMyName(params SetMyName) (bool, error) {
}
// SetMyNameWithContext is the context-aware variant of SetMyName.
// Since: Bot API 6.7
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#setmyname
func (api *API) SetMyNameWithContext(ctx context.Context, params SetMyName) (bool, error) {
@@ -95,12 +107,14 @@ func (api *API) SetMyNameWithContext(ctx context.Context, params SetMyName) (boo
}
// GetMyName holds parameters for the getMyName method.
// Since: Bot API 6.7
// See https://core.telegram.org/bots/api#getmyname
type GetMyName struct {
Language string `json:"language_code,omitempty"`
}
// GetMyName returns the bot's name for the given language.
// Since: Bot API 6.7
// See https://core.telegram.org/bots/api#getmyname
func (api *API) GetMyName(params GetMyName) (BotName, error) {
req := NewRequest[BotName]("getMyName", params)
@@ -108,6 +122,7 @@ func (api *API) GetMyName(params GetMyName) (BotName, error) {
}
// GetMyNameWithContext is the context-aware variant of GetMyName.
// Since: Bot API 6.7
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#getmyname
func (api *API) GetMyNameWithContext(ctx context.Context, params GetMyName) (BotName, error) {
@@ -116,6 +131,7 @@ func (api *API) GetMyNameWithContext(ctx context.Context, params GetMyName) (Bot
}
// SetMyDescription holds parameters for the setMyDescription method.
// Since: Bot API 6.6
// See https://core.telegram.org/bots/api#setmydescription
type SetMyDescription struct {
Description string `json:"description"`
@@ -123,6 +139,7 @@ type SetMyDescription struct {
}
// SetMyDescription changes the bot's description.
// Since: Bot API 6.6
// Returns true on success.
// See https://core.telegram.org/bots/api#setmydescription
func (api *API) SetMyDescription(params SetMyDescription) (bool, error) {
@@ -131,6 +148,7 @@ func (api *API) SetMyDescription(params SetMyDescription) (bool, error) {
}
// SetMyDescriptionWithContext is the context-aware variant of SetMyDescription.
// Since: Bot API 6.6
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#setmydescription
func (api *API) SetMyDescriptionWithContext(ctx context.Context, params SetMyDescription) (bool, error) {
@@ -139,12 +157,14 @@ func (api *API) SetMyDescriptionWithContext(ctx context.Context, params SetMyDes
}
// GetMyDescription holds parameters for the getMyDescription method.
// Since: Bot API 6.6
// See https://core.telegram.org/bots/api#getmydescription
type GetMyDescription struct {
Language string `json:"language_code,omitempty"`
}
// GetMyDescription returns the bot's description for the given language.
// Since: Bot API 6.6
// See https://core.telegram.org/bots/api#getmydescription
func (api *API) GetMyDescription(params GetMyDescription) (BotDescription, error) {
req := NewRequest[BotDescription]("getMyDescription", params)
@@ -152,6 +172,7 @@ func (api *API) GetMyDescription(params GetMyDescription) (BotDescription, error
}
// GetMyDescriptionWithContext is the context-aware variant of GetMyDescription.
// Since: Bot API 6.6
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#getmydescription
func (api *API) GetMyDescriptionWithContext(ctx context.Context, params GetMyDescription) (BotDescription, error) {
@@ -160,6 +181,7 @@ func (api *API) GetMyDescriptionWithContext(ctx context.Context, params GetMyDes
}
// SetMyShortDescription holds parameters for the setMyShortDescription method.
// Since: Bot API 6.6
// See https://core.telegram.org/bots/api#setmyshortdescription
type SetMyShortDescription struct {
ShortDescription string `json:"short_description,omitempty"`
@@ -167,6 +189,7 @@ type SetMyShortDescription struct {
}
// SetMyShortDescription changes the bot's short description.
// Since: Bot API 6.6
// Returns true on success.
// See https://core.telegram.org/bots/api#setmyshortdescription
func (api *API) SetMyShortDescription(params SetMyShortDescription) (bool, error) {
@@ -175,6 +198,7 @@ func (api *API) SetMyShortDescription(params SetMyShortDescription) (bool, error
}
// SetMyShortDescriptionWithContext is the context-aware variant of SetMyShortDescription.
// Since: Bot API 6.6
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#setmyshortdescription
func (api *API) SetMyShortDescriptionWithContext(ctx context.Context, params SetMyShortDescription) (bool, error) {
@@ -183,12 +207,14 @@ func (api *API) SetMyShortDescriptionWithContext(ctx context.Context, params Set
}
// GetMyShortDescription holds parameters for the getMyShortDescription method.
// Since: Bot API 6.6
// See https://core.telegram.org/bots/api#getmyshortdescription
type GetMyShortDescription struct {
Language string `json:"language_code,omitempty"`
}
// GetMyShortDescription returns the bot's short description for the given language.
// Since: Bot API 6.6
// See https://core.telegram.org/bots/api#getmyshortdescription
func (api *API) GetMyShortDescription(params GetMyShortDescription) (BotShortDescription, error) {
req := NewRequest[BotShortDescription]("getMyShortDescription", params)
@@ -196,6 +222,7 @@ func (api *API) GetMyShortDescription(params GetMyShortDescription) (BotShortDes
}
// GetMyShortDescriptionWithContext is the context-aware variant of GetMyShortDescription.
// Since: Bot API 6.6
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#getmyshortdescription
func (api *API) GetMyShortDescriptionWithContext(ctx context.Context, params GetMyShortDescription) (BotShortDescription, error) {
@@ -204,12 +231,14 @@ func (api *API) GetMyShortDescriptionWithContext(ctx context.Context, params Get
}
// SetMyProfilePhoto holds parameters for the setMyProfilePhoto method.
// Since: Bot API 9.0
// See https://core.telegram.org/bots/api#setmyprofilephoto
type SetMyProfilePhoto struct {
Photo InputProfilePhoto `json:"photo"`
}
// SetMyProfilePhoto changes the bot's profile photo.
// Since: Bot API 9.0
// Returns true on success.
// See https://core.telegram.org/bots/api#setmyprofilephoto
func (api *API) SetMyProfilePhoto(params SetMyProfilePhoto) (bool, error) {
@@ -218,6 +247,7 @@ func (api *API) SetMyProfilePhoto(params SetMyProfilePhoto) (bool, error) {
}
// SetMyProfilePhotoWithContext is the context-aware variant of SetMyProfilePhoto.
// Since: Bot API 9.0
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#setmyprofilephoto
func (api *API) SetMyProfilePhotoWithContext(ctx context.Context, params SetMyProfilePhoto) (bool, error) {
@@ -226,6 +256,7 @@ func (api *API) SetMyProfilePhotoWithContext(ctx context.Context, params SetMyPr
}
// RemoveMyProfilePhoto removes the bot's profile photo.
// Since: Bot API 9.0
// Returns true on success.
// See https://core.telegram.org/bots/api#removemyprofilephoto
func (api *API) RemoveMyProfilePhoto() (bool, error) {
@@ -234,6 +265,7 @@ func (api *API) RemoveMyProfilePhoto() (bool, error) {
}
// RemoveMyProfilePhotoWithContext is the context-aware variant of RemoveMyProfilePhoto.
// Since: Bot API 9.0
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#removemyprofilephoto
func (api *API) RemoveMyProfilePhotoWithContext(ctx context.Context) (bool, error) {
@@ -242,6 +274,7 @@ func (api *API) RemoveMyProfilePhotoWithContext(ctx context.Context) (bool, erro
}
// SetChatMenuButton holds parameters for the setChatMenuButton method.
// Since: Bot API 6.0
// See https://core.telegram.org/bots/api#setchatmenubutton
type SetChatMenuButton struct {
ChatID int64 `json:"chat_id,omitempty"`
@@ -249,6 +282,7 @@ type SetChatMenuButton struct {
}
// SetChatMenuButton changes the menu button for a given chat or the default menu button.
// Since: Bot API 6.0
// Returns true on success.
// See https://core.telegram.org/bots/api#setchatmenubutton
func (api *API) SetChatMenuButton(params SetChatMenuButton) (bool, error) {
@@ -257,6 +291,7 @@ func (api *API) SetChatMenuButton(params SetChatMenuButton) (bool, error) {
}
// SetChatMenuButtonWithContext is the context-aware variant of SetChatMenuButton.
// Since: Bot API 6.0
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#setchatmenubutton
func (api *API) SetChatMenuButtonWithContext(ctx context.Context, params SetChatMenuButton) (bool, error) {
@@ -265,12 +300,14 @@ func (api *API) SetChatMenuButtonWithContext(ctx context.Context, params SetChat
}
// GetChatMenuButton holds parameters for the getChatMenuButton method.
// Since: Bot API 6.0
// See https://core.telegram.org/bots/api#getchatmenubutton
type GetChatMenuButton struct {
ChatID int64 `json:"chat_id,omitempty"`
}
// GetChatMenuButton returns the current menu button for the given chat.
// Since: Bot API 6.0
// See https://core.telegram.org/bots/api#getchatmenubutton
func (api *API) GetChatMenuButton(params GetChatMenuButton) (MenuButton, error) {
req := NewRequest[MenuButton]("getChatMenuButton", params)
@@ -278,6 +315,7 @@ func (api *API) GetChatMenuButton(params GetChatMenuButton) (MenuButton, error)
}
// GetChatMenuButtonWithContext is the context-aware variant of GetChatMenuButton.
// Since: Bot API 6.0
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#getchatmenubutton
func (api *API) GetChatMenuButtonWithContext(ctx context.Context, params GetChatMenuButton) (MenuButton, error) {
@@ -286,6 +324,7 @@ func (api *API) GetChatMenuButtonWithContext(ctx context.Context, params GetChat
}
// SetMyDefaultAdministratorRights holds parameters for the setMyDefaultAdministratorRights method.
// Since: Bot API 6.0
// See https://core.telegram.org/bots/api#setmydefaultadministratorrights
type SetMyDefaultAdministratorRights struct {
Rights *ChatAdministratorRights `json:"rights"`
@@ -293,6 +332,7 @@ type SetMyDefaultAdministratorRights struct {
}
// SetMyDefaultAdministratorRights changes the default administrator rights for the bot.
// Since: Bot API 6.0
// Returns true on success.
// See https://core.telegram.org/bots/api#setmydefaultadministratorrights
func (api *API) SetMyDefaultAdministratorRights(params SetMyDefaultAdministratorRights) (bool, error) {
@@ -301,6 +341,7 @@ func (api *API) SetMyDefaultAdministratorRights(params SetMyDefaultAdministrator
}
// SetMyDefaultAdministratorRightsWithContext is the context-aware variant of SetMyDefaultAdministratorRights.
// Since: Bot API 6.0
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#setmydefaultadministratorrights
func (api *API) SetMyDefaultAdministratorRightsWithContext(ctx context.Context, params SetMyDefaultAdministratorRights) (bool, error) {
@@ -309,12 +350,14 @@ func (api *API) SetMyDefaultAdministratorRightsWithContext(ctx context.Context,
}
// GetMyDefaultAdministratorRights holds parameters for the getMyDefaultAdministratorRights method.
// Since: Bot API 6.0
// See https://core.telegram.org/bots/api#getmydefaultadministratorrights
type GetMyDefaultAdministratorRights struct {
ForChannels bool `json:"for_channels"`
}
// GetMyDefaultAdministratorRights returns the current default administrator rights for the bot.
// Since: Bot API 6.0
// See https://core.telegram.org/bots/api#getmydefaultadministratorrights
func (api *API) GetMyDefaultAdministratorRights(params GetMyDefaultAdministratorRights) (ChatAdministratorRights, error) {
req := NewRequest[ChatAdministratorRights]("getMyDefaultAdministratorRights", params)
@@ -322,6 +365,7 @@ func (api *API) GetMyDefaultAdministratorRights(params GetMyDefaultAdministrator
}
// GetMyDefaultAdministratorRightsWithContext is the context-aware variant of GetMyDefaultAdministratorRights.
// Since: Bot API 6.0
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#getmydefaultadministratorrights
func (api *API) GetMyDefaultAdministratorRightsWithContext(ctx context.Context, params GetMyDefaultAdministratorRights) (ChatAdministratorRights, error) {
@@ -330,6 +374,7 @@ func (api *API) GetMyDefaultAdministratorRightsWithContext(ctx context.Context,
}
// GetAvailableGifts returns the list of gifts that can be sent by the bot.
// Since: Bot API 9.0
// See https://core.telegram.org/bots/api#getavailablegifts
func (api *API) GetAvailableGifts() (Gifts, error) {
req := NewRequest[Gifts]("getAvailableGifts", NoParams)
@@ -337,6 +382,7 @@ func (api *API) GetAvailableGifts() (Gifts, error) {
}
// GetAvailableGiftsWithContext is the context-aware variant of GetAvailableGifts.
// Since: Bot API 9.0
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#getavailablegifts
func (api *API) GetAvailableGiftsWithContext(ctx context.Context) (Gifts, error) {
@@ -345,6 +391,7 @@ func (api *API) GetAvailableGiftsWithContext(ctx context.Context) (Gifts, error)
}
// SendGift holds parameters for the sendGift method.
// Since: Bot API 9.0
// See https://core.telegram.org/bots/api#sendgift
type SendGift struct {
UserID int64 `json:"user_id,omitempty"`
@@ -357,6 +404,7 @@ type SendGift struct {
}
// SendGift sends a gift to the given user or chat.
// Since: Bot API 9.0
// Returns true on success.
// See https://core.telegram.org/bots/api#sendgift
func (api *API) SendGift(params SendGift) (bool, error) {
@@ -365,6 +413,7 @@ func (api *API) SendGift(params SendGift) (bool, error) {
}
// SendGiftWithContext is the context-aware variant of SendGift.
// Since: Bot API 9.0
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#sendgift
func (api *API) SendGiftWithContext(ctx context.Context, params SendGift) (bool, error) {
@@ -373,6 +422,7 @@ func (api *API) SendGiftWithContext(ctx context.Context, params SendGift) (bool,
}
// GiftPremiumSubscription holds parameters for the giftPremiumSubscription method.
// Since: Bot API 9.0
// See https://core.telegram.org/bots/api#giftpremiumsubscription
type GiftPremiumSubscription struct {
UserID int64 `json:"user_id"`
@@ -384,6 +434,7 @@ type GiftPremiumSubscription struct {
}
// GiftPremiumSubscription gifts a Telegram Premium subscription to the user.
// Since: Bot API 9.0
// Returns true on success.
// See https://core.telegram.org/bots/api#giftpremiumsubscription
func (api *API) GiftPremiumSubscription(params GiftPremiumSubscription) (bool, error) {
@@ -392,9 +443,60 @@ func (api *API) GiftPremiumSubscription(params GiftPremiumSubscription) (bool, e
}
// GiftPremiumSubscriptionWithContext is the context-aware variant of GiftPremiumSubscription.
// Since: Bot API 9.0
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#giftpremiumsubscription
func (api *API) GiftPremiumSubscriptionWithContext(ctx context.Context, params GiftPremiumSubscription) (bool, error) {
req := NewRequest[bool]("giftPremiumSubscription", params)
return req.DoWithContext(ctx, api)
}
// GetManagedBotAccessSettings holds parameters for the getManagedBotAccessSettings method.
// Since: Bot API 10.0
// See https://core.telegram.org/bots/api#getmanagedbotaccesssettings
type GetManagedBotAccessSettings struct {
BotUserID int64 `json:"bot_user_id"`
}
// GetManagedBotAccessSettings returns the access settings of a managed bot.
// Since: Bot API 10.0
// See https://core.telegram.org/bots/api#getmanagedbotaccesssettings
func (api *API) GetManagedBotAccessSettings(params GetManagedBotAccessSettings) (BotAccessSettings, error) {
req := NewRequest[BotAccessSettings]("getManagedBotAccessSettings", params)
return req.Do(api)
}
// GetManagedBotAccessSettingsWithContext is the context-aware variant of GetManagedBotAccessSettings.
// Since: Bot API 10.0
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#getmanagedbotaccesssettings
func (api *API) GetManagedBotAccessSettingsWithContext(ctx context.Context, params GetManagedBotAccessSettings) (BotAccessSettings, error) {
req := NewRequest[BotAccessSettings]("getManagedBotAccessSettings", params)
return req.DoWithContext(ctx, api)
}
// SetManagedBotAccessSettings holds parameters for the setManagedBotAccessSettings method.
// Since: Bot API 10.0
// See https://core.telegram.org/bots/api#setmanagedbotaccesssettings
type SetManagedBotAccessSettings struct {
BotUserID int64 `json:"bot_user_id"`
AccessSettings BotAccessSettings `json:"access_settings"`
}
// SetManagedBotAccessSettings changes the access settings of a managed bot.
// Since: Bot API 10.0
// Returns True on success.
// See https://core.telegram.org/bots/api#setmanagedbotaccesssettings
func (api *API) SetManagedBotAccessSettings(params SetManagedBotAccessSettings) (bool, error) {
req := NewRequest[bool]("setManagedBotAccessSettings", params)
return req.Do(api)
}
// SetManagedBotAccessSettingsWithContext is the context-aware variant of SetManagedBotAccessSettings.
// Since: Bot API 10.0
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#setmanagedbotaccesssettings
func (api *API) SetManagedBotAccessSettingsWithContext(ctx context.Context, params SetManagedBotAccessSettings) (bool, error) {
req := NewRequest[bool]("setManagedBotAccessSettings", params)
return req.DoWithContext(ctx, api)
}
+14
View File
@@ -1,6 +1,7 @@
package tgapi
// BotCommand represents a bot command.
// Since: Bot API 4.7
// See https://core.telegram.org/bots/api#botcommand
type BotCommand struct {
Command string `json:"command"`
@@ -28,6 +29,7 @@ const (
)
// BotCommandScope represents the scope to which bot commands are applied.
// Since: Bot API 5.3
// See https://core.telegram.org/bots/api#botcommandscope
type BotCommandScope struct {
Type BotCommandScopeType `json:"type"`
@@ -36,16 +38,19 @@ type BotCommandScope struct {
}
// BotName represents the bot's name.
// Since: Bot API 6.7
type BotName struct {
Name string `json:"name"`
}
// BotDescription represents the bot's description.
// Since: Bot API 6.6
type BotDescription struct {
Description string `json:"description"`
}
// BotShortDescription represents the bot's short description.
// Since: Bot API 6.6
type BotShortDescription struct {
ShortDescription string `json:"short_description"`
}
@@ -61,6 +66,7 @@ const (
)
// InputProfilePhoto describes a profile photo to set.
// Since: Bot API 9.0
// See https://core.telegram.org/bots/api#inputprofilephoto
type InputProfilePhoto struct {
Type InputProfilePhotoType `json:"type"`
@@ -86,6 +92,7 @@ const (
)
// MenuButton represents a menu button.
// Since: Bot API 6.0
// See https://core.telegram.org/bots/api#menubutton
type MenuButton struct {
Type MenuButtonType `json:"type"`
@@ -94,3 +101,10 @@ type MenuButton struct {
Text *string `json:"text"`
WebApp *WebAppInfo `json:"web_app"`
}
// BotAccessSettings describes access settings of a managed bot.
// Since: Bot API 10.0
// See https://core.telegram.org/bots/api#botaccesssettings
type BotAccessSettings struct {
AllowAllPrivateChats bool `json:"allow_all_private_chats"`
}
+70 -1
View File
@@ -3,6 +3,7 @@ package tgapi
import "context"
// VerifyUser holds parameters for the verifyUser method.
// Since: Bot API 8.0
// See https://core.telegram.org/bots/api#verifyuser
type VerifyUser struct {
UserID int64 `json:"user_id"`
@@ -10,6 +11,7 @@ type VerifyUser struct {
}
// VerifyUser verifies a user.
// Since: Bot API 8.0
// Returns true on success.
// See https://core.telegram.org/bots/api#verifyuser
func (api *API) VerifyUser(params VerifyUser) (bool, error) {
@@ -18,6 +20,7 @@ func (api *API) VerifyUser(params VerifyUser) (bool, error) {
}
// VerifyUserWithContext is the context-aware variant of VerifyUser.
// Since: Bot API 8.0
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#verifyuser
func (api *API) VerifyUserWithContext(ctx context.Context, params VerifyUser) (bool, error) {
@@ -26,6 +29,7 @@ func (api *API) VerifyUserWithContext(ctx context.Context, params VerifyUser) (b
}
// VerifyChat holds parameters for the verifyChat method.
// Since: Bot API 8.0
// See https://core.telegram.org/bots/api#verifychat
type VerifyChat struct {
ChatID int64 `json:"chat_id"`
@@ -33,6 +37,7 @@ type VerifyChat struct {
}
// VerifyChat verifies a chat.
// Since: Bot API 8.0
// Returns true on success.
// See https://core.telegram.org/bots/api#verifychat
func (api *API) VerifyChat(params VerifyChat) (bool, error) {
@@ -41,6 +46,7 @@ func (api *API) VerifyChat(params VerifyChat) (bool, error) {
}
// VerifyChatWithContext is the context-aware variant of VerifyChat.
// Since: Bot API 8.0
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#verifychat
func (api *API) VerifyChatWithContext(ctx context.Context, params VerifyChat) (bool, error) {
@@ -49,12 +55,14 @@ func (api *API) VerifyChatWithContext(ctx context.Context, params VerifyChat) (b
}
// RemoveUserVerification holds parameters for the removeUserVerification method.
// Since: Bot API 8.0
// See https://core.telegram.org/bots/api#removeuserverification
type RemoveUserVerification struct {
UserID int64 `json:"user_id"`
}
// RemoveUserVerification removes a user's verification.
// Since: Bot API 8.0
// Returns true on success.
// See https://core.telegram.org/bots/api#removeuserverification
func (api *API) RemoveUserVerification(params RemoveUserVerification) (bool, error) {
@@ -63,6 +71,7 @@ func (api *API) RemoveUserVerification(params RemoveUserVerification) (bool, err
}
// RemoveUserVerificationWithContext is the context-aware variant of RemoveUserVerification.
// Since: Bot API 8.0
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#removeuserverification
func (api *API) RemoveUserVerificationWithContext(ctx context.Context, params RemoveUserVerification) (bool, error) {
@@ -71,12 +80,14 @@ func (api *API) RemoveUserVerificationWithContext(ctx context.Context, params Re
}
// RemoveChatVerification holds parameters for the removeChatVerification method.
// Since: Bot API 8.0
// See https://core.telegram.org/bots/api#removechatverification
type RemoveChatVerification struct {
ChatID int64 `json:"chat_id"`
}
// RemoveChatVerification removes a chat's verification.
// Since: Bot API 8.0
// Returns true on success.
// See https://core.telegram.org/bots/api#removechatverification
func (api *API) RemoveChatVerification(params RemoveChatVerification) (bool, error) {
@@ -85,6 +96,7 @@ func (api *API) RemoveChatVerification(params RemoveChatVerification) (bool, err
}
// RemoveChatVerificationWithContext is the context-aware variant of RemoveChatVerification.
// Since: Bot API 8.0
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#removechatverification
func (api *API) RemoveChatVerificationWithContext(ctx context.Context, params RemoveChatVerification) (bool, error) {
@@ -93,6 +105,7 @@ func (api *API) RemoveChatVerificationWithContext(ctx context.Context, params Re
}
// ReadBusinessMessage holds parameters for the readBusinessMessage method.
// Since: Bot API 9.0
// See https://core.telegram.org/bots/api#readbusinessmessage
type ReadBusinessMessage struct {
BusinessConnectionID string `json:"business_connection_id"`
@@ -101,6 +114,7 @@ type ReadBusinessMessage struct {
}
// ReadBusinessMessage marks a business message as read.
// Since: Bot API 9.0
// Returns true on success.
// See https://core.telegram.org/bots/api#readbusinessmessage
func (api *API) ReadBusinessMessage(params ReadBusinessMessage) (bool, error) {
@@ -109,6 +123,7 @@ func (api *API) ReadBusinessMessage(params ReadBusinessMessage) (bool, error) {
}
// ReadBusinessMessageWithContext is the context-aware variant of ReadBusinessMessage.
// Since: Bot API 9.0
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#readbusinessmessage
func (api *API) ReadBusinessMessageWithContext(ctx context.Context, params ReadBusinessMessage) (bool, error) {
@@ -117,12 +132,14 @@ func (api *API) ReadBusinessMessageWithContext(ctx context.Context, params ReadB
}
// GetBusinessConnection holds parameters for the getBusinessConnection method.
// Since: Bot API 7.2
// See https://core.telegram.org/bots/api#getbusinessconnection
type GetBusinessConnection struct {
BusinessConnectionID string `json:"business_connection_id"`
}
// GetBusinessConnection returns information about a business connection.
// Since: Bot API 7.2
// See https://core.telegram.org/bots/api#getbusinessconnection
func (api *API) GetBusinessConnection(params GetBusinessConnection) (BusinessConnection, error) {
req := NewRequest[BusinessConnection]("getBusinessConnection", params)
@@ -130,6 +147,7 @@ func (api *API) GetBusinessConnection(params GetBusinessConnection) (BusinessCon
}
// GetBusinessConnectionWithContext is the context-aware variant of GetBusinessConnection.
// Since: Bot API 7.2
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#getbusinessconnection
func (api *API) GetBusinessConnectionWithContext(ctx context.Context, params GetBusinessConnection) (BusinessConnection, error) {
@@ -138,6 +156,7 @@ func (api *API) GetBusinessConnectionWithContext(ctx context.Context, params Get
}
// DeleteBusinessMessages holds parameters for the deleteBusinessMessages method.
// Since: Bot API 9.0
// See https://core.telegram.org/bots/api#deletebusinessmessages
type DeleteBusinessMessages struct {
BusinessConnectionID string `json:"business_connection_id"`
@@ -145,6 +164,7 @@ type DeleteBusinessMessages struct {
}
// DeleteBusinessMessages deletes business messages.
// Since: Bot API 9.0
// Returns true on success.
// See https://core.telegram.org/bots/api#deletebusinessmessages
func (api *API) DeleteBusinessMessages(params DeleteBusinessMessages) (bool, error) {
@@ -153,6 +173,7 @@ func (api *API) DeleteBusinessMessages(params DeleteBusinessMessages) (bool, err
}
// DeleteBusinessMessagesWithContext is the context-aware variant of DeleteBusinessMessages.
// Since: Bot API 9.0
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#deletebusinessmessages
func (api *API) DeleteBusinessMessagesWithContext(ctx context.Context, params DeleteBusinessMessages) (bool, error) {
@@ -161,6 +182,7 @@ func (api *API) DeleteBusinessMessagesWithContext(ctx context.Context, params De
}
// SetBusinessAccountName holds parameters for the setBusinessAccountName method.
// Since: Bot API 9.0
// See https://core.telegram.org/bots/api#setbusinessaccountname
type SetBusinessAccountName struct {
BusinessConnectionID string `json:"business_connection_id"`
@@ -169,6 +191,7 @@ type SetBusinessAccountName struct {
}
// SetBusinessAccountName sets the first and last name of a business account.
// Since: Bot API 9.0
// Returns true on success.
// See https://core.telegram.org/bots/api#setbusinessaccountname
func (api *API) SetBusinessAccountName(params SetBusinessAccountName) (bool, error) {
@@ -177,6 +200,7 @@ func (api *API) SetBusinessAccountName(params SetBusinessAccountName) (bool, err
}
// SetBusinessAccountNameWithContext is the context-aware variant of SetBusinessAccountName.
// Since: Bot API 9.0
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#setbusinessaccountname
func (api *API) SetBusinessAccountNameWithContext(ctx context.Context, params SetBusinessAccountName) (bool, error) {
@@ -185,6 +209,7 @@ func (api *API) SetBusinessAccountNameWithContext(ctx context.Context, params Se
}
// SetBusinessAccountUsername holds parameters for the setBusinessAccountUsername method.
// Since: Bot API 9.0
// See https://core.telegram.org/bots/api#setbusinessaccountusername
type SetBusinessAccountUsername struct {
BusinessConnectionID string `json:"business_connection_id"`
@@ -192,6 +217,7 @@ type SetBusinessAccountUsername struct {
}
// SetBusinessAccountUsername sets the username of a business account.
// Since: Bot API 9.0
// Returns true on success.
// See https://core.telegram.org/bots/api#setbusinessaccountusername
func (api *API) SetBusinessAccountUsername(params SetBusinessAccountUsername) (bool, error) {
@@ -200,6 +226,7 @@ func (api *API) SetBusinessAccountUsername(params SetBusinessAccountUsername) (b
}
// SetBusinessAccountUsernameWithContext is the context-aware variant of SetBusinessAccountUsername.
// Since: Bot API 9.0
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#setbusinessaccountusername
func (api *API) SetBusinessAccountUsernameWithContext(ctx context.Context, params SetBusinessAccountUsername) (bool, error) {
@@ -208,6 +235,7 @@ func (api *API) SetBusinessAccountUsernameWithContext(ctx context.Context, param
}
// SetBusinessAccountBio holds parameters for the setBusinessAccountBio method.
// Since: Bot API 9.0
// See https://core.telegram.org/bots/api#setbusinessaccountbio
type SetBusinessAccountBio struct {
BusinessConnectionID string `json:"business_connection_id"`
@@ -215,6 +243,7 @@ type SetBusinessAccountBio struct {
}
// SetBusinessAccountBio sets the bio of a business account.
// Since: Bot API 9.0
// Returns true on success.
// See https://core.telegram.org/bots/api#setbusinessaccountbio
func (api *API) SetBusinessAccountBio(params SetBusinessAccountBio) (bool, error) {
@@ -223,6 +252,7 @@ func (api *API) SetBusinessAccountBio(params SetBusinessAccountBio) (bool, error
}
// SetBusinessAccountBioWithContext is the context-aware variant of SetBusinessAccountBio.
// Since: Bot API 9.0
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#setbusinessaccountbio
func (api *API) SetBusinessAccountBioWithContext(ctx context.Context, params SetBusinessAccountBio) (bool, error) {
@@ -231,6 +261,7 @@ func (api *API) SetBusinessAccountBioWithContext(ctx context.Context, params Set
}
// SetBusinessAccountProfilePhoto holds parameters for the setBusinessAccountProfilePhoto method.
// Since: Bot API 9.0
// See https://core.telegram.org/bots/api#setbusinessaccountprofilephoto
type SetBusinessAccountProfilePhoto struct {
BusinessConnectionID string `json:"business_connection_id"`
@@ -239,6 +270,7 @@ type SetBusinessAccountProfilePhoto struct {
}
// SetBusinessAccountProfilePhoto sets the profile photo of a business account.
// Since: Bot API 9.0
// Returns true on success.
// See https://core.telegram.org/bots/api#setbusinessaccountprofilephoto
func (api *API) SetBusinessAccountProfilePhoto(params SetBusinessAccountProfilePhoto) (bool, error) {
@@ -247,6 +279,7 @@ func (api *API) SetBusinessAccountProfilePhoto(params SetBusinessAccountProfileP
}
// SetBusinessAccountProfilePhotoWithContext is the context-aware variant of SetBusinessAccountProfilePhoto.
// Since: Bot API 9.0
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#setbusinessaccountprofilephoto
func (api *API) SetBusinessAccountProfilePhotoWithContext(ctx context.Context, params SetBusinessAccountProfilePhoto) (bool, error) {
@@ -255,6 +288,7 @@ func (api *API) SetBusinessAccountProfilePhotoWithContext(ctx context.Context, p
}
// RemoveBusinessAccountProfilePhoto holds parameters for the removeBusinessAccountProfilePhoto method.
// Since: Bot API 9.0
// See https://core.telegram.org/bots/api#removebusinessaccountprofilephoto
type RemoveBusinessAccountProfilePhoto struct {
BusinessConnectionID string `json:"business_connection_id"`
@@ -262,6 +296,7 @@ type RemoveBusinessAccountProfilePhoto struct {
}
// RemoveBusinessAccountProfilePhoto removes the profile photo of a business account.
// Since: Bot API 9.0
// Returns true on success.
// See https://core.telegram.org/bots/api#removebusinessaccountprofilephoto
func (api *API) RemoveBusinessAccountProfilePhoto(params RemoveBusinessAccountProfilePhoto) (bool, error) {
@@ -270,6 +305,7 @@ func (api *API) RemoveBusinessAccountProfilePhoto(params RemoveBusinessAccountPr
}
// RemoveBusinessAccountProfilePhotoWithContext is the context-aware variant of RemoveBusinessAccountProfilePhoto.
// Since: Bot API 9.0
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#removebusinessaccountprofilephoto
func (api *API) RemoveBusinessAccountProfilePhotoWithContext(ctx context.Context, params RemoveBusinessAccountProfilePhoto) (bool, error) {
@@ -278,6 +314,7 @@ func (api *API) RemoveBusinessAccountProfilePhotoWithContext(ctx context.Context
}
// SetBusinessAccountGiftSettings holds parameters for the setBusinessAccountGiftSettings method.
// Since: Bot API 9.0
// See https://core.telegram.org/bots/api#setbusinessaccountgiftsettings
type SetBusinessAccountGiftSettings struct {
BusinessConnectionID string `json:"business_connection_id"`
@@ -286,6 +323,7 @@ type SetBusinessAccountGiftSettings struct {
}
// SetBusinessAccountGiftSettings sets gift settings for a business account.
// Since: Bot API 9.0
// Returns true on success.
// See https://core.telegram.org/bots/api#setbusinessaccountgiftsettings
func (api *API) SetBusinessAccountGiftSettings(params SetBusinessAccountGiftSettings) (bool, error) {
@@ -294,6 +332,7 @@ func (api *API) SetBusinessAccountGiftSettings(params SetBusinessAccountGiftSett
}
// SetBusinessAccountGiftSettingsWithContext is the context-aware variant of SetBusinessAccountGiftSettings.
// Since: Bot API 9.0
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#setbusinessaccountgiftsettings
func (api *API) SetBusinessAccountGiftSettingsWithContext(ctx context.Context, params SetBusinessAccountGiftSettings) (bool, error) {
@@ -302,12 +341,14 @@ func (api *API) SetBusinessAccountGiftSettingsWithContext(ctx context.Context, p
}
// GetBusinessAccountStarBalance holds parameters for the getBusinessAccountStarBalance method.
// Since: Bot API 9.0
// See https://core.telegram.org/bots/api#getbusinessaccountstarbalance
type GetBusinessAccountStarBalance struct {
BusinessConnectionID string `json:"business_connection_id"`
}
// GetBusinessAccountStarBalance returns the star balance of a business account.
// Since: Bot API 9.0
// See https://core.telegram.org/bots/api#getbusinessaccountstarbalance
func (api *API) GetBusinessAccountStarBalance(params GetBusinessAccountStarBalance) (StarAmount, error) {
req := NewRequest[StarAmount]("getBusinessAccountStarBalance", params)
@@ -315,6 +356,7 @@ func (api *API) GetBusinessAccountStarBalance(params GetBusinessAccountStarBalan
}
// GetBusinessAccountStarBalanceWithContext is the context-aware variant of GetBusinessAccountStarBalance.
// Since: Bot API 9.0
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#getbusinessaccountstarbalance
func (api *API) GetBusinessAccountStarBalanceWithContext(ctx context.Context, params GetBusinessAccountStarBalance) (StarAmount, error) {
@@ -323,6 +365,7 @@ func (api *API) GetBusinessAccountStarBalanceWithContext(ctx context.Context, pa
}
// TransferBusinessAccountStars holds parameters for the transferBusinessAccountStars method.
// Since: Bot API 9.0
// See https://core.telegram.org/bots/api#transferbusinessaccountstars
type TransferBusinessAccountStars struct {
BusinessConnectionID string `json:"business_connection_id"`
@@ -330,6 +373,7 @@ type TransferBusinessAccountStars struct {
}
// TransferBusinessAccountStars transfers stars from a business account.
// Since: Bot API 9.0
// Returns true on success.
// See https://core.telegram.org/bots/api#transferbusinessaccountstars
func (api *API) TransferBusinessAccountStars(params TransferBusinessAccountStars) (bool, error) {
@@ -338,6 +382,7 @@ func (api *API) TransferBusinessAccountStars(params TransferBusinessAccountStars
}
// TransferBusinessAccountStarsWithContext is the context-aware variant of TransferBusinessAccountStars.
// Since: Bot API 9.0
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#transferbusinessaccountstars
func (api *API) TransferBusinessAccountStarsWithContext(ctx context.Context, params TransferBusinessAccountStars) (bool, error) {
@@ -346,6 +391,7 @@ func (api *API) TransferBusinessAccountStarsWithContext(ctx context.Context, par
}
// GetBusinessAccountGifts holds parameters for the getBusinessAccountGifts method.
// Since: Bot API 9.0
// See https://core.telegram.org/bots/api#getbusinessaccountgifts
type GetBusinessAccountGifts struct {
BusinessConnectionID string `json:"business_connection_id"`
@@ -362,6 +408,7 @@ type GetBusinessAccountGifts struct {
}
// GetBusinessAccountGifts returns gifts owned by a business account.
// Since: Bot API 9.0
// See https://core.telegram.org/bots/api#getbusinessaccountgifts
func (api *API) GetBusinessAccountGifts(params GetBusinessAccountGifts) (OwnedGifts, error) {
req := NewRequest[OwnedGifts]("getBusinessAccountGifts", params)
@@ -369,6 +416,7 @@ func (api *API) GetBusinessAccountGifts(params GetBusinessAccountGifts) (OwnedGi
}
// GetBusinessAccountGiftsWithContext is the context-aware variant of GetBusinessAccountGifts.
// Since: Bot API 9.0
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#getbusinessaccountgifts
func (api *API) GetBusinessAccountGiftsWithContext(ctx context.Context, params GetBusinessAccountGifts) (OwnedGifts, error) {
@@ -377,6 +425,7 @@ func (api *API) GetBusinessAccountGiftsWithContext(ctx context.Context, params G
}
// ConvertGiftToStars holds parameters for the convertGiftToStars method.
// Since: Bot API 9.0
// See https://core.telegram.org/bots/api#convertgifttostars
type ConvertGiftToStars struct {
BusinessConnectionID string `json:"business_connection_id"`
@@ -384,6 +433,7 @@ type ConvertGiftToStars struct {
}
// ConvertGiftToStars converts a gift to Telegram Stars.
// Since: Bot API 9.0
// Returns true on success.
// See https://core.telegram.org/bots/api#convertgifttostars
func (api *API) ConvertGiftToStars(params ConvertGiftToStars) (bool, error) {
@@ -392,6 +442,7 @@ func (api *API) ConvertGiftToStars(params ConvertGiftToStars) (bool, error) {
}
// ConvertGiftToStarsWithContext is the context-aware variant of ConvertGiftToStars.
// Since: Bot API 9.0
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#convertgifttostars
func (api *API) ConvertGiftToStarsWithContext(ctx context.Context, params ConvertGiftToStars) (bool, error) {
@@ -400,6 +451,7 @@ func (api *API) ConvertGiftToStarsWithContext(ctx context.Context, params Conver
}
// UpgradeGift holds parameters for the upgradeGift method.
// Since: Bot API 9.0
// See https://core.telegram.org/bots/api#upgradegift
type UpgradeGift struct {
BusinessConnectionID string `json:"business_connection_id"`
@@ -409,6 +461,7 @@ type UpgradeGift struct {
}
// UpgradeGift upgrades a gift.
// Since: Bot API 9.0
// Returns true on success.
// See https://core.telegram.org/bots/api#upgradegift
func (api *API) UpgradeGift(params UpgradeGift) (bool, error) {
@@ -417,6 +470,7 @@ func (api *API) UpgradeGift(params UpgradeGift) (bool, error) {
}
// UpgradeGiftWithContext is the context-aware variant of UpgradeGift.
// Since: Bot API 9.0
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#upgradegift
func (api *API) UpgradeGiftWithContext(ctx context.Context, params UpgradeGift) (bool, error) {
@@ -425,6 +479,7 @@ func (api *API) UpgradeGiftWithContext(ctx context.Context, params UpgradeGift)
}
// TransferGift holds parameters for the transferGift method.
// Since: Bot API 9.0
// See https://core.telegram.org/bots/api#transfergift
type TransferGift struct {
BusinessConnectionID string `json:"business_connection_id"`
@@ -434,6 +489,7 @@ type TransferGift struct {
}
// TransferGift transfers a gift to another chat.
// Since: Bot API 9.0
// Returns true on success.
// See https://core.telegram.org/bots/api#transfergift
func (api *API) TransferGift(params TransferGift) (bool, error) {
@@ -442,6 +498,7 @@ func (api *API) TransferGift(params TransferGift) (bool, error) {
}
// TransferGiftWithContext is the context-aware variant of TransferGift.
// Since: Bot API 9.0
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#transfergift
func (api *API) TransferGiftWithContext(ctx context.Context, params TransferGift) (bool, error) {
@@ -450,6 +507,7 @@ func (api *API) TransferGiftWithContext(ctx context.Context, params TransferGift
}
// PostStory holds parameters for the postStory method.
// Since: Bot API 7.2
// See https://core.telegram.org/bots/api#poststory
type PostStory struct {
BusinessConnectionID string `json:"business_connection_id"`
@@ -466,13 +524,15 @@ type PostStory struct {
}
// PostStory posts a story with a photo.
// Since: Bot API 7.2
// See https://core.telegram.org/bots/api#poststory
func (api *API) PostStory(params PostStory) (Story, error) {
req := NewRequest[Story]("postStory", params)
return req.Do(api)
}
// PostStoryWithContext is the context-aware variant of PostStoryPhoto.
// PostStoryWithContext is the context-aware variant of PostStory.
// Since: Bot API 7.2
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#poststory
func (api *API) PostStoryWithContext(ctx context.Context, params PostStory) (Story, error) {
@@ -481,6 +541,7 @@ func (api *API) PostStoryWithContext(ctx context.Context, params PostStory) (Sto
}
// RepostStory holds parameters for the repostStory method.
// Since: Bot API 7.2
// See https://core.telegram.org/bots/api#repoststory
type RepostStory struct {
BusinessConnectionID string `json:"business_connection_id"`
@@ -492,6 +553,7 @@ type RepostStory struct {
}
// RepostStory reposts a story from another chat.
// Since: Bot API 7.2
// Returns the reposted story.
// See https://core.telegram.org/bots/api#repoststory
func (api *API) RepostStory(params RepostStory) (Story, error) {
@@ -500,6 +562,7 @@ func (api *API) RepostStory(params RepostStory) (Story, error) {
}
// RepostStoryWithContext is the context-aware variant of RepostStory.
// Since: Bot API 7.2
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#repoststory
func (api *API) RepostStoryWithContext(ctx context.Context, params RepostStory) (Story, error) {
@@ -508,6 +571,7 @@ func (api *API) RepostStoryWithContext(ctx context.Context, params RepostStory)
}
// EditStory holds parameters for the editStory method.
// Since: Bot API 7.2
// See https://core.telegram.org/bots/api#editstory
type EditStory struct {
BusinessConnectionID string `json:"business_connection_id"`
@@ -521,6 +585,7 @@ type EditStory struct {
}
// EditStory edits an existing story.
// Since: Bot API 7.2
// Returns the updated story.
// See https://core.telegram.org/bots/api#editstory
func (api *API) EditStory(params EditStory) (Story, error) {
@@ -529,6 +594,7 @@ func (api *API) EditStory(params EditStory) (Story, error) {
}
// EditStoryWithContext is the context-aware variant of EditStory.
// Since: Bot API 7.2
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#editstory
func (api *API) EditStoryWithContext(ctx context.Context, params EditStory) (Story, error) {
@@ -537,6 +603,7 @@ func (api *API) EditStoryWithContext(ctx context.Context, params EditStory) (Sto
}
// DeleteStory holds parameters for the deleteStory method.
// Since: Bot API 7.2
// See https://core.telegram.org/bots/api#deletestory
type DeleteStory struct {
BusinessConnectionID string `json:"business_connection_id"`
@@ -544,6 +611,7 @@ type DeleteStory struct {
}
// DeleteStory deletes a story.
// Since: Bot API 7.2
// Returns true on success.
// See https://core.telegram.org/bots/api#deletestory
func (api *API) DeleteStory(params DeleteStory) (bool, error) {
@@ -552,6 +620,7 @@ func (api *API) DeleteStory(params DeleteStory) (bool, error) {
}
// DeleteStoryWithContext is the context-aware variant of DeleteStory.
// Since: Bot API 7.2
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#deletestory
func (api *API) DeleteStoryWithContext(ctx context.Context, params DeleteStory) (bool, error) {
+11 -1
View File
@@ -1,6 +1,7 @@
package tgapi
// BusinessIntro contains information about the business intro.
// Since: Bot API 7.2
// See https://core.telegram.org/bots/api#businessintro
type BusinessIntro struct {
Title string `json:"title,omitempty"`
@@ -9,6 +10,7 @@ type BusinessIntro struct {
}
// BusinessLocation contains information about the business location.
// Since: Bot API 7.2
// See https://core.telegram.org/bots/api#businesslocation
type BusinessLocation struct {
Address string `json:"address"`
@@ -16,6 +18,7 @@ type BusinessLocation struct {
}
// BusinessOpeningHoursInterval represents an interval of opening hours.
// Since: Bot API 7.2
// See https://core.telegram.org/bots/api#businessopeninghoursinterval
type BusinessOpeningHoursInterval struct {
OpeningMinute int `json:"opening_minute"`
@@ -23,6 +26,7 @@ type BusinessOpeningHoursInterval struct {
}
// BusinessOpeningHours represents the opening hours of a business.
// Since: Bot API 7.2
// See https://core.telegram.org/bots/api#businessopeninghours
type BusinessOpeningHours struct {
TimeZoneName string `json:"time_zone_name"`
@@ -31,6 +35,7 @@ type BusinessOpeningHours struct {
// BusinessBotRights represents the rights of a business bot.
// All fields are optional booleans that, when present, are always true.
// Since: Bot API 9.0
// See https://core.telegram.org/bots/api#businessbotrights
type BusinessBotRights struct {
CanReply *bool `json:"can_reply,omitempty"`
@@ -50,6 +55,7 @@ type BusinessBotRights struct {
}
// BusinessConnection contains information about a business connection.
// Since: Bot API 7.2
// See https://core.telegram.org/bots/api#businessconnection
type BusinessConnection struct {
ID string `json:"id"`
@@ -61,6 +67,7 @@ type BusinessConnection struct {
}
// BusinessMessagesDeleted is received when messages are deleted from a connected business account.
// Since: Bot API 7.2
// See https://core.telegram.org/bots/api#businessmessagesdeleted
type BusinessMessagesDeleted struct {
BusinessConnectionID string `json:"business_connection_id"`
@@ -79,6 +86,7 @@ const (
)
// InputStoryContent represents the content of a story to be posted.
// Since: Bot API 9.0
// See https://core.telegram.org/bots/api#inputstorycontent
type InputStoryContent struct {
Type InputStoryContentType `json:"type"`
@@ -94,6 +102,7 @@ type InputStoryContent struct {
}
// StoryAreaPosition describes the position of a clickable area on a story.
// Since: Bot API 9.0
// See https://core.telegram.org/bots/api#storyareaposition
type StoryAreaPosition struct {
XPercentage float64 `json:"x_percentage"`
@@ -121,7 +130,7 @@ const (
)
// StoryAreaType describes the type of a clickable area on a story.
// Fields should be set according to the Type.
// Since: Bot API 9.0
// See https://core.telegram.org/bots/api#storyareatype
type StoryAreaType struct {
Type StoryAreaTypeType `json:"type"`
@@ -149,6 +158,7 @@ type StoryAreaType struct {
}
// StoryArea represents a clickable area on a story.
// Since: Bot API 9.0
// See https://core.telegram.org/bots/api#storyarea
type StoryArea struct {
Position StoryAreaPosition `json:"position"`
+117 -18
View File
@@ -3,6 +3,7 @@ package tgapi
import "context"
// BanChatMember holds parameters for the banChatMember method.
// Since: Bot API 5.3
// See https://core.telegram.org/bots/api#banchatmember
type BanChatMember struct {
ChatID int64 `json:"chat_id"`
@@ -12,6 +13,7 @@ type BanChatMember struct {
}
// BanChatMember bans a user in a chat.
// Since: Bot API 5.3
// Returns True on success.
// See https://core.telegram.org/bots/api#banchatmember
func (api *API) BanChatMember(params BanChatMember) (bool, error) {
@@ -20,6 +22,7 @@ func (api *API) BanChatMember(params BanChatMember) (bool, error) {
}
// BanChatMemberWithContext is the context-aware variant of BanChatMember.
// Since: Bot API 5.3
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#banchatmember
func (api *API) BanChatMemberWithContext(ctx context.Context, params BanChatMember) (bool, error) {
@@ -28,6 +31,7 @@ func (api *API) BanChatMemberWithContext(ctx context.Context, params BanChatMemb
}
// UnbanChatMember holds parameters for the unbanChatMember method.
// Since: Bot API 2.0
// See https://core.telegram.org/bots/api#unbanchatmember
type UnbanChatMember struct {
ChatID int64 `json:"chat_id"`
@@ -36,6 +40,7 @@ type UnbanChatMember struct {
}
// UnbanChatMember unbans a previously banned user in a chat.
// Since: Bot API 2.0
// Returns True on success.
// See https://core.telegram.org/bots/api#unbanchatmember
func (api *API) UnbanChatMember(params UnbanChatMember) (bool, error) {
@@ -44,6 +49,7 @@ func (api *API) UnbanChatMember(params UnbanChatMember) (bool, error) {
}
// UnbanChatMemberWithContext is the context-aware variant of UnbanChatMember.
// Since: Bot API 2.0
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#unbanchatmember
func (api *API) UnbanChatMemberWithContext(ctx context.Context, params UnbanChatMember) (bool, error) {
@@ -52,6 +58,7 @@ func (api *API) UnbanChatMemberWithContext(ctx context.Context, params UnbanChat
}
// RestrictChatMember holds parameters for the restrictChatMember method.
// Since: Bot API 3.1
// See https://core.telegram.org/bots/api#restrictchatmember
type RestrictChatMember struct {
ChatID int64 `json:"chat_id"`
@@ -62,6 +69,7 @@ type RestrictChatMember struct {
}
// RestrictChatMember restricts a user in a chat.
// Since: Bot API 3.1
// Returns True on success.
// See https://core.telegram.org/bots/api#restrictchatmember
func (api *API) RestrictChatMember(params RestrictChatMember) (bool, error) {
@@ -70,6 +78,7 @@ func (api *API) RestrictChatMember(params RestrictChatMember) (bool, error) {
}
// RestrictChatMemberWithContext is the context-aware variant of RestrictChatMember.
// Since: Bot API 3.1
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#restrictchatmember
func (api *API) RestrictChatMemberWithContext(ctx context.Context, params RestrictChatMember) (bool, error) {
@@ -78,31 +87,33 @@ func (api *API) RestrictChatMemberWithContext(ctx context.Context, params Restri
}
// PromoteChatMember holds parameters for the promoteChatMember method.
// Since: Bot API 3.1
// See https://core.telegram.org/bots/api#promotechatmember
type PromoteChatMember struct {
ChatID int64 `json:"chat_id"`
UserID int64 `json:"user_id"`
IsAnonymous bool `json:"is_anonymous,omitempty"`
IsAnonymous bool `json:"is_anonymous,omitempty"` // Since: Bot API 5.1
CanManageChat bool `json:"can_manage_chat,omitempty"`
CanManageChat bool `json:"can_manage_chat,omitempty"` // Since: Bot API 5.3
CanDeleteMessages bool `json:"can_delete_messages,omitempty"`
CanManageVideoChats bool `json:"can_manage_video_chats,omitempty"`
CanManageVideoChats bool `json:"can_manage_video_chats,omitempty"` // Since: Bot API 6.0
CanRestrictMembers bool `json:"can_restrict_members,omitempty"`
CanPromoteMembers bool `json:"can_promote_members,omitempty"`
CanChangeInfo bool `json:"can_change_info,omitempty"`
CanInviteUsers bool `json:"can_invite_users,omitempty"`
CanPostStories bool `json:"can_post_stories,omitempty"`
CanEditStories bool `json:"can_edit_stories,omitempty"`
CanDeleteStories bool `json:"can_delete_stories,omitempty"`
CanPostStories bool `json:"can_post_stories,omitempty"` // Since: Bot API 6.9
CanEditStories bool `json:"can_edit_stories,omitempty"` // Since: Bot API 6.9
CanDeleteStories bool `json:"can_delete_stories,omitempty"` // Since: Bot API 6.9
CanPostMessages bool `json:"can_post_messages,omitempty"`
CanEditMessages bool `json:"can_edit_messages,omitempty"`
CanPinMessages bool `json:"can_pin_messages,omitempty"`
CanManageTopics bool `json:"can_manage_topics,omitempty"`
CanManageDirectMessages bool `json:"can_manage_direct_messages,omitempty"`
CanManageTags bool `json:"can_manage_tags,omitempty"`
CanManageTopics bool `json:"can_manage_topics,omitempty"` // Since: Bot API 6.3
CanManageDirectMessages bool `json:"can_manage_direct_messages,omitempty"` // Since: Bot API 9.1
CanManageTags bool `json:"can_manage_tags,omitempty"` // Since: Bot API 9.5
}
// PromoteChatMember promotes or demotes a user in a chat.
// Since: Bot API 3.1
// Returns True on success.
// See https://core.telegram.org/bots/api#promotechatmember
func (api *API) PromoteChatMember(params PromoteChatMember) (bool, error) {
@@ -111,6 +122,7 @@ func (api *API) PromoteChatMember(params PromoteChatMember) (bool, error) {
}
// PromoteChatMemberWithContext is the context-aware variant of PromoteChatMember.
// Since: Bot API 3.1
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#promotechatmember
func (api *API) PromoteChatMemberWithContext(ctx context.Context, params PromoteChatMember) (bool, error) {
@@ -119,6 +131,7 @@ func (api *API) PromoteChatMemberWithContext(ctx context.Context, params Promote
}
// SetChatAdministratorCustomTitle holds parameters for the setChatAdministratorCustomTitle method.
// Since: Bot API 5.0
// See https://core.telegram.org/bots/api#setchatadministratorcustomtitle
type SetChatAdministratorCustomTitle struct {
ChatID int64 `json:"chat_id"`
@@ -127,6 +140,7 @@ type SetChatAdministratorCustomTitle struct {
}
// SetChatAdministratorCustomTitle sets a custom title for an administrator.
// Since: Bot API 5.0
// Returns True on success.
// See https://core.telegram.org/bots/api#setchatadministratorcustomtitle
func (api *API) SetChatAdministratorCustomTitle(params SetChatAdministratorCustomTitle) (bool, error) {
@@ -135,6 +149,7 @@ func (api *API) SetChatAdministratorCustomTitle(params SetChatAdministratorCusto
}
// SetChatAdministratorCustomTitleWithContext is the context-aware variant of SetChatAdministratorCustomTitle.
// Since: Bot API 5.0
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#setchatadministratorcustomtitle
func (api *API) SetChatAdministratorCustomTitleWithContext(ctx context.Context, params SetChatAdministratorCustomTitle) (bool, error) {
@@ -143,6 +158,7 @@ func (api *API) SetChatAdministratorCustomTitleWithContext(ctx context.Context,
}
// SetChatMemberTag holds parameters for the setChatMemberTag method.
// Since: Bot API 9.5
// See https://core.telegram.org/bots/api#setchatmembertag
type SetChatMemberTag struct {
ChatID int64 `json:"chat_id"`
@@ -151,6 +167,7 @@ type SetChatMemberTag struct {
}
// SetChatMemberTag sets a tag for a chat member.
// Since: Bot API 9.5
// Returns True on success.
// See https://core.telegram.org/bots/api#setchatmembertag
func (api *API) SetChatMemberTag(params SetChatMemberTag) (bool, error) {
@@ -159,6 +176,7 @@ func (api *API) SetChatMemberTag(params SetChatMemberTag) (bool, error) {
}
// SetChatMemberTagWithContext is the context-aware variant of SetChatMemberTag.
// Since: Bot API 9.5
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#setchatmembertag
func (api *API) SetChatMemberTagWithContext(ctx context.Context, params SetChatMemberTag) (bool, error) {
@@ -167,6 +185,7 @@ func (api *API) SetChatMemberTagWithContext(ctx context.Context, params SetChatM
}
// BanChatSenderChat holds parameters for the banChatSenderChat method.
// Since: Bot API 5.6
// See https://core.telegram.org/bots/api#banchatsenderchat
type BanChatSenderChat struct {
ChatID int64 `json:"chat_id"`
@@ -174,6 +193,7 @@ type BanChatSenderChat struct {
}
// BanChatSenderChat bans a channel chat in a supergroup or channel.
// Since: Bot API 5.6
// Returns True on success.
// See https://core.telegram.org/bots/api#banchatsenderchat
func (api *API) BanChatSenderChat(params BanChatSenderChat) (bool, error) {
@@ -182,6 +202,7 @@ func (api *API) BanChatSenderChat(params BanChatSenderChat) (bool, error) {
}
// BanChatSenderChatWithContext is the context-aware variant of BanChatSenderChat.
// Since: Bot API 5.6
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#banchatsenderchat
func (api *API) BanChatSenderChatWithContext(ctx context.Context, params BanChatSenderChat) (bool, error) {
@@ -190,6 +211,7 @@ func (api *API) BanChatSenderChatWithContext(ctx context.Context, params BanChat
}
// UnbanChatSenderChat holds parameters for the unbanChatSenderChat method.
// Since: Bot API 5.6
// See https://core.telegram.org/bots/api#unbanchatsenderchat
type UnbanChatSenderChat struct {
ChatID int64 `json:"chat_id"`
@@ -197,6 +219,7 @@ type UnbanChatSenderChat struct {
}
// UnbanChatSenderChat unbans a previously banned channel chat.
// Since: Bot API 5.6
// Returns True on success.
// See https://core.telegram.org/bots/api#unbanchatsenderchat
func (api *API) UnbanChatSenderChat(params UnbanChatSenderChat) (bool, error) {
@@ -205,6 +228,7 @@ func (api *API) UnbanChatSenderChat(params UnbanChatSenderChat) (bool, error) {
}
// UnbanChatSenderChatWithContext is the context-aware variant of UnbanChatSenderChat.
// Since: Bot API 5.6
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#unbanchatsenderchat
func (api *API) UnbanChatSenderChatWithContext(ctx context.Context, params UnbanChatSenderChat) (bool, error) {
@@ -213,6 +237,7 @@ func (api *API) UnbanChatSenderChatWithContext(ctx context.Context, params Unban
}
// SetChatPermissions holds parameters for the setChatPermissions method.
// Since: Bot API 4.4
// See https://core.telegram.org/bots/api#setchatpermissions
type SetChatPermissions struct {
ChatID int64 `json:"chat_id"`
@@ -221,6 +246,7 @@ type SetChatPermissions struct {
}
// SetChatPermissions sets default chat permissions for all members.
// Since: Bot API 4.4
// Returns True on success.
// See https://core.telegram.org/bots/api#setchatpermissions
func (api *API) SetChatPermissions(params SetChatPermissions) (bool, error) {
@@ -229,6 +255,7 @@ func (api *API) SetChatPermissions(params SetChatPermissions) (bool, error) {
}
// SetChatPermissionsWithContext is the context-aware variant of SetChatPermissions.
// Since: Bot API 4.4
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#setchatpermissions
func (api *API) SetChatPermissionsWithContext(ctx context.Context, params SetChatPermissions) (bool, error) {
@@ -237,12 +264,14 @@ func (api *API) SetChatPermissionsWithContext(ctx context.Context, params SetCha
}
// ExportChatInviteLink holds parameters for the exportChatInviteLink method.
// Since: Bot API 3.1
// See https://core.telegram.org/bots/api#exportchatinvitelink
type ExportChatInviteLink struct {
ChatID int64 `json:"chat_id"`
}
// ExportChatInviteLink generates a new primary invite link for a chat.
// Since: Bot API 3.1
// Returns the new invite link as string.
// See https://core.telegram.org/bots/api#exportchatinvitelink
func (api *API) ExportChatInviteLink(params ExportChatInviteLink) (string, error) {
@@ -251,6 +280,7 @@ func (api *API) ExportChatInviteLink(params ExportChatInviteLink) (string, error
}
// ExportChatInviteLinkWithContext is the context-aware variant of ExportChatInviteLink.
// Since: Bot API 3.1
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#exportchatinvitelink
func (api *API) ExportChatInviteLinkWithContext(ctx context.Context, params ExportChatInviteLink) (string, error) {
@@ -259,6 +289,7 @@ func (api *API) ExportChatInviteLinkWithContext(ctx context.Context, params Expo
}
// CreateChatInviteLink holds parameters for the createChatInviteLink method.
// Since: Bot API 5.1
// See https://core.telegram.org/bots/api#createchatinvitelink
type CreateChatInviteLink struct {
ChatID int64 `json:"chat_id"`
@@ -269,6 +300,7 @@ type CreateChatInviteLink struct {
}
// CreateChatInviteLink creates an additional invite link for a chat.
// Since: Bot API 5.1
// Returns the created invite link.
// See https://core.telegram.org/bots/api#createchatinvitelink
func (api *API) CreateChatInviteLink(params CreateChatInviteLink) (ChatInviteLink, error) {
@@ -277,6 +309,7 @@ func (api *API) CreateChatInviteLink(params CreateChatInviteLink) (ChatInviteLin
}
// CreateChatInviteLinkWithContext is the context-aware variant of CreateChatInviteLink.
// Since: Bot API 5.1
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#createchatinvitelink
func (api *API) CreateChatInviteLinkWithContext(ctx context.Context, params CreateChatInviteLink) (ChatInviteLink, error) {
@@ -285,6 +318,7 @@ func (api *API) CreateChatInviteLinkWithContext(ctx context.Context, params Crea
}
// EditChatInviteLink holds parameters for the editChatInviteLink method.
// Since: Bot API 5.1
// See https://core.telegram.org/bots/api#editchatinvitelink
type EditChatInviteLink struct {
ChatID int64 `json:"chat_id"`
@@ -297,6 +331,7 @@ type EditChatInviteLink struct {
}
// EditChatInviteLink edits a nonprimary invite link.
// Since: Bot API 5.1
// Returns the edited invite link.
// See https://core.telegram.org/bots/api#editchatinvitelink
func (api *API) EditChatInviteLink(params EditChatInviteLink) (ChatInviteLink, error) {
@@ -305,6 +340,7 @@ func (api *API) EditChatInviteLink(params EditChatInviteLink) (ChatInviteLink, e
}
// EditChatInviteLinkWithContext is the context-aware variant of EditChatInviteLink.
// Since: Bot API 5.1
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#editchatinvitelink
func (api *API) EditChatInviteLinkWithContext(ctx context.Context, params EditChatInviteLink) (ChatInviteLink, error) {
@@ -313,6 +349,7 @@ func (api *API) EditChatInviteLinkWithContext(ctx context.Context, params EditCh
}
// CreateChatSubscriptionInviteLink holds parameters for the createChatSubscriptionInviteLink method.
// Since: Bot API 8.0
// See https://core.telegram.org/bots/api#createchatsubscriptioninvitelink
type CreateChatSubscriptionInviteLink struct {
ChatID int64 `json:"chat_id"`
@@ -322,6 +359,7 @@ type CreateChatSubscriptionInviteLink struct {
}
// CreateChatSubscriptionInviteLink creates a subscription invite link for a channel chat.
// Since: Bot API 8.0
// Returns the created invite link.
// See https://core.telegram.org/bots/api#createchatsubscriptioninvitelink
func (api *API) CreateChatSubscriptionInviteLink(params CreateChatSubscriptionInviteLink) (ChatInviteLink, error) {
@@ -330,6 +368,7 @@ func (api *API) CreateChatSubscriptionInviteLink(params CreateChatSubscriptionIn
}
// CreateChatSubscriptionInviteLinkWithContext is the context-aware variant of CreateChatSubscriptionInviteLink.
// Since: Bot API 8.0
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#createchatsubscriptioninvitelink
func (api *API) CreateChatSubscriptionInviteLinkWithContext(ctx context.Context, params CreateChatSubscriptionInviteLink) (ChatInviteLink, error) {
@@ -338,6 +377,7 @@ func (api *API) CreateChatSubscriptionInviteLinkWithContext(ctx context.Context,
}
// EditChatSubscriptionInviteLink holds parameters for the editChatSubscriptionInviteLink method.
// Since: Bot API 8.0
// See https://core.telegram.org/bots/api#editchatsubscriptioninvitelink
type EditChatSubscriptionInviteLink struct {
ChatID int64 `json:"chat_id"`
@@ -346,6 +386,7 @@ type EditChatSubscriptionInviteLink struct {
}
// EditChatSubscriptionInviteLink edits a subscription invite link.
// Since: Bot API 8.0
// Returns the edited invite link.
// See https://core.telegram.org/bots/api#editchatsubscriptioninvitelink
func (api *API) EditChatSubscriptionInviteLink(params EditChatSubscriptionInviteLink) (ChatInviteLink, error) {
@@ -354,6 +395,7 @@ func (api *API) EditChatSubscriptionInviteLink(params EditChatSubscriptionInvite
}
// EditChatSubscriptionInviteLinkWithContext is the context-aware variant of EditChatSubscriptionInviteLink.
// Since: Bot API 8.0
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#editchatsubscriptioninvitelink
func (api *API) EditChatSubscriptionInviteLinkWithContext(ctx context.Context, params EditChatSubscriptionInviteLink) (ChatInviteLink, error) {
@@ -362,6 +404,7 @@ func (api *API) EditChatSubscriptionInviteLinkWithContext(ctx context.Context, p
}
// RevokeChatInviteLink holds parameters for the revokeChatInviteLink method.
// Since: Bot API 5.1
// See https://core.telegram.org/bots/api#revokechatinvitelink
type RevokeChatInviteLink struct {
ChatID int64 `json:"chat_id"`
@@ -369,6 +412,7 @@ type RevokeChatInviteLink struct {
}
// RevokeChatInviteLink revokes an invite link.
// Since: Bot API 5.1
// Returns the revoked invite link object.
// See https://core.telegram.org/bots/api#revokechatinvitelink
func (api *API) RevokeChatInviteLink(params RevokeChatInviteLink) (ChatInviteLink, error) {
@@ -377,6 +421,7 @@ func (api *API) RevokeChatInviteLink(params RevokeChatInviteLink) (ChatInviteLin
}
// RevokeChatInviteLinkWithContext is the context-aware variant of RevokeChatInviteLink.
// Since: Bot API 5.1
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#revokechatinvitelink
func (api *API) RevokeChatInviteLinkWithContext(ctx context.Context, params RevokeChatInviteLink) (ChatInviteLink, error) {
@@ -385,6 +430,7 @@ func (api *API) RevokeChatInviteLinkWithContext(ctx context.Context, params Revo
}
// ApproveChatJoinRequest holds parameters for the approveChatJoinRequest method.
// Since: Bot API 5.4
// See https://core.telegram.org/bots/api#approvechatjoinrequest
type ApproveChatJoinRequest struct {
ChatID int64 `json:"chat_id"`
@@ -392,6 +438,7 @@ type ApproveChatJoinRequest struct {
}
// ApproveChatJoinRequest approves a chat join request.
// Since: Bot API 5.4
// Returns True on success.
// See https://core.telegram.org/bots/api#approvechatjoinrequest
func (api *API) ApproveChatJoinRequest(params ApproveChatJoinRequest) (bool, error) {
@@ -400,6 +447,7 @@ func (api *API) ApproveChatJoinRequest(params ApproveChatJoinRequest) (bool, err
}
// ApproveChatJoinRequestWithContext is the context-aware variant of ApproveChatJoinRequest.
// Since: Bot API 5.4
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#approvechatjoinrequest
func (api *API) ApproveChatJoinRequestWithContext(ctx context.Context, params ApproveChatJoinRequest) (bool, error) {
@@ -408,6 +456,7 @@ func (api *API) ApproveChatJoinRequestWithContext(ctx context.Context, params Ap
}
// DeclineChatJoinRequest holds parameters for the declineChatJoinRequest method.
// Since: Bot API 5.4
// See https://core.telegram.org/bots/api#declinechatjoinrequest
type DeclineChatJoinRequest struct {
ChatID int64 `json:"chat_id"`
@@ -415,6 +464,7 @@ type DeclineChatJoinRequest struct {
}
// DeclineChatJoinRequest declines a chat join request.
// Since: Bot API 5.4
// Returns True on success.
// See https://core.telegram.org/bots/api#declinechatjoinrequest
func (api *API) DeclineChatJoinRequest(params DeclineChatJoinRequest) (bool, error) {
@@ -423,6 +473,7 @@ func (api *API) DeclineChatJoinRequest(params DeclineChatJoinRequest) (bool, err
}
// DeclineChatJoinRequestWithContext is the context-aware variant of DeclineChatJoinRequest.
// Since: Bot API 5.4
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#declinechatjoinrequest
func (api *API) DeclineChatJoinRequestWithContext(ctx context.Context, params DeclineChatJoinRequest) (bool, error) {
@@ -431,12 +482,14 @@ func (api *API) DeclineChatJoinRequestWithContext(ctx context.Context, params De
}
// SetChatPhoto holds parameters for the setChatPhoto method.
// Since: Bot API 3.1
// See https://core.telegram.org/bots/api#setchatphoto
type SetChatPhoto struct {
ChatID int64 `json:"chat_id"`
}
// SetChatPhoto changes the chat photo.
// Since: Bot API 3.1
// photo is the file to upload as the new photo.
// Returns True on success.
// See https://core.telegram.org/bots/api#setchatphoto
@@ -450,12 +503,14 @@ func (api *API) SetChatPhoto(params SetChatPhoto, photo UploaderFile) (bool, err
}
// DeleteChatPhoto holds parameters for the deleteChatPhoto method.
// Since: Bot API 3.1
// See https://core.telegram.org/bots/api#deletechatphoto
type DeleteChatPhoto struct {
ChatID int64 `json:"chat_id"`
}
// DeleteChatPhoto deletes a chat photo.
// Since: Bot API 3.1
// Returns True on success.
// See https://core.telegram.org/bots/api#deletechatphoto
func (api *API) DeleteChatPhoto(params DeleteChatPhoto) (bool, error) {
@@ -464,6 +519,7 @@ func (api *API) DeleteChatPhoto(params DeleteChatPhoto) (bool, error) {
}
// DeleteChatPhotoWithContext is the context-aware variant of DeleteChatPhoto.
// Since: Bot API 3.1
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#deletechatphoto
func (api *API) DeleteChatPhotoWithContext(ctx context.Context, params DeleteChatPhoto) (bool, error) {
@@ -472,6 +528,7 @@ func (api *API) DeleteChatPhotoWithContext(ctx context.Context, params DeleteCha
}
// SetChatTitle holds parameters for the setChatTitle method.
// Since: Bot API 3.1
// See https://core.telegram.org/bots/api#setchattitle
type SetChatTitle struct {
ChatID int64 `json:"chat_id"`
@@ -479,6 +536,7 @@ type SetChatTitle struct {
}
// SetChatTitle changes the chat title.
// Since: Bot API 3.1
// Returns True on success.
// See https://core.telegram.org/bots/api#setchattitle
func (api *API) SetChatTitle(params SetChatTitle) (bool, error) {
@@ -487,6 +545,7 @@ func (api *API) SetChatTitle(params SetChatTitle) (bool, error) {
}
// SetChatTitleWithContext is the context-aware variant of SetChatTitle.
// Since: Bot API 3.1
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#setchattitle
func (api *API) SetChatTitleWithContext(ctx context.Context, params SetChatTitle) (bool, error) {
@@ -495,6 +554,7 @@ func (api *API) SetChatTitleWithContext(ctx context.Context, params SetChatTitle
}
// SetChatDescription holds parameters for the setChatDescription method.
// Since: Bot API 3.1
// See https://core.telegram.org/bots/api#setchatdescription
type SetChatDescription struct {
ChatID int64 `json:"chat_id"`
@@ -502,6 +562,7 @@ type SetChatDescription struct {
}
// SetChatDescription changes the chat description.
// Since: Bot API 3.1
// Returns True on success.
// See https://core.telegram.org/bots/api#setchatdescription
func (api *API) SetChatDescription(params SetChatDescription) (bool, error) {
@@ -510,6 +571,7 @@ func (api *API) SetChatDescription(params SetChatDescription) (bool, error) {
}
// SetChatDescriptionWithContext is the context-aware variant of SetChatDescription.
// Since: Bot API 3.1
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#setchatdescription
func (api *API) SetChatDescriptionWithContext(ctx context.Context, params SetChatDescription) (bool, error) {
@@ -518,6 +580,7 @@ func (api *API) SetChatDescriptionWithContext(ctx context.Context, params SetCha
}
// PinChatMessage holds parameters for the pinChatMessage method.
// Since: Bot API 3.1
// See https://core.telegram.org/bots/api#pinchatmessage
type PinChatMessage struct {
BusinessConnectionID *string `json:"business_connection_id,omitempty"`
@@ -527,6 +590,7 @@ type PinChatMessage struct {
}
// PinChatMessage pins a message in a chat.
// Since: Bot API 3.1
// Returns True on success.
// See https://core.telegram.org/bots/api#pinchatmessage
func (api *API) PinChatMessage(params PinChatMessage) (bool, error) {
@@ -535,6 +599,7 @@ func (api *API) PinChatMessage(params PinChatMessage) (bool, error) {
}
// PinChatMessageWithContext is the context-aware variant of PinChatMessage.
// Since: Bot API 3.1
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#pinchatmessage
func (api *API) PinChatMessageWithContext(ctx context.Context, params PinChatMessage) (bool, error) {
@@ -543,6 +608,7 @@ func (api *API) PinChatMessageWithContext(ctx context.Context, params PinChatMes
}
// UnpinChatMessage holds parameters for the unpinChatMessage method.
// Since: Bot API 3.1
// See https://core.telegram.org/bots/api#unpinchatmessage
type UnpinChatMessage struct {
BusinessConnectionID *string `json:"business_connection_id,omitempty"`
@@ -551,6 +617,7 @@ type UnpinChatMessage struct {
}
// UnpinChatMessage unpins a message in a chat.
// Since: Bot API 3.1
// Returns True on success.
// See https://core.telegram.org/bots/api#unpinchatmessage
func (api *API) UnpinChatMessage(params UnpinChatMessage) (bool, error) {
@@ -559,6 +626,7 @@ func (api *API) UnpinChatMessage(params UnpinChatMessage) (bool, error) {
}
// UnpinChatMessageWithContext is the context-aware variant of UnpinChatMessage.
// Since: Bot API 3.1
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#unpinchatmessage
func (api *API) UnpinChatMessageWithContext(ctx context.Context, params UnpinChatMessage) (bool, error) {
@@ -567,12 +635,14 @@ func (api *API) UnpinChatMessageWithContext(ctx context.Context, params UnpinCha
}
// UnpinAllChatMessages holds parameters for the unpinAllChatMessages method.
// Since: Bot API 5.0
// See https://core.telegram.org/bots/api#unpinallchatmessages
type UnpinAllChatMessages struct {
ChatID int64 `json:"chat_id"`
}
// UnpinAllChatMessages unpins all pinned messages in a chat.
// Since: Bot API 5.0
// Returns True on success.
// See https://core.telegram.org/bots/api#unpinallchatmessages
func (api *API) UnpinAllChatMessages(params UnpinAllChatMessages) (bool, error) {
@@ -581,6 +651,7 @@ func (api *API) UnpinAllChatMessages(params UnpinAllChatMessages) (bool, error)
}
// UnpinAllChatMessagesWithContext is the context-aware variant of UnpinAllChatMessages.
// Since: Bot API 5.0
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#unpinallchatmessages
func (api *API) UnpinAllChatMessagesWithContext(ctx context.Context, params UnpinAllChatMessages) (bool, error) {
@@ -589,55 +660,64 @@ func (api *API) UnpinAllChatMessagesWithContext(ctx context.Context, params Unpi
}
// LeaveChat holds parameters for the leaveChat method.
// Since: Bot API 2.1
// See https://core.telegram.org/bots/api#leavechat
type LeaveChat struct {
ChatID int64 `json:"chat_id"`
}
// LeaveChat makes the bot leave a chat.
// Since: Bot API 2.1
// Returns True on success.
// See https://core.telegram.org/bots/api#leavechat
func (api *API) LeaveChat(params LeaveChat) (bool, error) {
req := NewRequestWithChatID[bool]("leaveChat", params, params.ChatID) // fixed method name
req := NewRequestWithChatID[bool]("leaveChat", params, params.ChatID)
return req.Do(api)
}
// LeaveChatWithContext is the context-aware variant of LeaveChat.
// Since: Bot API 2.1
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#leavechat
func (api *API) LeaveChatWithContext(ctx context.Context, params LeaveChat) (bool, error) {
req := NewRequestWithChatID[bool]("leaveChat", params, params.ChatID) // fixed method name
req := NewRequestWithChatID[bool]("leaveChat", params, params.ChatID)
return req.DoWithContext(ctx, api)
}
// GetChat holds parameters for the getChat method.
// Since: Bot API 2.1
// See https://core.telegram.org/bots/api#getchat
type GetChat struct {
ChatID int64 `json:"chat_id"`
}
// GetChat gets uptodate information about a chat.
// Since: Bot API 2.1
// See https://core.telegram.org/bots/api#getchat
func (api *API) GetChat(params GetChat) (ChatFullInfo, error) {
req := NewRequestWithChatID[ChatFullInfo]("getChat", params, params.ChatID) // fixed method name
req := NewRequestWithChatID[ChatFullInfo]("getChat", params, params.ChatID)
return req.Do(api)
}
// GetChatWithContext is the context-aware variant of GetChat.
// Since: Bot API 2.1
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#getchat
func (api *API) GetChatWithContext(ctx context.Context, params GetChat) (ChatFullInfo, error) {
req := NewRequestWithChatID[ChatFullInfo]("getChat", params, params.ChatID) // fixed method name
req := NewRequestWithChatID[ChatFullInfo]("getChat", params, params.ChatID)
return req.DoWithContext(ctx, api)
}
// GetChatAdministrators holds parameters for the getChatAdministrators method.
// Since: Bot API 2.1
// See https://core.telegram.org/bots/api#getchatadministrators
type GetChatAdministrators struct {
ChatID int64 `json:"chat_id"`
ChatID int64 `json:"chat_id"`
ReturnBots bool `json:"return_bots,omitempty"` // Since: Bot API 10.0
}
// GetChatAdministrators returns a list of administrators in a chat.
// Since: Bot API 2.1
// See https://core.telegram.org/bots/api#getchatadministrators
func (api *API) GetChatAdministrators(params GetChatAdministrators) ([]ChatMember, error) {
req := NewRequestWithChatID[[]ChatMember]("getChatAdministrators", params, params.ChatID)
@@ -645,6 +725,7 @@ func (api *API) GetChatAdministrators(params GetChatAdministrators) ([]ChatMembe
}
// GetChatAdministratorsWithContext is the context-aware variant of GetChatAdministrators.
// Since: Bot API 2.1
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#getchatadministrators
func (api *API) GetChatAdministratorsWithContext(ctx context.Context, params GetChatAdministrators) ([]ChatMember, error) {
@@ -652,28 +733,32 @@ func (api *API) GetChatAdministratorsWithContext(ctx context.Context, params Get
return req.DoWithContext(ctx, api)
}
// GetChatMembersCount holds parameters for the getChatMemberCount method.
// GetChatMemberCount holds parameters for the getChatMemberCount method.
// Since: Bot API 2.1
// See https://core.telegram.org/bots/api#getchatmembercount
type GetChatMembersCount struct {
type GetChatMemberCount struct {
ChatID int64 `json:"chat_id"`
}
// GetChatMemberCount returns the number of members in a chat.
// Since: Bot API 2.1
// See https://core.telegram.org/bots/api#getchatmembercount
func (api *API) GetChatMemberCount(params GetChatMembersCount) (int, error) {
func (api *API) GetChatMemberCount(params GetChatMemberCount) (int, error) {
req := NewRequestWithChatID[int]("getChatMemberCount", params, params.ChatID)
return req.Do(api)
}
// GetChatMemberCountWithContext is the context-aware variant of GetChatMemberCount.
// Since: Bot API 2.1
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#getchatmembercount
func (api *API) GetChatMemberCountWithContext(ctx context.Context, params GetChatMembersCount) (int, error) {
func (api *API) GetChatMemberCountWithContext(ctx context.Context, params GetChatMemberCount) (int, error) {
req := NewRequestWithChatID[int]("getChatMemberCount", params, params.ChatID)
return req.DoWithContext(ctx, api)
}
// GetChatMember holds parameters for the getChatMember method.
// Since: Bot API 2.1
// See https://core.telegram.org/bots/api#getchatmember
type GetChatMember struct {
ChatID int64 `json:"chat_id"`
@@ -681,6 +766,7 @@ type GetChatMember struct {
}
// GetChatMember returns information about a member of a chat.
// Since: Bot API 2.1
// See https://core.telegram.org/bots/api#getchatmember
func (api *API) GetChatMember(params GetChatMember) (ChatMember, error) {
req := NewRequestWithChatID[ChatMember]("getChatMember", params, params.ChatID)
@@ -688,6 +774,7 @@ func (api *API) GetChatMember(params GetChatMember) (ChatMember, error) {
}
// GetChatMemberWithContext is the context-aware variant of GetChatMember.
// Since: Bot API 2.1
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#getchatmember
func (api *API) GetChatMemberWithContext(ctx context.Context, params GetChatMember) (ChatMember, error) {
@@ -696,6 +783,7 @@ func (api *API) GetChatMemberWithContext(ctx context.Context, params GetChatMemb
}
// SetChatStickerSet holds parameters for the setChatStickerSet method.
// Since: Bot API 3.2
// See https://core.telegram.org/bots/api#setchatstickerset
type SetChatStickerSet struct {
ChatID int64 `json:"chat_id"`
@@ -703,6 +791,7 @@ type SetChatStickerSet struct {
}
// SetChatStickerSet associates a sticker set with a supergroup.
// Since: Bot API 3.2
// Returns True on success.
// See https://core.telegram.org/bots/api#setchatstickerset
func (api *API) SetChatStickerSet(params SetChatStickerSet) (bool, error) {
@@ -711,6 +800,7 @@ func (api *API) SetChatStickerSet(params SetChatStickerSet) (bool, error) {
}
// SetChatStickerSetWithContext is the context-aware variant of SetChatStickerSet.
// Since: Bot API 3.2
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#setchatstickerset
func (api *API) SetChatStickerSetWithContext(ctx context.Context, params SetChatStickerSet) (bool, error) {
@@ -719,12 +809,14 @@ func (api *API) SetChatStickerSetWithContext(ctx context.Context, params SetChat
}
// DeleteChatStickerSet holds parameters for the deleteChatStickerSet method.
// Since: Bot API 3.2
// See https://core.telegram.org/bots/api#deletechatstickerset
type DeleteChatStickerSet struct {
ChatID int64 `json:"chat_id"`
}
// DeleteChatStickerSet deletes a sticker set from a supergroup.
// Since: Bot API 3.2
// Returns True on success.
// See https://core.telegram.org/bots/api#deletechatstickerset
func (api *API) DeleteChatStickerSet(params DeleteChatStickerSet) (bool, error) {
@@ -733,6 +825,7 @@ func (api *API) DeleteChatStickerSet(params DeleteChatStickerSet) (bool, error)
}
// DeleteChatStickerSetWithContext is the context-aware variant of DeleteChatStickerSet.
// Since: Bot API 3.2
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#deletechatstickerset
func (api *API) DeleteChatStickerSetWithContext(ctx context.Context, params DeleteChatStickerSet) (bool, error) {
@@ -741,6 +834,7 @@ func (api *API) DeleteChatStickerSetWithContext(ctx context.Context, params Dele
}
// GetUserChatBoosts holds parameters for the getUserChatBoosts method.
// Since: Bot API 7.0
// See https://core.telegram.org/bots/api#getuserchatboosts
type GetUserChatBoosts struct {
ChatID int64 `json:"chat_id"`
@@ -748,6 +842,7 @@ type GetUserChatBoosts struct {
}
// GetUserChatBoosts returns the list of boosts a user has given to a chat.
// Since: Bot API 7.0
// See https://core.telegram.org/bots/api#getuserchatboosts
func (api *API) GetUserChatBoosts(params GetUserChatBoosts) (UserChatBoosts, error) {
req := NewRequestWithChatID[UserChatBoosts]("getUserChatBoosts", params, params.ChatID)
@@ -755,6 +850,7 @@ func (api *API) GetUserChatBoosts(params GetUserChatBoosts) (UserChatBoosts, err
}
// GetUserChatBoostsWithContext is the context-aware variant of GetUserChatBoosts.
// Since: Bot API 7.0
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#getuserchatboosts
func (api *API) GetUserChatBoostsWithContext(ctx context.Context, params GetUserChatBoosts) (UserChatBoosts, error) {
@@ -763,6 +859,7 @@ func (api *API) GetUserChatBoostsWithContext(ctx context.Context, params GetUser
}
// GetChatGifts holds parameters for the getChatGifts method.
// Since: Bot API 9.3
// See https://core.telegram.org/bots/api#getchatgifts
type GetChatGifts struct {
ChatID int64 `json:"chat_id"`
@@ -779,6 +876,7 @@ type GetChatGifts struct {
}
// GetChatGifts returns gifts owned by a chat.
// Since: Bot API 9.3
// See https://core.telegram.org/bots/api#getchatgifts
func (api *API) GetChatGifts(params GetChatGifts) (OwnedGifts, error) {
req := NewRequestWithChatID[OwnedGifts]("getChatGifts", params, params.ChatID)
@@ -786,6 +884,7 @@ func (api *API) GetChatGifts(params GetChatGifts) (OwnedGifts, error) {
}
// GetChatGiftsWithContext is the context-aware variant of GetChatGifts.
// Since: Bot API 9.3
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#getchatgifts
func (api *API) GetChatGiftsWithContext(ctx context.Context, params GetChatGifts) (OwnedGifts, error) {
+50 -28
View File
@@ -1,6 +1,7 @@
package tgapi
// Chat represents a chat (private, group, supergroup, channel).
// Since: Bot API 1.0
// See https://core.telegram.org/bots/api#chat
type Chat struct {
ID int64 `json:"id"`
@@ -9,8 +10,8 @@ type Chat struct {
Username *string `json:"username,omitempty"`
FirstName *string `json:"first_name,omitempty"`
LastName *string `json:"last_name,omitempty"`
IsForum *bool `json:"is_forum,omitempty"`
IsDirectMessages *bool `json:"is_direct_messages,omitempty"`
IsForum *bool `json:"is_forum,omitempty"` // Since: Bot API 6.3
IsDirectMessages *bool `json:"is_direct_messages,omitempty"` // Since: Bot API 9.2
}
// ChatType represents the type of a chat.
@@ -28,6 +29,7 @@ const (
)
// ChatFullInfo contains full information about a chat.
// Since: Bot API 7.5
// See https://core.telegram.org/bots/api#chatfullinfo
type ChatFullInfo struct {
ID int64 `json:"id"`
@@ -49,7 +51,7 @@ type ChatFullInfo struct {
BusinessOpeningHours *BusinessOpeningHours `json:"business_opening_hours,omitempty"`
PersonalChat *Chat `json:"personal_chat,omitempty"`
ParentChat *Chat `json:"parent_chat,omitempty"`
ParentChat *Chat `json:"parent_chat,omitempty"` // Since: Bot API 9.2
AvailableReaction []ReactionType `json:"available_reaction,omitempty"`
@@ -86,12 +88,13 @@ type ChatFullInfo struct {
Location *ChatLocation `json:"location,omitempty"`
Rating *UserRating `json:"rating,omitempty"`
FirstProfileAudio *Audio `json:"first_profile_audio,omitempty"`
UniqueGiftColors *UniqueGiftColors `json:"unique_gift_colors,omitempty"`
PaidMessageStarCount *int `json:"paid_message_star_count,omitempty"`
FirstProfileAudio *Audio `json:"first_profile_audio,omitempty"` // Since: Bot API 9.4
UniqueGiftColors *UniqueGiftColors `json:"unique_gift_colors,omitempty"` // Since: Bot API 9.3
PaidMessageStarCount *int `json:"paid_message_star_count,omitempty"` // Since: Bot API 9.3
}
// ChatPhoto represents a chat photo.
// Since: Bot API 3.1
// See https://core.telegram.org/bots/api#chatphoto
type ChatPhoto struct {
SmallFileID string `json:"small_file_id"`
@@ -101,26 +104,29 @@ type ChatPhoto struct {
}
// ChatPermissions describes actions that a nonadministrator user is allowed to take in a chat.
// Since: Bot API 4.4
// See https://core.telegram.org/bots/api#chatpermissions
type ChatPermissions struct {
CanSendMessages bool `json:"can_send_messages"`
CanSendAudios bool `json:"can_send_audios"`
CanSendDocuments bool `json:"can_send_documents"`
CanSendPhotos bool `json:"can_send_photos"`
CanSendVideos bool `json:"can_send_videos"`
CanSendVideoNotes bool `json:"can_send_video_notes"`
CanSendVoiceNotes bool `json:"can_send_voice_notes"`
CanSendAudios bool `json:"can_send_audios"` // Since: Bot API 6.5
CanSendDocuments bool `json:"can_send_documents"` // Since: Bot API 6.5
CanSendPhotos bool `json:"can_send_photos"` // Since: Bot API 6.5
CanSendVideos bool `json:"can_send_videos"` // Since: Bot API 6.5
CanSendVideoNotes bool `json:"can_send_video_notes"` // Since: Bot API 6.5
CanSendVoiceNotes bool `json:"can_send_voice_notes"` // Since: Bot API 6.5
CanSendPolls bool `json:"can_send_polls"`
CanSendOtherMessages bool `json:"can_send_other_messages"`
CanAddWebPagePreview bool `json:"can_add_web_page_previews"`
CanEditTag bool `json:"can_edit_tag"`
CanReactToMessages bool `json:"can_react_to_messages"` // Since: Bot API 10.0
CanEditTag bool `json:"can_edit_tag"` // Since: Bot API 9.5
CanChangeInfo bool `json:"can_change_info"`
CanInviteUsers bool `json:"can_invite_users"`
CanPinMessages bool `json:"can_pin_messages"`
CanManageTopics bool `json:"can_manage_topics"`
CanManageTopics bool `json:"can_manage_topics"` // Since: Bot API 6.3
}
// ChatLocation represents a location to which a chat is connected.
// Since: Bot API 5.0
// See https://core.telegram.org/bots/api#chatlocation
type ChatLocation struct {
Location Location `json:"location"`
@@ -128,6 +134,7 @@ type ChatLocation struct {
}
// ChatInviteLink represents an invite link for a chat.
// Since: Bot API 5.1
// See https://core.telegram.org/bots/api#chatinvitelink
type ChatInviteLink struct {
InviteLink string `json:"invite_link"`
@@ -163,11 +170,12 @@ const (
)
// ChatMember contains information about one member of a chat.
// Since: Bot API 3.1
// See https://core.telegram.org/bots/api#chatmember
type ChatMember struct {
Status ChatMemberStatusType `json:"status"`
User User `json:"user"`
Tag string `json:"tag,omitempty"`
Tag string `json:"tag,omitempty"` // Since: Bot API 9.5
// Owner
IsAnonymous *bool `json:"is_anonymous"`
@@ -182,16 +190,16 @@ type ChatMember struct {
CanPromoteMembers *bool `json:"can_promote_members,omitempty"`
CanChangeInfo *bool `json:"can_change_info,omitempty"`
CanInviteUsers *bool `json:"can_invite_users,omitempty"`
CanPostStories *bool `json:"can_post_stories,omitempty"`
CanEditStories *bool `json:"can_edit_stories,omitempty"`
CanDeleteStories *bool `json:"can_delete_stories,omitempty"`
CanPostStories *bool `json:"can_post_stories,omitempty"` // Since: Bot API 6.9
CanEditStories *bool `json:"can_edit_stories,omitempty"` // Since: Bot API 6.9
CanDeleteStories *bool `json:"can_delete_stories,omitempty"` // Since: Bot API 6.9
CanPostMessages *bool `json:"can_post_messages,omitempty"`
CanEditMessages *bool `json:"can_edit_messages,omitempty"`
CanPinMessages *bool `json:"can_pin_messages,omitempty"`
CanManageTopics *bool `json:"can_manage_topics,omitempty"`
CanManageDirectMessages *bool `json:"can_manage_direct_messages,omitempty"`
CanManageTags *bool `json:"can_manage_tags,omitempty"`
CanManageTopics *bool `json:"can_manage_topics,omitempty"` // Since: Bot API 6.3
CanManageDirectMessages *bool `json:"can_manage_direct_messages,omitempty"` // Since: Bot API 9.1
CanManageTags *bool `json:"can_manage_tags,omitempty"` // Since: Bot API 9.5
// Member
UntilDate *int `json:"until_date,omitempty"`
@@ -199,19 +207,21 @@ type ChatMember struct {
// Restricted
IsMember *bool `json:"is_member,omitempty"`
CanSendMessages *bool `json:"can_send_messages,omitempty"`
CanSendAudios *bool `json:"can_send_audios,omitempty"`
CanSendDocuments *bool `json:"can_send_documents,omitempty"`
CanSendPhotos *bool `json:"can_send_photos,omitempty"`
CanSendVideos *bool `json:"can_send_videos,omitempty"`
CanSendVideoNotes *bool `json:"can_send_video_notes,omitempty"`
CanSendVoiceNotes *bool `json:"can_send_voice_notes,omitempty"`
CanSendAudios *bool `json:"can_send_audios,omitempty"` // Since: Bot API 6.5
CanSendDocuments *bool `json:"can_send_documents,omitempty"` // Since: Bot API 6.5
CanSendPhotos *bool `json:"can_send_photos,omitempty"` // Since: Bot API 6.5
CanSendVideos *bool `json:"can_send_videos,omitempty"` // Since: Bot API 6.5
CanSendVideoNotes *bool `json:"can_send_video_notes,omitempty"` // Since: Bot API 6.5
CanSendVoiceNotes *bool `json:"can_send_voice_notes,omitempty"` // Since: Bot API 6.5
CanSendPolls *bool `json:"can_send_polls,omitempty"`
CanSendOtherMessages *bool `json:"can_send_other_messages,omitempty"`
CanAddWebPagePreview *bool `json:"can_add_web_page_previews,omitempty"`
CanEditTag *bool `json:"can_edit_tag,omitempty"`
CanReactToMessages *bool `json:"can_react_to_messages,omitempty"` // Since: Bot API 10.0
CanEditTag *bool `json:"can_edit_tag,omitempty"` // Since: Bot API 9.5
}
// ChatBoostSource describes the source of a chat boost.
// Since: Bot API 7.0
// See https://core.telegram.org/bots/api#chatboostsource
type ChatBoostSource struct {
Source string `json:"source"`
@@ -224,6 +234,7 @@ type ChatBoostSource struct {
}
// ChatBoost represents a boost added to a chat.
// Since: Bot API 7.0
// See https://core.telegram.org/bots/api#chatboost
type ChatBoost struct {
BoostID string `json:"boost_id"`
@@ -233,31 +244,40 @@ type ChatBoost struct {
}
// UserChatBoosts represents a list of boosts a user has given to a chat.
// Since: Bot API 7.0
// See https://core.telegram.org/bots/api#userchatboosts
type UserChatBoosts struct {
Boosts []ChatBoost `json:"boosts"`
}
// ChatBoostAdded describes a service message about a user boosting a chat.
// Since: Bot API 7.1
type ChatBoostAdded struct {
BoostCount int `json:"boost_count"`
}
// ChatBackground represents a chat background.
// Since: Bot API 7.5
type ChatBackground struct {
Type BackgroundType `json:"type"`
}
// ChatOwnerLeft describes a service message about a chat owner leaving.
// Since: Bot API 9.4
// See https://core.telegram.org/bots/api#chatownerleft
type ChatOwnerLeft struct {
NewOwner *User `json:"new_owner,omitempty"`
}
// ChatOwnerChanged describes a service message about a chat owner change.
// Since: Bot API 9.4
// See https://core.telegram.org/bots/api#chatownerchanged
type ChatOwnerChanged struct {
NewOwner User `json:"new_owner"`
}
// ChatAdministratorRights represents the rights of an administrator in a chat.
// Since: Bot API 6.0
// See https://core.telegram.org/bots/api#chatadministratorrights
type ChatAdministratorRights struct {
IsAnonymous bool `json:"is_anonymous"`
@@ -281,6 +301,7 @@ type ChatAdministratorRights struct {
}
// ChatBoostUpdated represents a boost added to a chat or changed.
// Since: Bot API 7.0
// See https://core.telegram.org/bots/api#chatboostupdated
type ChatBoostUpdated struct {
Chat Chat `json:"chat"`
@@ -288,6 +309,7 @@ type ChatBoostUpdated struct {
}
// ChatBoostRemoved represents a boost removed from a chat.
// Since: Bot API 7.0
// See https://core.telegram.org/bots/api#chatboostremoved
type ChatBoostRemoved struct {
Chat Chat `json:"chat"`
+19 -1
View File
@@ -1,6 +1,9 @@
package tgapi
import "errors"
import (
"errors"
"fmt"
)
// ErrPoolUnexpected reports an unexpected result type returned from the worker pool.
var ErrPoolUnexpected = errors.New("unexpected response from pool")
@@ -10,3 +13,18 @@ var ErrPoolQueueFull = errors.New("worker pool queue full")
// ErrPoolStopped reports that a request was submitted after the worker pool stopped.
var ErrPoolStopped = errors.New("worker pool stopped")
// ResponseError reports an unsuccessful Telegram API response.
type ResponseError struct {
Code int
Description string
Parameters *ResponseParameters
}
// Error returns the Telegram API error code and description.
func (e *ResponseError) Error() string {
if e == nil {
return "<nil>"
}
return fmt.Sprintf("[%d] %s", e.Code, e.Description)
}
+31
View File
@@ -3,12 +3,14 @@ package tgapi
import "context"
// BaseForumTopic contains common fields for forum topic operations that require a chat ID and a message thread ID.
// Since: Bot API 6.3
type BaseForumTopic struct {
ChatID int64 `json:"chat_id"`
MessageThreadID int `json:"message_thread_id"`
}
// GetForumTopicIconStickers returns the list of custom emoji that can be used as a forum topic icon.
// Since: Bot API 6.3
// See https://core.telegram.org/bots/api#getforumtopiciconstickers
func (api *API) GetForumTopicIconStickers() ([]Sticker, error) {
req := NewRequest[[]Sticker]("getForumTopicIconStickers", NoParams)
@@ -16,6 +18,7 @@ func (api *API) GetForumTopicIconStickers() ([]Sticker, error) {
}
// GetForumTopicIconStickersWithContext is the context-aware variant of GetForumTopicIconStickers.
// Since: Bot API 6.3
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#getforumtopiciconstickers
func (api *API) GetForumTopicIconStickersWithContext(ctx context.Context) ([]Sticker, error) {
@@ -24,6 +27,7 @@ func (api *API) GetForumTopicIconStickersWithContext(ctx context.Context) ([]Sti
}
// CreateForumTopic holds parameters for the createForumTopic method.
// Since: Bot API 6.3
// See https://core.telegram.org/bots/api#createforumtopic
type CreateForumTopic struct {
ChatID int64 `json:"chat_id"`
@@ -33,6 +37,7 @@ type CreateForumTopic struct {
}
// CreateForumTopic creates a topic in a forum supergroup.
// Since: Bot API 6.3
// Returns the created ForumTopic on success.
// See https://core.telegram.org/bots/api#createforumtopic
func (api *API) CreateForumTopic(params CreateForumTopic) (ForumTopic, error) {
@@ -41,6 +46,7 @@ func (api *API) CreateForumTopic(params CreateForumTopic) (ForumTopic, error) {
}
// CreateForumTopicWithContext is the context-aware variant of CreateForumTopic.
// Since: Bot API 6.3
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#createforumtopic
func (api *API) CreateForumTopicWithContext(ctx context.Context, params CreateForumTopic) (ForumTopic, error) {
@@ -49,6 +55,7 @@ func (api *API) CreateForumTopicWithContext(ctx context.Context, params CreateFo
}
// EditForumTopic holds parameters for the editForumTopic method.
// Since: Bot API 6.3
// See https://core.telegram.org/bots/api#editforumtopic
type EditForumTopic struct {
BaseForumTopic
@@ -57,6 +64,7 @@ type EditForumTopic struct {
}
// EditForumTopic edits name and icon of a forum topic.
// Since: Bot API 6.3
// Returns True on success.
// See https://core.telegram.org/bots/api#editforumtopic
func (api *API) EditForumTopic(params EditForumTopic) (bool, error) {
@@ -65,6 +73,7 @@ func (api *API) EditForumTopic(params EditForumTopic) (bool, error) {
}
// EditForumTopicWithContext is the context-aware variant of EditForumTopic.
// Since: Bot API 6.3
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#editforumtopic
func (api *API) EditForumTopicWithContext(ctx context.Context, params EditForumTopic) (bool, error) {
@@ -73,6 +82,7 @@ func (api *API) EditForumTopicWithContext(ctx context.Context, params EditForumT
}
// CloseForumTopic closes an open forum topic.
// Since: Bot API 6.3
// Returns True on success.
// See https://core.telegram.org/bots/api#closeforumtopic
func (api *API) CloseForumTopic(params BaseForumTopic) (bool, error) {
@@ -81,6 +91,7 @@ func (api *API) CloseForumTopic(params BaseForumTopic) (bool, error) {
}
// CloseForumTopicWithContext is the context-aware variant of CloseForumTopic.
// Since: Bot API 6.3
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#closeforumtopic
func (api *API) CloseForumTopicWithContext(ctx context.Context, params BaseForumTopic) (bool, error) {
@@ -89,6 +100,7 @@ func (api *API) CloseForumTopicWithContext(ctx context.Context, params BaseForum
}
// ReopenForumTopic reopens a closed forum topic.
// Since: Bot API 6.3
// Returns True on success.
// See https://core.telegram.org/bots/api#reopenforumtopic
func (api *API) ReopenForumTopic(params BaseForumTopic) (bool, error) {
@@ -97,6 +109,7 @@ func (api *API) ReopenForumTopic(params BaseForumTopic) (bool, error) {
}
// ReopenForumTopicWithContext is the context-aware variant of ReopenForumTopic.
// Since: Bot API 6.3
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#reopenforumtopic
func (api *API) ReopenForumTopicWithContext(ctx context.Context, params BaseForumTopic) (bool, error) {
@@ -105,6 +118,7 @@ func (api *API) ReopenForumTopicWithContext(ctx context.Context, params BaseForu
}
// DeleteForumTopic deletes a forum topic.
// Since: Bot API 6.3
// Returns True on success.
// See https://core.telegram.org/bots/api#deleteforumtopic
func (api *API) DeleteForumTopic(params BaseForumTopic) (bool, error) {
@@ -113,6 +127,7 @@ func (api *API) DeleteForumTopic(params BaseForumTopic) (bool, error) {
}
// DeleteForumTopicWithContext is the context-aware variant of DeleteForumTopic.
// Since: Bot API 6.3
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#deleteforumtopic
func (api *API) DeleteForumTopicWithContext(ctx context.Context, params BaseForumTopic) (bool, error) {
@@ -121,6 +136,7 @@ func (api *API) DeleteForumTopicWithContext(ctx context.Context, params BaseForu
}
// UnpinAllForumTopicMessages clears the list of pinned messages in a forum topic.
// Since: Bot API 6.3
// Returns True on success.
// See https://core.telegram.org/bots/api#unpinallforumtopicmessages
func (api *API) UnpinAllForumTopicMessages(params BaseForumTopic) (bool, error) {
@@ -129,6 +145,7 @@ func (api *API) UnpinAllForumTopicMessages(params BaseForumTopic) (bool, error)
}
// UnpinAllForumTopicMessagesWithContext is the context-aware variant of UnpinAllForumTopicMessages.
// Since: Bot API 6.3
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#unpinallforumtopicmessages
func (api *API) UnpinAllForumTopicMessagesWithContext(ctx context.Context, params BaseForumTopic) (bool, error) {
@@ -137,11 +154,13 @@ func (api *API) UnpinAllForumTopicMessagesWithContext(ctx context.Context, param
}
// BaseGeneralForumTopic contains common fields for general forum topic operations that require a chat ID.
// Since: Bot API 6.4
type BaseGeneralForumTopic struct {
ChatID int64 `json:"chat_id"`
}
// EditGeneralForumTopic holds parameters for the editGeneralForumTopic method.
// Since: Bot API 6.4
// See https://core.telegram.org/bots/api#editgeneralforumtopic
type EditGeneralForumTopic struct {
ChatID int64 `json:"chat_id"`
@@ -149,6 +168,7 @@ type EditGeneralForumTopic struct {
}
// EditGeneralForumTopic edits the name of the 'General' topic in a forum supergroup.
// Since: Bot API 6.4
// Returns True on success.
// See https://core.telegram.org/bots/api#editgeneralforumtopic
func (api *API) EditGeneralForumTopic(params EditGeneralForumTopic) (bool, error) {
@@ -157,6 +177,7 @@ func (api *API) EditGeneralForumTopic(params EditGeneralForumTopic) (bool, error
}
// EditGeneralForumTopicWithContext is the context-aware variant of EditGeneralForumTopic.
// Since: Bot API 6.4
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#editgeneralforumtopic
func (api *API) EditGeneralForumTopicWithContext(ctx context.Context, params EditGeneralForumTopic) (bool, error) {
@@ -165,6 +186,7 @@ func (api *API) EditGeneralForumTopicWithContext(ctx context.Context, params Edi
}
// CloseGeneralForumTopic closes the 'General' topic in a forum supergroup.
// Since: Bot API 6.4
// Returns True on success.
// See https://core.telegram.org/bots/api#closegeneralforumtopic
func (api *API) CloseGeneralForumTopic(params BaseGeneralForumTopic) (bool, error) {
@@ -173,6 +195,7 @@ func (api *API) CloseGeneralForumTopic(params BaseGeneralForumTopic) (bool, erro
}
// CloseGeneralForumTopicWithContext is the context-aware variant of CloseGeneralForumTopic.
// Since: Bot API 6.4
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#closegeneralforumtopic
func (api *API) CloseGeneralForumTopicWithContext(ctx context.Context, params BaseGeneralForumTopic) (bool, error) {
@@ -181,6 +204,7 @@ func (api *API) CloseGeneralForumTopicWithContext(ctx context.Context, params Ba
}
// ReopenGeneralForumTopic reopens the 'General' topic in a forum supergroup.
// Since: Bot API 6.4
// Returns True on success.
// See https://core.telegram.org/bots/api#reopengeneralforumtopic
func (api *API) ReopenGeneralForumTopic(params BaseGeneralForumTopic) (bool, error) {
@@ -189,6 +213,7 @@ func (api *API) ReopenGeneralForumTopic(params BaseGeneralForumTopic) (bool, err
}
// ReopenGeneralForumTopicWithContext is the context-aware variant of ReopenGeneralForumTopic.
// Since: Bot API 6.4
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#reopengeneralforumtopic
func (api *API) ReopenGeneralForumTopicWithContext(ctx context.Context, params BaseGeneralForumTopic) (bool, error) {
@@ -197,6 +222,7 @@ func (api *API) ReopenGeneralForumTopicWithContext(ctx context.Context, params B
}
// HideGeneralForumTopic hides the 'General' topic in a forum supergroup.
// Since: Bot API 6.4
// Returns True on success.
// See https://core.telegram.org/bots/api#hidegeneralforumtopic
func (api *API) HideGeneralForumTopic(params BaseGeneralForumTopic) (bool, error) {
@@ -205,6 +231,7 @@ func (api *API) HideGeneralForumTopic(params BaseGeneralForumTopic) (bool, error
}
// HideGeneralForumTopicWithContext is the context-aware variant of HideGeneralForumTopic.
// Since: Bot API 6.4
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#hidegeneralforumtopic
func (api *API) HideGeneralForumTopicWithContext(ctx context.Context, params BaseGeneralForumTopic) (bool, error) {
@@ -213,6 +240,7 @@ func (api *API) HideGeneralForumTopicWithContext(ctx context.Context, params Bas
}
// UnhideGeneralForumTopic unhides the 'General' topic in a forum supergroup.
// Since: Bot API 6.4
// Returns True on success.
// See https://core.telegram.org/bots/api#unhidegeneralforumtopic
func (api *API) UnhideGeneralForumTopic(params BaseGeneralForumTopic) (bool, error) {
@@ -221,6 +249,7 @@ func (api *API) UnhideGeneralForumTopic(params BaseGeneralForumTopic) (bool, err
}
// UnhideGeneralForumTopicWithContext is the context-aware variant of UnhideGeneralForumTopic.
// Since: Bot API 6.4
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#unhidegeneralforumtopic
func (api *API) UnhideGeneralForumTopicWithContext(ctx context.Context, params BaseGeneralForumTopic) (bool, error) {
@@ -229,6 +258,7 @@ func (api *API) UnhideGeneralForumTopicWithContext(ctx context.Context, params B
}
// UnpinAllGeneralForumTopicMessages clears the list of pinned messages in the 'General' topic.
// Since: Bot API 6.4
// Returns True on success.
// See https://core.telegram.org/bots/api#unpinallgeneralforumtopicmessages
func (api *API) UnpinAllGeneralForumTopicMessages(params BaseGeneralForumTopic) (bool, error) {
@@ -237,6 +267,7 @@ func (api *API) UnpinAllGeneralForumTopicMessages(params BaseGeneralForumTopic)
}
// UnpinAllGeneralForumTopicMessagesWithContext is the context-aware variant of UnpinAllGeneralForumTopicMessages.
// Since: Bot API 6.4
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#unpinallgeneralforumtopicmessages
func (api *API) UnpinAllGeneralForumTopicMessagesWithContext(ctx context.Context, params BaseGeneralForumTopic) (bool, error) {
+20 -2
View File
@@ -1,6 +1,7 @@
package tgapi
// ForumTopic represents a forum topic.
// Since: Bot API 6.3
// See https://core.telegram.org/bots/api#forumtopic
type ForumTopic struct {
MessageThreadID int `json:"message_thread_id"`
@@ -12,6 +13,7 @@ type ForumTopic struct {
// ForumTopicIconColor represents the color of a forum topic icon.
// The value is an integer representing the color in RGB format.
// Since: Bot API 6.3
// See https://core.telegram.org/bots/api#forumtopiciconcolor
type ForumTopicIconColor int
@@ -20,18 +22,34 @@ const (
ForumTopicIconColorBlue ForumTopicIconColor = 7322096
)
// ForumTopicCreated represents a service message about a new forum topic created.
// Since: Bot API 6.3
type ForumTopicCreated struct {
Name string `json:"name"`
IconColor int `json:"icon_color"`
IconCustomEmojiID string `json:"icon_custom_emoji_id,omitempty"`
IsNameImplicit bool `json:"is_name_implicit,omitempty"`
}
// ForumTopicEdited represents a service message about an edited forum topic.
// Since: Bot API 6.4
type ForumTopicEdited struct {
Name string `json:"name,omitempty"`
IconCustomEmojiID string `json:"icon_custom_emoji_id,omitempty"`
}
// ForumTopicClosed represents a service message about a forum topic closed.
// Since: Bot API 6.3
type ForumTopicClosed struct{}
// ForumTopicReopened represents a service message about a forum topic reopened.
// Since: Bot API 6.3
type ForumTopicReopened struct{}
// GeneralForumTopicHidden represents a service message about the General forum topic hidden.
// Since: Bot API 6.4
type GeneralForumTopicHidden struct{}
type GeneralForumTopicUnhidden struct {
}
// GeneralForumTopicUnhidden represents a service message about the General forum topic unhidden.
// Since: Bot API 6.4
type GeneralForumTopicUnhidden struct{}
+9
View File
@@ -3,6 +3,7 @@ package tgapi
import "context"
// SendGame holds parameters for the sendGame method.
// Since: Bot API 2.2
// See https://core.telegram.org/bots/api#sendgame
type SendGame struct {
BusinessConnectionID string `json:"business_connection_id,omitempty"`
@@ -20,6 +21,7 @@ type SendGame struct {
}
// SendGame sends a game message.
// Since: Bot API 2.2
// See https://core.telegram.org/bots/api#sendgame
func (api *API) SendGame(params SendGame) (Message, error) {
req := NewRequestWithChatID[Message]("sendGame", params, params.ChatID)
@@ -27,6 +29,7 @@ func (api *API) SendGame(params SendGame) (Message, error) {
}
// SendGameWithContext is the context-aware variant of SendGame.
// Since: Bot API 2.2
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#sendgame
func (api *API) SendGameWithContext(ctx context.Context, params SendGame) (Message, error) {
@@ -35,6 +38,7 @@ func (api *API) SendGameWithContext(ctx context.Context, params SendGame) (Messa
}
// SetGameScore holds parameters for the setGameScore method.
// Since: Bot API 2.2
// See https://core.telegram.org/bots/api#setgamescore
type SetGameScore struct {
UserID int64 `json:"user_id"`
@@ -47,6 +51,7 @@ type SetGameScore struct {
}
// SetGameScore sets a user's score in a game message.
// Since: Bot API 2.2
// If inline_message_id is provided, returns a boolean success flag.
// Otherwise returns the edited Message.
// See https://core.telegram.org/bots/api#setgamescore
@@ -63,6 +68,7 @@ func (api *API) SetGameScore(params SetGameScore) (Message, bool, error) {
}
// SetGameScoreWithContext is the context-aware variant of SetGameScore.
// Since: Bot API 2.2
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#setgamescore
func (api *API) SetGameScoreWithContext(ctx context.Context, params SetGameScore) (Message, bool, error) {
@@ -78,6 +84,7 @@ func (api *API) SetGameScoreWithContext(ctx context.Context, params SetGameScore
}
// GetGameHighScores holds parameters for the getGameHighScores method.
// Since: Bot API 2.2
// See https://core.telegram.org/bots/api#getgamehighscores
type GetGameHighScores struct {
UserID int64 `json:"user_id"`
@@ -87,6 +94,7 @@ type GetGameHighScores struct {
}
// GetGameHighScores returns game high score data for a user.
// Since: Bot API 2.2
// See https://core.telegram.org/bots/api#getgamehighscores
func (api *API) GetGameHighScores(params GetGameHighScores) ([]GameHighScore, error) {
req := NewRequestWithChatID[[]GameHighScore]("getGameHighScores", params, params.ChatID)
@@ -94,6 +102,7 @@ func (api *API) GetGameHighScores(params GetGameHighScores) ([]GameHighScore, er
}
// GetGameHighScoresWithContext is the context-aware variant of GetGameHighScores.
// Since: Bot API 2.2
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#getgamehighscores
func (api *API) GetGameHighScoresWithContext(ctx context.Context, params GetGameHighScores) ([]GameHighScore, error) {
+6
View File
@@ -1,5 +1,7 @@
package tgapi
// Game represents a game.
// Since: Bot API 2.2
type Game struct {
Title string `json:"title"`
Description string `json:"description"`
@@ -8,9 +10,13 @@ type Game struct {
TextEntities []MessageEntity `json:"text_entities,omitempty"`
Animation *Animation `json:"animation,omitempty"`
}
// CallbackGame is a placeholder for the future use of callback games.
// Since: Bot API 2.2
type CallbackGame struct{}
// GameHighScore represents one row in a game high score table.
// Since: Bot API 2.2
// See https://core.telegram.org/bots/api#gamehighscore
type GameHighScore struct {
Position int `json:"position"`
+12
View File
@@ -3,6 +3,7 @@ package tgapi
import "context"
// AnswerInlineQuery holds parameters for the answerInlineQuery method.
// Since: Bot API 1.7
// See https://core.telegram.org/bots/api#answerinlinequery
type AnswerInlineQuery struct {
InlineQueryID string `json:"inline_query_id"`
@@ -14,6 +15,7 @@ type AnswerInlineQuery struct {
}
// AnswerInlineQuery sends answers to an inline query.
// Since: Bot API 1.7
// Returns true on success.
// See https://core.telegram.org/bots/api#answerinlinequery
func (api *API) AnswerInlineQuery(params AnswerInlineQuery) (bool, error) {
@@ -22,6 +24,7 @@ func (api *API) AnswerInlineQuery(params AnswerInlineQuery) (bool, error) {
}
// AnswerInlineQueryWithContext is the context-aware variant of AnswerInlineQuery.
// Since: Bot API 1.7
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#answerinlinequery
func (api *API) AnswerInlineQueryWithContext(ctx context.Context, params AnswerInlineQuery) (bool, error) {
@@ -30,6 +33,7 @@ func (api *API) AnswerInlineQueryWithContext(ctx context.Context, params AnswerI
}
// AnswerWebAppQuery holds parameters for the answerWebAppQuery method.
// Since: Bot API 8.0
// See https://core.telegram.org/bots/api#answerwebappquery
type AnswerWebAppQuery struct {
WebAppQueryID string `json:"web_app_query_id"`
@@ -37,6 +41,7 @@ type AnswerWebAppQuery struct {
}
// AnswerWebAppQuery sets the result of a Web App interaction.
// Since: Bot API 8.0
// See https://core.telegram.org/bots/api#answerwebappquery
func (api *API) AnswerWebAppQuery(params AnswerWebAppQuery) (SentWebAppMessage, error) {
req := NewRequest[SentWebAppMessage]("answerWebAppQuery", params)
@@ -44,6 +49,7 @@ func (api *API) AnswerWebAppQuery(params AnswerWebAppQuery) (SentWebAppMessage,
}
// AnswerWebAppQueryWithContext is the context-aware variant of AnswerWebAppQuery.
// Since: Bot API 8.0
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#answerwebappquery
func (api *API) AnswerWebAppQueryWithContext(ctx context.Context, params AnswerWebAppQuery) (SentWebAppMessage, error) {
@@ -52,6 +58,7 @@ func (api *API) AnswerWebAppQueryWithContext(ctx context.Context, params AnswerW
}
// SavePreparedInlineMessage holds parameters for the savePreparedInlineMessage method.
// Since: Bot API 8.0
// See https://core.telegram.org/bots/api#savepreparedinlinemessage
type SavePreparedInlineMessage struct {
UserID int64 `json:"user_id"`
@@ -63,6 +70,7 @@ type SavePreparedInlineMessage struct {
}
// SavePreparedInlineMessage stores a prepared message for Mini App users.
// Since: Bot API 8.0
// See https://core.telegram.org/bots/api#savepreparedinlinemessage
func (api *API) SavePreparedInlineMessage(params SavePreparedInlineMessage) (PreparedInlineMessage, error) {
req := NewRequest[PreparedInlineMessage]("savePreparedInlineMessage", params)
@@ -70,6 +78,7 @@ func (api *API) SavePreparedInlineMessage(params SavePreparedInlineMessage) (Pre
}
// SavePreparedInlineMessageWithContext is the context-aware variant of SavePreparedInlineMessage.
// Since: Bot API 8.0
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#savepreparedinlinemessage
func (api *API) SavePreparedInlineMessageWithContext(ctx context.Context, params SavePreparedInlineMessage) (PreparedInlineMessage, error) {
@@ -78,6 +87,7 @@ func (api *API) SavePreparedInlineMessageWithContext(ctx context.Context, params
}
// SavePreparedKeyboardButton holds parameters for the savePreparedKeyboardButton method.
// Since: Bot API 8.0
// See https://core.telegram.org/bots/api#savepreparedkeyboardbutton
type SavePreparedKeyboardButton struct {
UserID int64 `json:"user_id"`
@@ -85,6 +95,7 @@ type SavePreparedKeyboardButton struct {
}
// SavePreparedKeyboardButton stores a prepared keyboard button for Mini App users.
// Since: Bot API 8.0
// See https://core.telegram.org/bots/api#savepreparedkeyboardbutton
func (api *API) SavePreparedKeyboardButton(params SavePreparedKeyboardButton) (PreparedKeyboardButton, error) {
req := NewRequest[PreparedKeyboardButton]("savePreparedKeyboardButton", params)
@@ -92,6 +103,7 @@ func (api *API) SavePreparedKeyboardButton(params SavePreparedKeyboardButton) (P
}
// SavePreparedKeyboardButtonWithContext is the context-aware variant of SavePreparedKeyboardButton.
// Since: Bot API 8.0
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#savepreparedkeyboardbutton
func (api *API) SavePreparedKeyboardButtonWithContext(ctx context.Context, params SavePreparedKeyboardButton) (PreparedKeyboardButton, error) {
+5
View File
@@ -1,10 +1,12 @@
package tgapi
// InlineQueryResult is a JSON-serializable inline query result object.
// Since: Bot API 1.7
// See https://core.telegram.org/bots/api#inlinequeryresult
type InlineQueryResult map[string]any
// InlineQueryResultsButton represents a button shown above inline query results.
// Since: Bot API 6.3
// See https://core.telegram.org/bots/api#inlinequeryresultsbutton
type InlineQueryResultsButton struct {
Text string `json:"text"`
@@ -13,12 +15,14 @@ type InlineQueryResultsButton struct {
}
// SentWebAppMessage describes an inline message sent by a Web App on behalf of a user.
// Since: Bot API 8.0
// See https://core.telegram.org/bots/api#sentwebappmessage
type SentWebAppMessage struct {
InlineMessageID string `json:"inline_message_id,omitempty"`
}
// PreparedInlineMessage describes a prepared inline message.
// Since: Bot API 8.0
// See https://core.telegram.org/bots/api#preparedinlinemessage
type PreparedInlineMessage struct {
ID string `json:"id"`
@@ -26,6 +30,7 @@ type PreparedInlineMessage struct {
}
// PreparedKeyboardButton describes a prepared keyboard button.
// Since: Bot API 8.0
// See https://core.telegram.org/bots/api#preparedkeyboardbutton
type PreparedKeyboardButton struct {
ID string `json:"id"`
+166
View File
@@ -3,6 +3,7 @@ package tgapi
import "context"
// SendMessage holds parameters for the sendMessage method.
// Since: Bot API 1.0
// See https://core.telegram.org/bots/api#sendmessage
type SendMessage struct {
BusinessConnectionID string `json:"business_connection_id,omitempty"`
@@ -25,6 +26,7 @@ type SendMessage struct {
}
// SendMessage sends a text message.
// Since: Bot API 1.0
// See https://core.telegram.org/bots/api#sendmessage
func (api *API) SendMessage(params SendMessage) (Message, error) {
req := NewRequestWithChatID[Message, SendMessage]("sendMessage", params, params.ChatID)
@@ -32,6 +34,7 @@ func (api *API) SendMessage(params SendMessage) (Message, error) {
}
// SendMessageWithContext is the context-aware variant of SendMessage.
// Since: Bot API 1.0
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#sendmessage
func (api *API) SendMessageWithContext(ctx context.Context, params SendMessage) (Message, error) {
@@ -40,6 +43,7 @@ func (api *API) SendMessageWithContext(ctx context.Context, params SendMessage)
}
// ForwardMessage holds parameters for the forwardMessage method.
// Since: Bot API 1.0
// See https://core.telegram.org/bots/api#forwardmessage
type ForwardMessage struct {
ChatID int64 `json:"chat_id"`
@@ -57,6 +61,7 @@ type ForwardMessage struct {
}
// ForwardMessage forwards a message.
// Since: Bot API 1.0
// See https://core.telegram.org/bots/api#forwardmessage
func (api *API) ForwardMessage(params ForwardMessage) (Message, error) {
req := NewRequestWithChatID[Message]("forwardMessage", params, params.ChatID)
@@ -64,6 +69,7 @@ func (api *API) ForwardMessage(params ForwardMessage) (Message, error) {
}
// ForwardMessageWithContext is the context-aware variant of ForwardMessage.
// Since: Bot API 1.0
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#forwardmessage
func (api *API) ForwardMessageWithContext(ctx context.Context, params ForwardMessage) (Message, error) {
@@ -72,6 +78,7 @@ func (api *API) ForwardMessageWithContext(ctx context.Context, params ForwardMes
}
// ForwardMessages holds parameters for the forwardMessages method.
// Since: Bot API 7.0
// See https://core.telegram.org/bots/api#forwardmessages
type ForwardMessages struct {
ChatID int64 `json:"chat_id"`
@@ -85,6 +92,7 @@ type ForwardMessages struct {
}
// ForwardMessages forwards multiple messages.
// Since: Bot API 7.0
// Returns an array of message IDs of the sent messages.
// See https://core.telegram.org/bots/api#forwardmessages
func (api *API) ForwardMessages(params ForwardMessages) ([]MessageID, error) {
@@ -93,6 +101,7 @@ func (api *API) ForwardMessages(params ForwardMessages) ([]MessageID, error) {
}
// ForwardMessagesWithContext is the context-aware variant of ForwardMessages.
// Since: Bot API 7.0
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#forwardmessages
func (api *API) ForwardMessagesWithContext(ctx context.Context, params ForwardMessages) ([]MessageID, error) {
@@ -101,6 +110,7 @@ func (api *API) ForwardMessagesWithContext(ctx context.Context, params ForwardMe
}
// CopyMessage holds parameters for the copyMessage method.
// Since: Bot API 5.0
// See https://core.telegram.org/bots/api#copymessage
type CopyMessage struct {
ChatID int64 `json:"chat_id"`
@@ -126,6 +136,7 @@ type CopyMessage struct {
}
// CopyMessage copies a message.
// Since: Bot API 5.0
// Returns the MessageID of the sent copy.
// See https://core.telegram.org/bots/api#copymessage
func (api *API) CopyMessage(params CopyMessage) (int, error) {
@@ -137,6 +148,7 @@ func (api *API) CopyMessage(params CopyMessage) (int, error) {
}
// CopyMessageWithContext is the context-aware variant of CopyMessage.
// Since: Bot API 5.0
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#copymessage
func (api *API) CopyMessageWithContext(ctx context.Context, params CopyMessage) (int, error) {
@@ -148,6 +160,7 @@ func (api *API) CopyMessageWithContext(ctx context.Context, params CopyMessage)
}
// CopyMessages holds parameters for the copyMessages method.
// Since: Bot API 7.0
// See https://core.telegram.org/bots/api#copymessages
type CopyMessages struct {
ChatID int64 `json:"chat_id"`
@@ -162,6 +175,7 @@ type CopyMessages struct {
}
// CopyMessages copies multiple messages.
// Since: Bot API 7.0
// Returns an array of message IDs of the sent copies.
// See https://core.telegram.org/bots/api#copymessages
func (api *API) CopyMessages(params CopyMessages) ([]MessageID, error) {
@@ -170,6 +184,7 @@ func (api *API) CopyMessages(params CopyMessages) ([]MessageID, error) {
}
// CopyMessagesWithContext is the context-aware variant of CopyMessages.
// Since: Bot API 7.0
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#copymessages
func (api *API) CopyMessagesWithContext(ctx context.Context, params CopyMessages) ([]MessageID, error) {
@@ -178,6 +193,7 @@ func (api *API) CopyMessagesWithContext(ctx context.Context, params CopyMessages
}
// SendLocation holds parameters for the sendLocation method.
// Since: Bot API 1.0
// See https://core.telegram.org/bots/api#sendlocation
type SendLocation struct {
BusinessConnectionID string `json:"business_connection_id,omitempty"`
@@ -203,6 +219,7 @@ type SendLocation struct {
}
// SendLocation sends a point on the map.
// Since: Bot API 1.0
// See https://core.telegram.org/bots/api#sendlocation
func (api *API) SendLocation(params SendLocation) (Message, error) {
req := NewRequestWithChatID[Message]("sendLocation", params, params.ChatID)
@@ -210,6 +227,7 @@ func (api *API) SendLocation(params SendLocation) (Message, error) {
}
// SendLocationWithContext is the context-aware variant of SendLocation.
// Since: Bot API 1.0
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#sendlocation
func (api *API) SendLocationWithContext(ctx context.Context, params SendLocation) (Message, error) {
@@ -218,6 +236,7 @@ func (api *API) SendLocationWithContext(ctx context.Context, params SendLocation
}
// SendVenue holds parameters for the sendVenue method.
// Since: Bot API 2.0
// See https://core.telegram.org/bots/api#sendvenue
type SendVenue struct {
BusinessConnectionID string `json:"business_connection_id,omitempty"`
@@ -245,6 +264,7 @@ type SendVenue struct {
}
// SendVenue sends information about a venue.
// Since: Bot API 2.0
// See https://core.telegram.org/bots/api#sendvenue
func (api *API) SendVenue(params SendVenue) (Message, error) {
req := NewRequestWithChatID[Message]("sendVenue", params, params.ChatID)
@@ -252,6 +272,7 @@ func (api *API) SendVenue(params SendVenue) (Message, error) {
}
// SendVenueWithContext is the context-aware variant of SendVenue.
// Since: Bot API 2.0
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#sendvenue
func (api *API) SendVenueWithContext(ctx context.Context, params SendVenue) (Message, error) {
@@ -260,6 +281,7 @@ func (api *API) SendVenueWithContext(ctx context.Context, params SendVenue) (Mes
}
// SendContact holds parameters for the sendContact method.
// Since: Bot API 2.0
// See https://core.telegram.org/bots/api#sendcontact
type SendContact struct {
BusinessConnectionID string `json:"business_connection_id,omitempty"`
@@ -283,6 +305,7 @@ type SendContact struct {
}
// SendContact sends a phone contact.
// Since: Bot API 2.0
// See https://core.telegram.org/bots/api#sendcontact
func (api *API) SendContact(params SendContact) (Message, error) {
req := NewRequestWithChatID[Message]("sendContact", params, params.ChatID)
@@ -290,6 +313,7 @@ func (api *API) SendContact(params SendContact) (Message, error) {
}
// SendContactWithContext is the context-aware variant of SendContact.
// Since: Bot API 2.0
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#sendcontact
func (api *API) SendContactWithContext(ctx context.Context, params SendContact) (Message, error) {
@@ -298,6 +322,7 @@ func (api *API) SendContactWithContext(ctx context.Context, params SendContact)
}
// SendPoll holds parameters for the sendPoll method.
// Since: Bot API 4.2
// See https://core.telegram.org/bots/api#sendpoll
type SendPoll struct {
BusinessConnectionID string `json:"business_connection_id,omitempty"`
@@ -315,10 +340,14 @@ type SendPoll struct {
ShuffleOptions bool `json:"shuffle_options,omitempty"`
AllowAddingOptions bool `json:"allow_adding_options,omitempty"`
HideResultsUntilCloses bool `json:"hide_results_until_closes,omitempty"`
MembersOnly bool `json:"members_only,omitempty"` // Since: Bot API 10.0
CountryCodes []string `json:"country_codes,omitempty"` // Since: Bot API 10.0
CorrectOptionIDs []int `json:"correct_option_ids,omitempty"`
Explanation string `json:"explanation,omitempty"`
ExplanationParseMode ParseMode `json:"explanation_parse_mode,omitempty"`
ExplanationEntities []MessageEntity `json:"explanation_entities,omitempty"`
ExplanationMedia *InputPollMedia `json:"explanation_media,omitempty"`
Media *InputPollMedia `json:"media,omitempty"`
OpenPeriod int `json:"open_period,omitempty"`
CloseDate int `json:"close_date"`
IsClosed bool `json:"is_closed,omitempty"`
@@ -337,6 +366,7 @@ type SendPoll struct {
}
// SendPoll sends a native poll.
// Since: Bot API 4.2
// See https://core.telegram.org/bots/api#sendpoll
func (api *API) SendPoll(params SendPoll) (Message, error) {
req := NewRequestWithChatID[Message]("sendPoll", params, params.ChatID)
@@ -344,6 +374,7 @@ func (api *API) SendPoll(params SendPoll) (Message, error) {
}
// SendPollWithContext is the context-aware variant of SendPoll.
// Since: Bot API 4.2
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#sendpoll
func (api *API) SendPollWithContext(ctx context.Context, params SendPoll) (Message, error) {
@@ -352,6 +383,7 @@ func (api *API) SendPollWithContext(ctx context.Context, params SendPoll) (Messa
}
// SendChecklist holds parameters for the sendChecklist method.
// Since: Bot API 9.1
// See https://core.telegram.org/bots/api#sendchecklist
type SendChecklist struct {
BusinessConnectionID string `json:"business_connection_id"`
@@ -367,6 +399,7 @@ type SendChecklist struct {
}
// SendChecklist sends a checklist.
// Since: Bot API 9.1
// See https://core.telegram.org/bots/api#sendchecklist
func (api *API) SendChecklist(params SendChecklist) (Message, error) {
req := NewRequestWithChatID[Message]("sendChecklist", params, params.ChatID)
@@ -374,6 +407,7 @@ func (api *API) SendChecklist(params SendChecklist) (Message, error) {
}
// SendChecklistWithContext is the context-aware variant of SendChecklist.
// Since: Bot API 9.1
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#sendchecklist
func (api *API) SendChecklistWithContext(ctx context.Context, params SendChecklist) (Message, error) {
@@ -382,6 +416,7 @@ func (api *API) SendChecklistWithContext(ctx context.Context, params SendCheckli
}
// SendDice holds parameters for the sendDice method.
// Since: Bot API 4.7
// See https://core.telegram.org/bots/api#senddice
type SendDice struct {
BusinessConnectionID string `json:"business_connection_id,omitempty"`
@@ -402,6 +437,7 @@ type SendDice struct {
}
// SendDice sends a dice, which will have a random value.
// Since: Bot API 4.7
// See https://core.telegram.org/bots/api#senddice
func (api *API) SendDice(params SendDice) (Message, error) {
req := NewRequestWithChatID[Message]("sendDice", params, params.ChatID)
@@ -409,6 +445,7 @@ func (api *API) SendDice(params SendDice) (Message, error) {
}
// SendDiceWithContext is the context-aware variant of SendDice.
// Since: Bot API 4.7
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#senddice
func (api *API) SendDiceWithContext(ctx context.Context, params SendDice) (Message, error) {
@@ -417,6 +454,7 @@ func (api *API) SendDiceWithContext(ctx context.Context, params SendDice) (Messa
}
// SendMessageDraft holds parameters for the sendMessageDraft method.
// Since: Bot API 9.1
// See https://core.telegram.org/bots/api#sendmessagedraft
type SendMessageDraft struct {
ChatID int64 `json:"chat_id"`
@@ -428,6 +466,7 @@ type SendMessageDraft struct {
}
// SendMessageDraft sends or updates a draft message in the target chat.
// Since: Bot API 9.1
// Returns True on success.
// See https://core.telegram.org/bots/api#sendmessagedraft
func (api *API) SendMessageDraft(params SendMessageDraft) (bool, error) {
@@ -436,6 +475,7 @@ func (api *API) SendMessageDraft(params SendMessageDraft) (bool, error) {
}
// SendMessageDraftWithContext is the context-aware variant of SendMessageDraft.
// Since: Bot API 9.1
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#sendmessagedraft
func (api *API) SendMessageDraftWithContext(ctx context.Context, params SendMessageDraft) (bool, error) {
@@ -444,6 +484,7 @@ func (api *API) SendMessageDraftWithContext(ctx context.Context, params SendMess
}
// SendChatAction holds parameters for the sendChatAction method.
// Since: Bot API 1.0
// See https://core.telegram.org/bots/api#sendchataction
type SendChatAction struct {
BusinessConnectionID string `json:"business_connection_id,omitempty"`
@@ -453,6 +494,7 @@ type SendChatAction struct {
}
// SendChatAction sends a chat action (typing, uploading photo, etc.).
// Since: Bot API 1.0
// Returns True on success.
// See https://core.telegram.org/bots/api#sendchataction
func (api *API) SendChatAction(params SendChatAction) (bool, error) {
@@ -461,6 +503,7 @@ func (api *API) SendChatAction(params SendChatAction) (bool, error) {
}
// SendChatActionWithContext is the context-aware variant of SendChatAction.
// Since: Bot API 1.0
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#sendchataction
func (api *API) SendChatActionWithContext(ctx context.Context, params SendChatAction) (bool, error) {
@@ -469,6 +512,7 @@ func (api *API) SendChatActionWithContext(ctx context.Context, params SendChatAc
}
// SetMessageReaction holds parameters for the setMessageReaction method.
// Since: Bot API 7.0
// See https://core.telegram.org/bots/api#setmessagereaction
type SetMessageReaction struct {
ChatID int64 `json:"chat_id"`
@@ -478,6 +522,7 @@ type SetMessageReaction struct {
}
// SetMessageReaction changes the chosen reaction on a message.
// Since: Bot API 7.0
// Returns True on success.
// See https://core.telegram.org/bots/api#setmessagereaction
func (api *API) SetMessageReaction(params SetMessageReaction) (bool, error) {
@@ -486,6 +531,7 @@ func (api *API) SetMessageReaction(params SetMessageReaction) (bool, error) {
}
// SetMessageReactionWithContext is the context-aware variant of SetMessageReaction.
// Since: Bot API 7.0
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#setmessagereaction
func (api *API) SetMessageReactionWithContext(ctx context.Context, params SetMessageReaction) (bool, error) {
@@ -494,6 +540,7 @@ func (api *API) SetMessageReactionWithContext(ctx context.Context, params SetMes
}
// EditMessageText holds parameters for the editMessageText method.
// Since: Bot API 2.0
// See https://core.telegram.org/bots/api#editmessagetext
type EditMessageText struct {
BusinessConnectionID string `json:"business_connection_id,omitempty"`
@@ -508,6 +555,7 @@ type EditMessageText struct {
}
// EditMessageText edits text messages.
// Since: Bot API 2.0
// If inline_message_id is provided, returns a boolean success flag;
// otherwise returns the edited Message.
// See https://core.telegram.org/bots/api#editmessagetext
@@ -524,6 +572,7 @@ func (api *API) EditMessageText(params EditMessageText) (Message, bool, error) {
}
// EditMessageTextWithContext is the context-aware variant of EditMessageText.
// Since: Bot API 2.0
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#editmessagetext
func (api *API) EditMessageTextWithContext(ctx context.Context, params EditMessageText) (Message, bool, error) {
@@ -539,6 +588,7 @@ func (api *API) EditMessageTextWithContext(ctx context.Context, params EditMessa
}
// EditMessageCaption holds parameters for the editMessageCaption method.
// Since: Bot API 2.0
// See https://core.telegram.org/bots/api#editmessagecaption
type EditMessageCaption struct {
BusinessConnectionID string `json:"business_connection_id,omitempty"`
@@ -553,6 +603,7 @@ type EditMessageCaption struct {
}
// EditMessageCaption edits captions of messages.
// Since: Bot API 2.0
// If inline_message_id is provided, returns a boolean success flag;
// otherwise returns the edited Message.
// See https://core.telegram.org/bots/api#editmessagecaption
@@ -569,6 +620,7 @@ func (api *API) EditMessageCaption(params EditMessageCaption) (Message, bool, er
}
// EditMessageCaptionWithContext is the context-aware variant of EditMessageCaption.
// Since: Bot API 2.0
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#editmessagecaption
func (api *API) EditMessageCaptionWithContext(ctx context.Context, params EditMessageCaption) (Message, bool, error) {
@@ -584,6 +636,7 @@ func (api *API) EditMessageCaptionWithContext(ctx context.Context, params EditMe
}
// EditMessageMedia holds parameters for the editMessageMedia method.
// Since: Bot API 4.0
// See https://core.telegram.org/bots/api#editmessagemedia
type EditMessageMedia struct {
BusinessConnectionID string `json:"business_connection_id,omitempty"`
@@ -595,6 +648,7 @@ type EditMessageMedia struct {
}
// EditMessageMedia edits media messages.
// Since: Bot API 4.0
// If inline_message_id is provided, returns a boolean success flag;
// otherwise returns the edited Message.
// See https://core.telegram.org/bots/api#editmessagemedia
@@ -611,6 +665,7 @@ func (api *API) EditMessageMedia(params EditMessageMedia) (Message, bool, error)
}
// EditMessageMediaWithContext is the context-aware variant of EditMessageMedia.
// Since: Bot API 4.0
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#editmessagemedia
func (api *API) EditMessageMediaWithContext(ctx context.Context, params EditMessageMedia) (Message, bool, error) {
@@ -626,6 +681,7 @@ func (api *API) EditMessageMediaWithContext(ctx context.Context, params EditMess
}
// EditMessageLiveLocation holds parameters for the editMessageLiveLocation method.
// Since: Bot API 3.4
// See https://core.telegram.org/bots/api#editmessagelivelocation
type EditMessageLiveLocation struct {
BusinessConnectionID string `json:"business_connection_id,omitempty"`
@@ -643,6 +699,7 @@ type EditMessageLiveLocation struct {
}
// EditMessageLiveLocation edits live location messages.
// Since: Bot API 3.4
// If inline_message_id is provided, returns a boolean success flag;
// otherwise returns the edited Message.
// See https://core.telegram.org/bots/api#editmessagelivelocation
@@ -659,6 +716,7 @@ func (api *API) EditMessageLiveLocation(params EditMessageLiveLocation) (Message
}
// EditMessageLiveLocationWithContext is the context-aware variant of EditMessageLiveLocation.
// Since: Bot API 3.4
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#editmessagelivelocation
func (api *API) EditMessageLiveLocationWithContext(ctx context.Context, params EditMessageLiveLocation) (Message, bool, error) {
@@ -674,6 +732,7 @@ func (api *API) EditMessageLiveLocationWithContext(ctx context.Context, params E
}
// StopMessageLiveLocation holds parameters for the stopMessageLiveLocation method.
// Since: Bot API 3.4
// See https://core.telegram.org/bots/api#stopmessagelivelocation
type StopMessageLiveLocation struct {
BusinessConnectionID string `json:"business_connection_id,omitempty"`
@@ -684,6 +743,7 @@ type StopMessageLiveLocation struct {
}
// StopMessageLiveLocation stops a live location message.
// Since: Bot API 3.4
// If inline_message_id is provided, returns a boolean success flag;
// otherwise returns the edited Message.
// See https://core.telegram.org/bots/api#stopmessagelivelocation
@@ -700,6 +760,7 @@ func (api *API) StopMessageLiveLocation(params StopMessageLiveLocation) (Message
}
// StopMessageLiveLocationWithContext is the context-aware variant of StopMessageLiveLocation.
// Since: Bot API 3.4
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#stopmessagelivelocation
func (api *API) StopMessageLiveLocationWithContext(ctx context.Context, params StopMessageLiveLocation) (Message, bool, error) {
@@ -715,6 +776,8 @@ func (api *API) StopMessageLiveLocationWithContext(ctx context.Context, params S
}
// EditMessageChecklist holds parameters for the editMessageChecklist method.
// Since: Bot API 9.1
// See https://core.telegram.org/bots/api#editmessagechecklist
type EditMessageChecklist struct {
BusinessConnectionID string `json:"business_connection_id"`
ChatID int64 `json:"chat_id"`
@@ -724,6 +787,7 @@ type EditMessageChecklist struct {
}
// EditMessageChecklist edits a checklist message.
// Since: Bot API 9.1
// See https://core.telegram.org/bots/api#editmessagechecklist
func (api *API) EditMessageChecklist(params EditMessageChecklist) (Message, error) {
req := NewRequestWithChatID[Message]("editMessageChecklist", params, params.ChatID)
@@ -731,6 +795,7 @@ func (api *API) EditMessageChecklist(params EditMessageChecklist) (Message, erro
}
// EditMessageChecklistWithContext is the context-aware variant of EditMessageChecklist.
// Since: Bot API 9.1
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#editmessagechecklist
func (api *API) EditMessageChecklistWithContext(ctx context.Context, params EditMessageChecklist) (Message, error) {
@@ -739,6 +804,7 @@ func (api *API) EditMessageChecklistWithContext(ctx context.Context, params Edit
}
// EditMessageReplyMarkup holds parameters for the editMessageReplyMarkup method.
// Since: Bot API 2.0
// See https://core.telegram.org/bots/api#editmessagereplymarkup
type EditMessageReplyMarkup struct {
BusinessConnectionID string `json:"business_connection_id,omitempty"`
@@ -749,6 +815,7 @@ type EditMessageReplyMarkup struct {
}
// EditMessageReplyMarkup edits only the reply markup of messages.
// Since: Bot API 2.0
// If inline_message_id is provided, returns a boolean success flag;
// otherwise returns the edited Message.
// See https://core.telegram.org/bots/api#editmessagereplymarkup
@@ -765,6 +832,7 @@ func (api *API) EditMessageReplyMarkup(params EditMessageReplyMarkup) (Message,
}
// EditMessageReplyMarkupWithContext is the context-aware variant of EditMessageReplyMarkup.
// Since: Bot API 2.0
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#editmessagereplymarkup
func (api *API) EditMessageReplyMarkupWithContext(ctx context.Context, params EditMessageReplyMarkup) (Message, bool, error) {
@@ -780,6 +848,7 @@ func (api *API) EditMessageReplyMarkupWithContext(ctx context.Context, params Ed
}
// StopPoll holds parameters for the stopPoll method.
// Since: Bot API 4.2
// See https://core.telegram.org/bots/api#stoppoll
type StopPoll struct {
BusinessConnectionID string `json:"business_connection_id,omitempty"`
@@ -789,6 +858,7 @@ type StopPoll struct {
}
// StopPoll stops a poll that was sent by the bot.
// Since: Bot API 4.2
// Returns the stopped Poll.
// See https://core.telegram.org/bots/api#stoppoll
func (api *API) StopPoll(params StopPoll) (Poll, error) {
@@ -797,6 +867,7 @@ func (api *API) StopPoll(params StopPoll) (Poll, error) {
}
// StopPollWithContext is the context-aware variant of StopPoll.
// Since: Bot API 4.2
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#stoppoll
func (api *API) StopPollWithContext(ctx context.Context, params StopPoll) (Poll, error) {
@@ -805,6 +876,7 @@ func (api *API) StopPollWithContext(ctx context.Context, params StopPoll) (Poll,
}
// ApproveSuggestedPost holds parameters for the approveSuggestedPost method.
// Since: Bot API 9.2
// See https://core.telegram.org/bots/api#approvesuggestedpost
type ApproveSuggestedPost struct {
ChatID int64 `json:"chat_id"`
@@ -813,6 +885,7 @@ type ApproveSuggestedPost struct {
}
// ApproveSuggestedPost approves a suggested channel post.
// Since: Bot API 9.2
// Returns True on success.
// See https://core.telegram.org/bots/api#approvesuggestedpost
func (api *API) ApproveSuggestedPost(params ApproveSuggestedPost) (bool, error) {
@@ -821,6 +894,7 @@ func (api *API) ApproveSuggestedPost(params ApproveSuggestedPost) (bool, error)
}
// ApproveSuggestedPostWithContext is the context-aware variant of ApproveSuggestedPost.
// Since: Bot API 9.2
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#approvesuggestedpost
func (api *API) ApproveSuggestedPostWithContext(ctx context.Context, params ApproveSuggestedPost) (bool, error) {
@@ -829,6 +903,7 @@ func (api *API) ApproveSuggestedPostWithContext(ctx context.Context, params Appr
}
// DeclineSuggestedPost holds parameters for the declineSuggestedPost method.
// Since: Bot API 9.2
// See https://core.telegram.org/bots/api#declinesuggestedpost
type DeclineSuggestedPost struct {
ChatID int64 `json:"chat_id"`
@@ -837,6 +912,7 @@ type DeclineSuggestedPost struct {
}
// DeclineSuggestedPost declines a suggested channel post.
// Since: Bot API 9.2
// Returns True on success.
// See https://core.telegram.org/bots/api#declinesuggestedpost
func (api *API) DeclineSuggestedPost(params DeclineSuggestedPost) (bool, error) {
@@ -845,6 +921,7 @@ func (api *API) DeclineSuggestedPost(params DeclineSuggestedPost) (bool, error)
}
// DeclineSuggestedPostWithContext is the context-aware variant of DeclineSuggestedPost.
// Since: Bot API 9.2
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#declinesuggestedpost
func (api *API) DeclineSuggestedPostWithContext(ctx context.Context, params DeclineSuggestedPost) (bool, error) {
@@ -853,6 +930,7 @@ func (api *API) DeclineSuggestedPostWithContext(ctx context.Context, params Decl
}
// DeleteMessage holds parameters for the deleteMessage method.
// Since: Bot API 3.0
// See https://core.telegram.org/bots/api#deletemessage
type DeleteMessage struct {
ChatID int64 `json:"chat_id"`
@@ -860,6 +938,7 @@ type DeleteMessage struct {
}
// DeleteMessage deletes a message.
// Since: Bot API 3.0
// Returns True on success.
// See https://core.telegram.org/bots/api#deletemessage
func (api *API) DeleteMessage(params DeleteMessage) (bool, error) {
@@ -868,6 +947,7 @@ func (api *API) DeleteMessage(params DeleteMessage) (bool, error) {
}
// DeleteMessageWithContext is the context-aware variant of DeleteMessage.
// Since: Bot API 3.0
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#deletemessage
func (api *API) DeleteMessageWithContext(ctx context.Context, params DeleteMessage) (bool, error) {
@@ -876,6 +956,7 @@ func (api *API) DeleteMessageWithContext(ctx context.Context, params DeleteMessa
}
// DeleteMessages holds parameters for the deleteMessages method.
// Since: Bot API 7.0
// See https://core.telegram.org/bots/api#deletemessages
type DeleteMessages struct {
ChatID int64 `json:"chat_id"`
@@ -883,6 +964,7 @@ type DeleteMessages struct {
}
// DeleteMessages deletes multiple messages at once.
// Since: Bot API 7.0
// Returns True on success.
// See https://core.telegram.org/bots/api#deletemessages
func (api *API) DeleteMessages(params DeleteMessages) (bool, error) {
@@ -891,6 +973,7 @@ func (api *API) DeleteMessages(params DeleteMessages) (bool, error) {
}
// DeleteMessagesWithContext is the context-aware variant of DeleteMessages.
// Since: Bot API 7.0
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#deletemessages
func (api *API) DeleteMessagesWithContext(ctx context.Context, params DeleteMessages) (bool, error) {
@@ -899,6 +982,7 @@ func (api *API) DeleteMessagesWithContext(ctx context.Context, params DeleteMess
}
// AnswerCallbackQuery holds parameters for the answerCallbackQuery method.
// Since: Bot API 2.0
// See https://core.telegram.org/bots/api#answercallbackquery
type AnswerCallbackQuery struct {
CallbackQueryID string `json:"callback_query_id"`
@@ -909,6 +993,7 @@ type AnswerCallbackQuery struct {
}
// AnswerCallbackQuery sends answers to callback queries sent from inline keyboards.
// Since: Bot API 2.0
// Returns True on success.
// See https://core.telegram.org/bots/api#answercallbackquery
func (api *API) AnswerCallbackQuery(params AnswerCallbackQuery) (bool, error) {
@@ -917,9 +1002,90 @@ func (api *API) AnswerCallbackQuery(params AnswerCallbackQuery) (bool, error) {
}
// AnswerCallbackQueryWithContext is the context-aware variant of AnswerCallbackQuery.
// Since: Bot API 2.0
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#answercallbackquery
func (api *API) AnswerCallbackQueryWithContext(ctx context.Context, params AnswerCallbackQuery) (bool, error) {
req := NewRequest[bool]("answerCallbackQuery", params)
return req.DoWithContext(ctx, api)
}
// AnswerGuestQuery holds parameters for the answerGuestQuery method.
// Since: Bot API 10.0
// See https://core.telegram.org/bots/api#answerguestquery
type AnswerGuestQuery struct {
GuestQueryID string `json:"guest_query_id"`
Result InlineQueryResult `json:"result"`
}
// AnswerGuestQuery answers a guest query.
// Since: Bot API 10.0
// See https://core.telegram.org/bots/api#answerguestquery
func (api *API) AnswerGuestQuery(params AnswerGuestQuery) (SentGuestMessage, error) {
req := NewRequest[SentGuestMessage]("answerGuestQuery", params)
return req.Do(api)
}
// AnswerGuestQueryWithContext is the context-aware variant of AnswerGuestQuery.
// Since: Bot API 10.0
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#answerguestquery
func (api *API) AnswerGuestQueryWithContext(ctx context.Context, params AnswerGuestQuery) (SentGuestMessage, error) {
req := NewRequest[SentGuestMessage]("answerGuestQuery", params)
return req.DoWithContext(ctx, api)
}
// DeleteAllMessageReactions holds parameters for the deleteAllMessageReactions method.
// Since: Bot API 10.0
// See https://core.telegram.org/bots/api#deleteallmessagereactions
type DeleteAllMessageReactions struct {
ChatID int64 `json:"chat_id"`
UserID int64 `json:"user_id,omitempty"`
ActorChatID int64 `json:"actor_chat_id,omitempty"`
}
// DeleteAllMessageReactions deletes all reactions on a message.
// Since: Bot API 10.0
// Returns True on success.
// See https://core.telegram.org/bots/api#deleteallmessagereactions
func (api *API) DeleteAllMessageReactions(params DeleteAllMessageReactions) (bool, error) {
req := NewRequest[bool]("deleteAllMessageReactions", params)
return req.Do(api)
}
// DeleteAllMessageReactionWithContext is the context-aware variant of DeleteAllMessageReactions.
// Since: Bot API 10.0
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#deleteallmessagereactions
func (api *API) DeleteAllMessageReactionWithContext(ctx context.Context, params DeleteAllMessageReactions) (bool, error) {
req := NewRequest[bool]("deleteAllMessageReactions", params)
return req.DoWithContext(ctx, api)
}
// DeleteMessageReaction holds parameters for the deleteMessageReaction method.
// Since: Bot API 10.0
// See https://core.telegram.org/bots/api#deletemessagereaction
type DeleteMessageReaction struct {
ChatID int64 `json:"chat_id"`
MessageID int `json:"message_id"`
UserID int64 `json:"user_id,omitempty"`
ActorChatID int64 `json:"actor_chat_id,omitempty"`
}
// DeleteMessageReaction deletes a reaction on a message.
// Since: Bot API 10.0
// Returns True on success.
// See https://core.telegram.org/bots/api#deletemessagereaction
func (api *API) DeleteMessageReaction(params DeleteMessageReaction) (bool, error) {
req := NewRequest[bool]("deleteMessageReaction", params)
return req.Do(api)
}
// DeleteMessageReactionWithContext is the context-aware variant of DeleteMessageReaction.
// Since: Bot API 10.0
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#deletemessagereaction
func (api *API) DeleteMessageReactionWithContext(ctx context.Context, params DeleteMessageReaction) (bool, error) {
req := NewRequest[bool]("deleteMessageReaction", params)
return req.DoWithContext(ctx, api)
}
+175 -91
View File
@@ -7,16 +7,19 @@ import (
)
// MessageID represents a message identifier wrapper returned by some API methods.
// Since: Bot API 7.0
type MessageID struct {
MessageID int `json:"message_id"`
}
// DirectMessageTopic represents a forum topic in a direct message.
// Since: Bot API 9.2
type DirectMessageTopic struct {
TopicID int64 `json:"topic_id"`
User *User `json:"user,omitempty"`
}
// MessageOriginType represents the type of a message origin.
type MessageOriginType string
const (
@@ -26,6 +29,8 @@ const (
MessageOriginChannel = "channel"
)
// MessageOrigin describes the origin of a message.
// Since: Bot API 7.0
type MessageOrigin struct {
Type MessageOriginType `json:"type"`
Date int64 `json:"date"`
@@ -42,6 +47,8 @@ type MessageOrigin struct {
AuthorSignature string `json:"author_signature,omitempty"`
}
// ExternalReplyInfo contains information about a message that is being replied to.
// Since: Bot API 7.0
type ExternalReplyInfo struct {
Origin MessageOrigin `json:"origin"`
Chat *Chat `json:"chat,omitempty"`
@@ -50,15 +57,16 @@ type ExternalReplyInfo struct {
Animation *Animation `json:"animation,omitempty"`
Audio *Audio `json:"audio,omitempty"`
Document *Document `json:"document,omitempty"`
PaidMedia *PaidMediaInfo `json:"paid_media,omitempty"`
PaidMedia *PaidMediaInfo `json:"paid_media,omitempty"` // Since: Bot API 7.6
Photo []PhotoSize `json:"photo,omitempty"`
LivePhoto *LivePhoto `json:"live_photo,omitempty"` // Since: Bot API 10.0
Sticker *Sticker `json:"sticker,omitempty"`
Story *Story `json:"story,omitempty"`
Video *Video `json:"video,omitempty"`
VideoNote *VideoNote `json:"video_note,omitempty"`
Voice *Voice `json:"voice,omitempty"`
HasMediaSpoiler bool `json:"has_media_spoiler,omitempty"`
Checklist *Checklist `json:"checklist,omitempty"`
Checklist *Checklist `json:"checklist,omitempty"` // Since: Bot API 9.1
Contact *Contact `json:"contact,omitempty"`
Dice *Dice `json:"dice,omitempty"`
Game *Game `json:"game,omitempty"`
@@ -70,6 +78,8 @@ type ExternalReplyInfo struct {
Venue *Venue `json:"venue,omitempty"`
}
// TextQuote contains information about the quoted part of a message.
// Since: Bot API 7.0
type TextQuote struct {
Text string `json:"text"`
Entities []MessageEntity `json:"entities"`
@@ -77,75 +87,86 @@ type TextQuote struct {
IsManual bool `json:"is_manual,omitempty"`
}
// MessageAutoDeleteTimerChanged represents a service message about a change in auto-delete timer settings.
// Since: Bot API 5.1
type MessageAutoDeleteTimerChanged struct {
MessageAutoDeleteTime int `json:"message_auto_delete_time"`
}
// DirectMessagePriceChanged represents a service message about a change in the price of direct messages.
// Since: Bot API 9.1
type DirectMessagePriceChanged struct {
AreDirectMessagesEnabled bool `json:"are_direct_messages_enabled"`
DirectMessageStarCount int `json:"direct_message_star_count,omitempty"`
}
// PaidMessagePriceChanged represents a service message about a change in the price of paid messages.
// Since: Bot API 9.x
type PaidMessagePriceChanged struct {
PaidMessageStarCount int `json:"paid_message_star_count"`
}
// Message represents a Telegram message.
// Since: Bot API 1.0
// See https://core.telegram.org/bots/api#message
type Message struct {
MessageID int `json:"message_id"`
MessageThreadID int `json:"message_thread_id,omitempty"`
DirectMessageTopic *DirectMessageTopic `json:"direct_message_topic,omitempty"`
MessageThreadID int `json:"message_thread_id,omitempty"` // Since: Bot API 6.3
DirectMessageTopic *DirectMessageTopic `json:"direct_message_topic,omitempty"` // Since: Bot API 9.2
From *User `json:"from,omitempty"`
SenderChat *Chat `json:"sender_chat,omitempty"`
SenderBoostCount int `json:"sender_boost_count,omitempty"`
SenderBusinessBot *User `json:"sender_business_bot,omitempty"`
SenderTag string `json:"sender_tag,omitempty"`
SenderChat *Chat `json:"sender_chat,omitempty"` // Since: Bot API 5.0
SenderBoostCount int `json:"sender_boost_count,omitempty"` // Since: Bot API 7.1
SenderBusinessBot *User `json:"sender_business_bot,omitempty"` // Since: Bot API 7.2
SenderTag string `json:"sender_tag,omitempty"` // Since: Bot API 9.5
Date int `json:"date"`
BusinessConnectionID string `json:"business_connection_id,omitempty"`
GuestQueryID string `json:"guest_query_id,omitempty"` // Since: Bot API 10.0
BusinessConnectionID string `json:"business_connection_id,omitempty"` // Since: Bot API 7.2
Chat *Chat `json:"chat,omitempty"`
ForwardOrigin *MessageOrigin `json:"forward_origin,omitempty"`
ForwardOrigin *MessageOrigin `json:"forward_origin,omitempty"` // Since: Bot API 7.0
IsTopicMessage bool `json:"is_topic_message,omitempty"`
IsAutomaticForward bool `json:"is_automatic_forward,omitempty"`
IsTopicMessage bool `json:"is_topic_message,omitempty"` // Since: Bot API 6.3
IsAutomaticForward bool `json:"is_automatic_forward,omitempty"` // Since: Bot API 5.5
ReplyToMessage *Message `json:"reply_to_message,omitempty"`
ExternalReply *ExternalReplyInfo `json:"external_reply,omitempty"`
Quote *TextQuote `json:"quote,omitempty"`
ExternalReply *ExternalReplyInfo `json:"external_reply,omitempty"` // Since: Bot API 7.0
Quote *TextQuote `json:"quote,omitempty"` // Since: Bot API 7.0
ReplyToStory *Story `json:"reply_to_story,omitempty"`
ReplyToChecklistTaskID int `json:"reply_to_checklist_task_id,omitempty"`
ReplyToPollOptionID string `json:"reply_to_poll_option_id,omitempty"`
ReplyToStory *Story `json:"reply_to_story,omitempty"` // Since: Bot API 7.1
ReplyToChecklistTaskID int `json:"reply_to_checklist_task_id,omitempty"` // Since: Bot API 9.1
ReplyToPollOptionID string `json:"reply_to_poll_option_id,omitempty"` // Since: Bot API 9.6
ViaBot *User `json:"via_bot,omitempty"`
EditDate int `json:"edit_date,omitempty"`
HasProtectedContent bool `json:"has_protected_content,omitempty"`
IsFromOffline bool `json:"is_from_offline,omitempty"`
IsPaidPost bool `json:"is_paid_post,omitempty"`
MediaGroupID string `json:"media_group_id,omitempty"`
GuestBotCallerUser *User `json:"guest_bot_caller_user,omitempty"` // Since: Bot API 10.0
GuestBotCallerChat *Chat `json:"guest_bot_caller_chat,omitempty"` // Since: Bot API 10.0
EditDate int `json:"edit_date,omitempty"` // Since: Bot API 2.1
HasProtectedContent bool `json:"has_protected_content,omitempty"` // Since: Bot API 5.5
IsFromOffline bool `json:"is_from_offline,omitempty"` // Since: Bot API 7.2
IsPaidPost bool `json:"is_paid_post,omitempty"` // Since: Bot API 9.1
MediaGroupID string `json:"media_group_id,omitempty"` // Since: Bot API 3.5
AuthorSignature string `json:"author_signature,omitempty"`
PaidStarCount int `json:"paid_star_count,omitempty"`
PaidStarCount int `json:"paid_star_count,omitempty"` // Since: Bot API 8.3
Text string `json:"text"`
Entities []MessageEntity `json:"entities,omitempty"`
LinkPreviewOptions *LinkPreviewOptions `json:"link_preview_options,omitempty"`
SuggestedPostInfo *SuggestedPostInfo `json:"suggested_post_info,omitempty"`
EffectID string `json:"effect_id,omitempty"`
SuggestedPostInfo *SuggestedPostInfo `json:"suggested_post_info,omitempty"` // Since: Bot API 9.1
EffectID string `json:"effect_id,omitempty"` // Since: Bot API 7.4
Animation *Animation `json:"animation,omitempty"`
Animation *Animation `json:"animation,omitempty"` // Since: Bot API 4.0
Audio *Audio `json:"audio,omitempty"`
Document *Document `json:"document,omitempty"`
PaidMedia *PaidMediaInfo `json:"paid_media,omitempty"`
PaidMedia *PaidMediaInfo `json:"paid_media,omitempty"` // Since: Bot API 7.6
Photo extypes.Slice[PhotoSize] `json:"photo,omitempty"`
LivePhoto *LivePhoto `json:"live_photo,omitempty"` // Since: Bot API 10.0
Sticker *Sticker `json:"sticker,omitempty"`
Story *Story `json:"story,omitempty"`
Video *Video `json:"video,omitempty"`
VideoNote *VideoNote `json:"video_note,omitempty"`
Voice *Voice `json:"voice,omitempty"`
Caption string `json:"caption,omitempty"`
CaptionEntities []MessageEntity `json:"caption_entities,omitempty"`
ShowCaptionAboveMedia bool `json:"show_caption_above_media,omitempty"`
HasMediaSpoiler bool `json:"has_media_spoiler,omitempty"`
Checklist *Checklist `json:"checklist,omitempty"`
VideoNote *VideoNote `json:"video_note,omitempty"` // Since: Bot API 3.0
Voice *Voice `json:"voice,omitempty"` // Since: Bot API 1.2
Caption string `json:"caption,omitempty"` // Since: Bot API 3.4
CaptionEntities []MessageEntity `json:"caption_entities,omitempty"` // Since: Bot API 3.4
ShowCaptionAboveMedia bool `json:"show_caption_above_media,omitempty"` // Since: Bot API 7.4
HasMediaSpoiler bool `json:"has_media_spoiler,omitempty"` // Since: Bot API 6.4
Checklist *Checklist `json:"checklist,omitempty"` // Since: Bot API 9.1
Contact *Contact `json:"contact,omitempty"`
Dice *Dice `json:"dice,omitempty"`
Game *Game `json:"game,omitempty"`
@@ -163,63 +184,64 @@ type Message struct {
GroupChatCreated bool `json:"group_chat_created,omitempty"`
SupergroupChatCreated bool `json:"supergroup_chat_created,omitempty"`
ChannelChatCreated bool `json:"channel_chat_created,omitempty"`
MessageAutoDeleteTimerChanged *MessageAutoDeleteTimerChanged `json:"message_auto_delete_timer_changed,omitempty"`
MessageAutoDeleteTimerChanged *MessageAutoDeleteTimerChanged `json:"message_auto_delete_timer_changed,omitempty"` // Since: Bot API 5.1
MigrateToChatID int64 `json:"migrate_to_chat_id,omitempty"`
MigrateFromChatID int64 `json:"migrate_from_chat_id,omitempty"`
PinnedMessage *MaybeInaccessibleMessage `json:"pinned_message,omitempty"`
Invoice *Invoice `json:"invoice,omitempty"`
SuccessfulPayment *SuccessfulPayment `json:"successful_payment,omitempty"`
RefundedPayment *RefundedPayment `json:"refunded_payment,omitempty"`
UsersShared *UsersShared `json:"users_shared,omitempty"`
ChatShared *ChatShared `json:"chat_shared,omitempty"`
Gift *GiftInfo `json:"gift,omitempty"`
UniqueGift *UniqueGiftInfo `json:"unique_gift,omitempty"`
GiftUpgradeSent *GiftInfo `json:"gift_upgrade_sent,omitempty"`
Invoice *Invoice `json:"invoice,omitempty"` // Since: Bot API 3.0
SuccessfulPayment *SuccessfulPayment `json:"successful_payment,omitempty"` // Since: Bot API 3.0
RefundedPayment *RefundedPayment `json:"refunded_payment,omitempty"` // Since: Bot API 7.7
UsersShared *UsersShared `json:"users_shared,omitempty"` // Since: Bot API 6.5
ChatShared *ChatShared `json:"chat_shared,omitempty"` // Since: Bot API 6.5
Gift *GiftInfo `json:"gift,omitempty"` // Since: Bot API 9.0
UniqueGift *UniqueGiftInfo `json:"unique_gift,omitempty"` // Since: Bot API 9.0
GiftUpgradeSent *GiftInfo `json:"gift_upgrade_sent,omitempty"` // Since: Bot API 9.3
ConnectedWebsite string `json:"connected_website,omitempty"`
WriteAccessAllowed *WriteAccessAllowed `json:"write_access_allowed,omitempty"`
WriteAccessAllowed *WriteAccessAllowed `json:"write_access_allowed,omitempty"` // Since: Bot API 6.4
PassportData *PassportData `json:"passport_data,omitempty"`
ProximityAlertTriggered *ProximityAlertTriggered `json:"proximity_alert_triggered,omitempty"`
BoostAdded *ChatBoostAdded `json:"boost_added,omitempty"`
ChatBackgroundSet *ChatBackground `json:"chat_background_set,omitempty"`
ProximityAlertTriggered *ProximityAlertTriggered `json:"proximity_alert_triggered,omitempty"` // Since: Bot API 5.0
BoostAdded *ChatBoostAdded `json:"boost_added,omitempty"` // Since: Bot API 7.1
ChatBackgroundSet *ChatBackground `json:"chat_background_set,omitempty"` // Since: Bot API 7.5
ChecklistTaskDone *ChecklistTaskDone `json:"checklist_task_done,omitempty"`
ChecklistTasksAdded *ChecklistTasksAdded `json:"checklist_tasks_added,omitempty"`
DirectMessagePriceChanged *DirectMessagePriceChanged `json:"direct_message_price_changed,omitempty"`
ForumTopicCreated *ForumTopicCreated `json:"forum_topic_created,omitempty"`
ForumTopicEdited *ForumTopicEdited `json:"forum_topic_edited,omitempty"`
ForumTopicClosed *ForumTopicClosed `json:"forum_topic_closed,omitempty"`
ForumTopicReopened *ForumTopicReopened `json:"forum_topic_reopened,omitempty"`
GeneralForumTopicHidden *GeneralForumTopicHidden `json:"general_forum_topic_hidden,omitempty"`
GeneralForumTopicUnhidden *GeneralForumTopicUnhidden `json:"general_forum_topic_unhidden,omitempty"`
ChecklistTaskDone *ChecklistTaskDone `json:"checklist_task_done,omitempty"` // Since: Bot API 9.1
ChecklistTasksAdded *ChecklistTasksAdded `json:"checklist_tasks_added,omitempty"` // Since: Bot API 9.1
DirectMessagePriceChanged *DirectMessagePriceChanged `json:"direct_message_price_changed,omitempty"` // Since: Bot API 9.1
PaidMessagePriceChanged *PaidMessagePriceChanged `json:"paid_message_price_changed,omitempty"` // Since: Bot API 9.x
ForumTopicCreated *ForumTopicCreated `json:"forum_topic_created,omitempty"` // Since: Bot API 6.3
ForumTopicEdited *ForumTopicEdited `json:"forum_topic_edited,omitempty"` // Since: Bot API 6.4
ForumTopicClosed *ForumTopicClosed `json:"forum_topic_closed,omitempty"` // Since: Bot API 6.3
ForumTopicReopened *ForumTopicReopened `json:"forum_topic_reopened,omitempty"` // Since: Bot API 6.3
GeneralForumTopicHidden *GeneralForumTopicHidden `json:"general_forum_topic_hidden,omitempty"` // Since: Bot API 6.4
GeneralForumTopicUnhidden *GeneralForumTopicUnhidden `json:"general_forum_topic_unhidden,omitempty"` // Since: Bot API 6.4
GiveawayCreated *GiveawayCreated `json:"giveaway_created,omitempty"`
Giveaway *Giveaway `json:"giveaway,omitempty"`
GiveawayWinners *GiveawayWinners `json:"giveaway_winners,omitempty"`
GiveawayCompleted *GiveawayCompleted `json:"giveaway_completed,omitempty"`
GiveawayCreated *GiveawayCreated `json:"giveaway_created,omitempty"` // Since: Bot API 7.0
Giveaway *Giveaway `json:"giveaway,omitempty"` // Since: Bot API 7.0
GiveawayWinners *GiveawayWinners `json:"giveaway_winners,omitempty"` // Since: Bot API 7.0
GiveawayCompleted *GiveawayCompleted `json:"giveaway_completed,omitempty"` // Since: Bot API 7.0
ManagedBotCreated *ManagedBotCreated `json:"managed_bot_created,omitempty"`
PaidMessagePriceChanged *PaidMessagePriceChanged `json:"paid_message_price_changed,omitempty"`
PollOptionAdded *PollOptionAdded `json:"poll_option_added,omitempty"`
PollOptionDeleted *PollOptionDeleted `json:"poll_option_deleted,omitempty"`
ManagedBotCreated *ManagedBotCreated `json:"managed_bot_created,omitempty"` // Since: Bot API 9.6
PollOptionAdded *PollOptionAdded `json:"poll_option_added,omitempty"` // Since: Bot API 9.6
PollOptionDeleted *PollOptionDeleted `json:"poll_option_deleted,omitempty"` // Since: Bot API 9.6
SuggestedPostApproved *SuggestedPostApproved `json:"suggested_post_approved,omitempty"`
SuggestedPostApprovalFailed *SuggestedPostApprovalFailed `json:"suggested_post_approval_failed,omitempty"`
SuggestedPostDeclined *SuggestedPostDeclined `json:"suggested_post_declined,omitempty"`
SuggestedPostPaid *SuggestedPostPaid `json:"suggested_post_paid,omitempty"`
SuggestedPostRefunded *SuggestedPostRefunded `json:"suggested_post_refunded,omitempty"`
SuggestedPostApproved *SuggestedPostApproved `json:"suggested_post_approved,omitempty"` // Since: Bot API 9.1
SuggestedPostApprovalFailed *SuggestedPostApprovalFailed `json:"suggested_post_approval_failed,omitempty"` // Since: Bot API 9.1
SuggestedPostDeclined *SuggestedPostDeclined `json:"suggested_post_declined,omitempty"` // Since: Bot API 9.1
SuggestedPostPaid *SuggestedPostPaid `json:"suggested_post_paid,omitempty"` // Since: Bot API 9.1
SuggestedPostRefunded *SuggestedPostRefunded `json:"suggested_post_refunded,omitempty"` // Since: Bot API 9.1
VideoChatScheduled *VideoChatScheduled `json:"video_chat_scheduled,omitempty"`
VideoChatStarted *VideoChatStarted `json:"video_chat_started,omitempty"`
VideoChatEnded *VideoChatEnded `json:"video_chat_ended,omitempty"`
VideoChatParticipantsInvited *VideoChatParticipantsInvited `json:"video_chat_participants_invited,omitempty"`
VideoChatScheduled *VideoChatScheduled `json:"video_chat_scheduled,omitempty"` // Since: Bot API 6.0
VideoChatStarted *VideoChatStarted `json:"video_chat_started,omitempty"` // Since: Bot API 5.1
VideoChatEnded *VideoChatEnded `json:"video_chat_ended,omitempty"` // Since: Bot API 5.1
VideoChatParticipantsInvited *VideoChatParticipantsInvited `json:"video_chat_participants_invited,omitempty"` // Since: Bot API 5.1
WebAppData *WebAppData `json:"web_app_data,omitempty"`
ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"`
WebAppData *WebAppData `json:"web_app_data,omitempty"` // Since: Bot API 6.0
ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` // Since: Bot API 4.3
}
// InaccessibleMessage describes a message that was deleted or is otherwise inaccessible.
// Since: Bot API 7.0
// See https://core.telegram.org/bots/api#inaccessiblemessage
type InaccessibleMessage struct {
Chat Chat `json:"chat"`
@@ -228,6 +250,7 @@ type InaccessibleMessage struct {
}
// MaybeInaccessibleMessage is a union type that can be either Message or InaccessibleMessage.
// Since: Bot API 7.0
// See https://core.telegram.org/bots/api#maybeinaccessiblemessage
type MaybeInaccessibleMessage struct {
msg *Message
@@ -331,11 +354,11 @@ const (
// MessageEntityStrike identifies strikethrough text.
MessageEntityStrike MessageEntityType = "strikethrough"
// MessageEntitySpoiler identifies spoiler text.
MessageEntitySpoiler MessageEntityType = "spoiler"
MessageEntitySpoiler MessageEntityType = "spoiler" // Since: Bot API 5.6
// MessageEntityBlockquote identifies a blockquote entity.
MessageEntityBlockquote MessageEntityType = "blockquote"
// MessageEntityExpandableBlockquote identifies an expandable blockquote entity.
MessageEntityExpandableBlockquote MessageEntityType = "expandable_blockquote"
MessageEntityExpandableBlockquote MessageEntityType = "expandable_blockquote" // Since: Bot API 7.5
// MessageEntityCode identifies inline code.
MessageEntityCode MessageEntityType = "code"
// MessageEntityPre identifies a preformatted block.
@@ -345,12 +368,13 @@ const (
// MessageEntityTextMention identifies a text mention.
MessageEntityTextMention MessageEntityType = "text_mention"
// MessageEntityCustomEmoji identifies a custom emoji entity.
MessageEntityCustomEmoji MessageEntityType = "custom_emoji"
MessageEntityCustomEmoji MessageEntityType = "custom_emoji" // Since: Bot API 6.2
// MessageEntityDateTime identifies a date-time entity.
MessageEntityDateTime MessageEntityType = "date_time"
MessageEntityDateTime MessageEntityType = "date_time" // Since: Bot API 9.5
)
// MessageEntity represents one special entity in a text message.
// Since: Bot API 2.0
// See https://core.telegram.org/bots/api#messageentity
type MessageEntity struct {
Type MessageEntityType `json:"type"`
@@ -360,13 +384,14 @@ type MessageEntity struct {
URL string `json:"url,omitempty"`
User *User `json:"user,omitempty"`
Language string `json:"language,omitempty"`
CustomEmojiID string `json:"custom_emoji_id,omitempty"`
CustomEmojiID string `json:"custom_emoji_id,omitempty"` // Since: Bot API 6.2
UnixTime int `json:"unix_time,omitempty"`
DateTimeFormat string `json:"date_time_format,omitempty"`
UnixTime int64 `json:"unix_time,omitempty"`
DateTimeFormat string `json:"date_time_format,omitempty"` // Since: Bot API 9.5
}
// ReplyParameters describes the parameters to use when replying to a message.
// Since: Bot API 7.0
// See https://core.telegram.org/bots/api#replyparameters
type ReplyParameters struct {
MessageID int `json:"message_id"`
@@ -382,6 +407,7 @@ type ReplyParameters struct {
}
// LinkPreviewOptions describes the options used for link preview generation.
// Since: Bot API 7.0
// See https://core.telegram.org/bots/api#linkpreviewoptions
type LinkPreviewOptions struct {
IsDisabled bool `json:"is_disabled,omitempty"`
@@ -392,6 +418,7 @@ type LinkPreviewOptions struct {
}
// ReplyMarkup represents a custom keyboard or inline keyboard.
// Since: Bot API 1.0
// See https://core.telegram.org/bots/api#replymarkup
type ReplyMarkup struct {
InlineKeyboard [][]InlineKeyboardButton `json:"inline_keyboard,omitempty"`
@@ -409,6 +436,7 @@ type ReplyMarkup struct {
}
// InlineKeyboardMarkup represents an inline keyboard that appears right next to the message it belongs to.
// Since: Bot API 2.0
// See https://core.telegram.org/bots/api#inlinekeyboardmarkup
type InlineKeyboardMarkup struct {
InlineKeyboard [][]InlineKeyboardButton `json:"inline_keyboard,omitempty"`
@@ -427,21 +455,23 @@ const (
)
// KeyboardButton represents one button of the reply keyboard.
// Since: Bot API 2.0
// See https://core.telegram.org/bots/api#keyboardbutton
type KeyboardButton struct {
Text string `json:"text"`
IconCustomEmojiID string `json:"icon_custom_emoji_id,omitempty"`
Style KeyboardButtonStyle `json:"style,omitempty"`
RequestUsers *KeyboardButtonRequestUsers `json:"request_users,omitempty"`
RequestChat *KeyboardButtonRequestChat `json:"request_chat,omitempty"`
RequestManagedBot *KeyboardButtonRequestManagedBot `json:"request_managed_bot,omitempty"`
IconCustomEmojiID string `json:"icon_custom_emoji_id,omitempty"` // Since: Bot API 9.4
Style KeyboardButtonStyle `json:"style,omitempty"` // Since: Bot API 9.4
RequestUsers *KeyboardButtonRequestUsers `json:"request_users,omitempty"` // Since: Bot API 7.0
RequestChat *KeyboardButtonRequestChat `json:"request_chat,omitempty"` // Since: Bot API 6.5
RequestManagedBot *KeyboardButtonRequestManagedBot `json:"request_managed_bot,omitempty"` // Since: Bot API 9.6
RequestContact bool `json:"request_contact,omitempty"`
RequestLocation bool `json:"request_location,omitempty"`
RequestPoll *KeyboardButtonPollType `json:"request_poll,omitempty"`
WebApp *WebAppInfo `json:"web_app,omitempty"`
RequestPoll *KeyboardButtonPollType `json:"request_poll,omitempty"` // Since: Bot API 4.6
WebApp *WebAppInfo `json:"web_app,omitempty"` // Since: Bot API 6.0
}
// KeyboardButtonRequestUsers defines criteria used to request suitable users.
// Since: Bot API 7.0
// See https://core.telegram.org/bots/api#keyboardbuttonrequestusers
type KeyboardButtonRequestUsers struct {
RequestID int `json:"request_id"`
@@ -454,6 +484,7 @@ type KeyboardButtonRequestUsers struct {
}
// KeyboardButtonRequestChat defines criteria used to request a suitable chat.
// Since: Bot API 6.5
// See https://core.telegram.org/bots/api#keyboardbuttonrequestchat
type KeyboardButtonRequestChat struct {
RequestID int `json:"request_id"`
@@ -470,6 +501,7 @@ type KeyboardButtonRequestChat struct {
}
// KeyboardButtonRequestManagedBot defines criteria used to request a managed bot.
// Since: Bot API 9.6
// See https://core.telegram.org/bots/api#keyboardbuttonrequestmanagedbot
type KeyboardButtonRequestManagedBot struct {
RequestID int32 `json:"request_id"`
@@ -478,22 +510,25 @@ type KeyboardButtonRequestManagedBot struct {
}
// KeyboardButtonPollType represents the type of a poll that may be created from a keyboard button.
// Since: Bot API 4.6
// See https://core.telegram.org/bots/api#keyboardbuttonpolltype
type KeyboardButtonPollType struct {
Type PollType `json:"type,omitempty"`
}
// InlineKeyboardButton represents one button of an inline keyboard.
// Since: Bot API 2.0
// See https://core.telegram.org/bots/api#inlinekeyboardbutton
type InlineKeyboardButton struct {
Text string `json:"text"`
URL string `json:"url,omitempty"`
CallbackData string `json:"callback_data,omitempty"`
Style KeyboardButtonStyle `json:"style,omitempty"`
IconCustomEmojiID string `json:"icon_custom_emoji_id,omitempty"`
Style KeyboardButtonStyle `json:"style,omitempty"` // Since: Bot API 9.4
IconCustomEmojiID string `json:"icon_custom_emoji_id,omitempty"` // Since: Bot API 9.4
}
// ReplyKeyboardMarkup represents a custom keyboard with reply options.
// Since: Bot API 1.0
// See https://core.telegram.org/bots/api#replykeyboardmarkup
type ReplyKeyboardMarkup struct {
Keyboard [][]KeyboardButton `json:"keyboard"`
@@ -505,6 +540,7 @@ type ReplyKeyboardMarkup struct {
}
// CallbackQuery represents an incoming callback query from a callback button in an inline keyboard.
// Since: Bot API 2.0
// See https://core.telegram.org/bots/api#callbackquery
type CallbackQuery struct {
ID string `json:"id"`
@@ -541,6 +577,7 @@ const (
)
// MessageReactionUpdated represents a change of a reaction on a message.
// Since: Bot API 7.0
// See https://core.telegram.org/bots/api#messagereactionupdated
type MessageReactionUpdated struct {
Chat *Chat `json:"chat"`
@@ -553,6 +590,7 @@ type MessageReactionUpdated struct {
}
// MessageReactionCountUpdated represents a change in the count of reactions on a message.
// Since: Bot API 7.0
// See https://core.telegram.org/bots/api#messagereactioncountupdated
type MessageReactionCountUpdated struct {
Chat *Chat `json:"chat"`
@@ -562,6 +600,7 @@ type MessageReactionCountUpdated struct {
}
// ReactionType describes the type of a reaction.
// Since: Bot API 7.0
// See https://core.telegram.org/bots/api#reactiontype
type ReactionType struct {
Type string `json:"type"`
@@ -572,6 +611,7 @@ type ReactionType struct {
}
// ReactionCount represents a reaction added to a message along with the number of times it was added.
// Since: Bot API 7.0
// See https://core.telegram.org/bots/api#reactioncount
type ReactionCount struct {
Type ReactionType `json:"type"`
@@ -579,12 +619,14 @@ type ReactionCount struct {
}
// SuggestedPostPrice represents the price of a suggested post.
// Since: Bot API 9.1
type SuggestedPostPrice struct {
Currency string `json:"currency"`
Amount int `json:"amount"`
}
// SuggestedPostInfo contains information about a suggested post.
// Since: Bot API 9.1
// See https://core.telegram.org/bots/api#suggestedpostinfo
type SuggestedPostInfo struct {
State string `json:"state"` // "pending", "approved", or "declined"
@@ -593,24 +635,29 @@ type SuggestedPostInfo struct {
}
// SuggestedPostParameters holds parameters for suggesting a post.
// Since: Bot API 9.2
type SuggestedPostParameters struct {
Price SuggestedPostPrice `json:"price"`
SendDate int `json:"send_date"`
}
// ManagedBotCreated describes a service message about a newly created managed bot.
// Since: Bot API 9.6
// See https://core.telegram.org/bots/api#managedbotcreated
type ManagedBotCreated struct {
Bot User `json:"bot"`
}
// ManagedBotUpdated describes an update about a managed bot and its manager.
// Since: Bot API 9.6
// See https://core.telegram.org/bots/api#managedbotupdated
type ManagedBotUpdated struct {
User User `json:"user"`
Bot User `json:"bot"`
}
// SharedUser represents a user shared via a KeyboardButtonRequestUsers button.
// Since: Bot API 7.2
type SharedUser struct {
UserID int64 `json:"user_id"`
FirstName string `json:"first_name,omitempty"`
@@ -618,10 +665,16 @@ type SharedUser struct {
Username string `json:"username,omitempty"`
Photo []PhotoSize `json:"photo,omitempty"`
}
// UsersShared represents a service message about users shared via a KeyboardButtonRequestUsers button.
// Since: Bot API 6.5
type UsersShared struct {
RequestID int `json:"request_id"`
Users []SharedUser `json:"users"`
}
// ChatShared represents a service message about a chat shared via a KeyboardButtonRequestChat button.
// Since: Bot API 6.5
type ChatShared struct {
RequestID int `json:"request_id"`
ChatID int64 `json:"chat_id"`
@@ -630,37 +683,68 @@ type ChatShared struct {
Photo []PhotoSize `json:"photo,omitempty"`
}
// SuggestedPostApproved is a service message about an approved suggested post.
// Since: Bot API 9.1
type SuggestedPostApproved struct {
SuggestedPostMessage *Message `json:"suggested_post_message,omitempty"`
Price SuggestedPostPrice `json:"price"`
SendDate int `json:"send_date"`
}
// SuggestedPostApprovalFailed is a service message about a failed suggested post approval.
// Since: Bot API 9.1
type SuggestedPostApprovalFailed struct {
SuggestedPostMessage *Message `json:"suggested_post_message,omitempty"`
Price SuggestedPostPrice `json:"price"`
}
// SuggestedPostDeclined is a service message about a declined suggested post.
// Since: Bot API 9.1
type SuggestedPostDeclined struct {
SuggestedPostMessage *Message `json:"suggested_post_message,omitempty"`
Comment string `json:"comment,omitempty"`
}
// SuggestedPostPaid is a service message about a paid suggested post.
// Since: Bot API 9.1
type SuggestedPostPaid struct {
SuggestedPostMessage *Message `json:"suggested_post_message,omitempty"`
Currency string `json:"currency"`
Amount int `json:"amount"`
StarAmount *StarAmount `json:"star_amount,omitempty"`
}
// SuggestedPostRefunded is a service message about a refunded suggested post.
// Since: Bot API 9.1
type SuggestedPostRefunded struct {
SuggestedPostMessage *Message `json:"suggested_post_message,omitempty"`
Reason string `json:"reason,omitempty"`
}
// VideoChatScheduled represents a service message about a video chat scheduled in the chat.
// Since: Bot API 6.0
type VideoChatScheduled struct {
StartDate int64 `json:"start_date"`
}
// VideoChatStarted represents a service message about a video chat started in the chat.
// Since: Bot API 5.1
type VideoChatStarted struct{}
// VideoChatEnded represents a service message about a video chat ended in the chat.
// Since: Bot API 5.1
type VideoChatEnded struct {
Duration int64 `json:"duration"`
}
// VideoChatParticipantsInvited represents a service message about new members invited to a video chat.
// Since: Bot API 5.1
type VideoChatParticipantsInvited struct {
Users []User `json:"users"`
}
// SentGuestMessage describes an inline message sent by a guest bot.
// Since: Bot API 10.0
type SentGuestMessage struct {
InlineMessageID string `json:"inline_message_id"`
}
+4 -4
View File
@@ -4,12 +4,12 @@ package tgapi
type ParseMode string
const (
// ParseMDV2 enables MarkdownV2 style parsing.
ParseMDV2 ParseMode = "MarkdownV2"
// ParseMarkdownV2 enables MarkdownV2 style parsing.
ParseMarkdownV2 ParseMode = "MarkdownV2"
// ParseHTML enables HTML style parsing.
ParseHTML ParseMode = "HTML"
// ParseMD enables legacy Markdown style parsing.
ParseMD ParseMode = "Markdown"
// ParseMarkdown enables legacy Markdown style parsing.
ParseMarkdown ParseMode = "Markdown"
// ParseNone disables parse_mode and leaves plain-text requests unannotated.
ParseNone ParseMode = ""
)
+1 -1
View File
@@ -25,7 +25,7 @@ func TestParseModeStillSerializesExplicitModes(t *testing.T) {
data, err := json.Marshal(SendMessage{
ChatID: 42,
Text: "hello",
ParseMode: ParseMDV2,
ParseMode: ParseMarkdownV2,
})
if err != nil {
t.Fatalf("Marshal returned error: %v", err)
+3
View File
@@ -3,6 +3,7 @@ package tgapi
import "context"
// SetPassportDataErrors holds parameters for the setPassportDataErrors method.
// Since: Bot API 4.0
// See https://core.telegram.org/bots/api#setpassportdataerrors
type SetPassportDataErrors struct {
UserID int64 `json:"user_id"`
@@ -10,6 +11,7 @@ type SetPassportDataErrors struct {
}
// SetPassportDataErrors informs a user about Telegram Passport data errors.
// Since: Bot API 4.0
// Returns true on success.
// See https://core.telegram.org/bots/api#setpassportdataerrors
func (api *API) SetPassportDataErrors(params SetPassportDataErrors) (bool, error) {
@@ -18,6 +20,7 @@ func (api *API) SetPassportDataErrors(params SetPassportDataErrors) (bool, error
}
// SetPassportDataErrorsWithContext is the context-aware variant of SetPassportDataErrors.
// Since: Bot API 4.0
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#setpassportdataerrors
func (api *API) SetPassportDataErrorsWithContext(ctx context.Context, params SetPassportDataErrors) (bool, error) {
+13
View File
@@ -1,15 +1,22 @@
package tgapi
// PassportData contains information about Telegram Passport data shared with the bot.
// Since: Bot API 4.0
type PassportData struct {
Data []EncryptedPassportElement `json:"data"`
Credentials EncryptedCredentials `json:"credentials"`
}
// PassportFile represents a file uploaded to Telegram Passport.
// Since: Bot API 4.0
type PassportFile struct {
FileID string `json:"file_id"`
FileUniqueID string `json:"file_unique_id"`
FileSize int64 `json:"file_size"`
FileDate int64 `json:"file_date"`
}
// PassportElementType represents the type of a Telegram Passport element.
type PassportElementType string
const (
@@ -28,6 +35,8 @@ const (
PassportEmailType PassportElementType = "email"
)
// EncryptedPassportElement contains information about documents or other Telegram Passport elements.
// Since: Bot API 4.0
type EncryptedPassportElement struct {
Type PassportElementType `json:"type"`
Data string `json:"data,omitempty"`
@@ -40,6 +49,9 @@ type EncryptedPassportElement struct {
Translation *PassportFile `json:"translation,omitempty"`
Hash string `json:"hash,omitempty"`
}
// EncryptedCredentials contains data required for decrypting and authenticating EncryptedPassportElement.
// Since: Bot API 4.0
type EncryptedCredentials struct {
Data string `json:"data"`
Hash string `json:"hash"`
@@ -47,6 +59,7 @@ type EncryptedCredentials struct {
}
// PassportElementError is a JSON-serializable passport element error object.
// Since: Bot API 4.0
// See https://core.telegram.org/bots/api#passportelementerror
type PassportElementError struct {
Source string `json:"source"`
+12
View File
@@ -3,6 +3,7 @@ package tgapi
import "context"
// SendInvoice holds parameters for the sendInvoice method.
// Since: Bot API 3.0
// See https://core.telegram.org/bots/api#sendinvoice
type SendInvoice struct {
ChatID int64 `json:"chat_id"`
@@ -42,6 +43,7 @@ type SendInvoice struct {
}
// SendInvoice sends an invoice.
// Since: Bot API 3.0
// See https://core.telegram.org/bots/api#sendinvoice
func (api *API) SendInvoice(params SendInvoice) (Message, error) {
req := NewRequestWithChatID[Message]("sendInvoice", params, params.ChatID)
@@ -49,6 +51,7 @@ func (api *API) SendInvoice(params SendInvoice) (Message, error) {
}
// SendInvoiceWithContext is the context-aware variant of SendInvoice.
// Since: Bot API 3.0
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#sendinvoice
func (api *API) SendInvoiceWithContext(ctx context.Context, params SendInvoice) (Message, error) {
@@ -57,6 +60,7 @@ func (api *API) SendInvoiceWithContext(ctx context.Context, params SendInvoice)
}
// CreateInvoiceLink holds parameters for the createInvoiceLink method.
// Since: Bot API 6.1
// See https://core.telegram.org/bots/api#createinvoicelink
type CreateInvoiceLink struct {
BusinessConnectionID string `json:"business_connection_id,omitempty"`
@@ -86,6 +90,7 @@ type CreateInvoiceLink struct {
}
// CreateInvoiceLink creates an invoice link.
// Since: Bot API 6.1
// See https://core.telegram.org/bots/api#createinvoicelink
func (api *API) CreateInvoiceLink(params CreateInvoiceLink) (string, error) {
req := NewRequest[string]("createInvoiceLink", params)
@@ -93,6 +98,7 @@ func (api *API) CreateInvoiceLink(params CreateInvoiceLink) (string, error) {
}
// CreateInvoiceLinkWithContext is the context-aware variant of CreateInvoiceLink.
// Since: Bot API 6.1
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#createinvoicelink
func (api *API) CreateInvoiceLinkWithContext(ctx context.Context, params CreateInvoiceLink) (string, error) {
@@ -101,6 +107,7 @@ func (api *API) CreateInvoiceLinkWithContext(ctx context.Context, params CreateI
}
// AnswerShippingQuery holds parameters for the answerShippingQuery method.
// Since: Bot API 3.0
// See https://core.telegram.org/bots/api#answershippingquery
type AnswerShippingQuery struct {
ShippingQueryID string `json:"shipping_query_id"`
@@ -110,6 +117,7 @@ type AnswerShippingQuery struct {
}
// AnswerShippingQuery answers a shipping query.
// Since: Bot API 3.0
// Returns true on success.
// See https://core.telegram.org/bots/api#answershippingquery
func (api *API) AnswerShippingQuery(params AnswerShippingQuery) (bool, error) {
@@ -118,6 +126,7 @@ func (api *API) AnswerShippingQuery(params AnswerShippingQuery) (bool, error) {
}
// AnswerShippingQueryWithContext is the context-aware variant of AnswerShippingQuery.
// Since: Bot API 3.0
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#answershippingquery
func (api *API) AnswerShippingQueryWithContext(ctx context.Context, params AnswerShippingQuery) (bool, error) {
@@ -126,6 +135,7 @@ func (api *API) AnswerShippingQueryWithContext(ctx context.Context, params Answe
}
// AnswerPreCheckoutQuery holds parameters for the answerPreCheckoutQuery method.
// Since: Bot API 3.0
// See https://core.telegram.org/bots/api#answerprecheckoutquery
type AnswerPreCheckoutQuery struct {
PreCheckoutQueryID string `json:"pre_checkout_query_id"`
@@ -134,6 +144,7 @@ type AnswerPreCheckoutQuery struct {
}
// AnswerPreCheckoutQuery answers a pre-checkout query.
// Since: Bot API 3.0
// Returns true on success.
// See https://core.telegram.org/bots/api#answerprecheckoutquery
func (api *API) AnswerPreCheckoutQuery(params AnswerPreCheckoutQuery) (bool, error) {
@@ -142,6 +153,7 @@ func (api *API) AnswerPreCheckoutQuery(params AnswerPreCheckoutQuery) (bool, err
}
// AnswerPreCheckoutQueryWithContext is the context-aware variant of AnswerPreCheckoutQuery.
// Since: Bot API 3.0
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#answerprecheckoutquery
func (api *API) AnswerPreCheckoutQueryWithContext(ctx context.Context, params AnswerPreCheckoutQuery) (bool, error) {
+16 -3
View File
@@ -1,12 +1,15 @@
package tgapi
// LabeledPrice represents a price portion.
// Since: Bot API 3.0
// See https://core.telegram.org/bots/api#labeledprice
type LabeledPrice struct {
Label string `json:"label"`
Amount int `json:"amount"`
}
// Invoice contains basic information about an invoice.
// Since: Bot API 3.0
type Invoice struct {
Title string `json:"title"`
Description string `json:"description"`
@@ -16,6 +19,7 @@ type Invoice struct {
}
// ShippingQuery represents an incoming shipping query.
// Since: Bot API 3.0
// See https://core.telegram.org/bots/api#shippingquery
type ShippingQuery struct {
ID string `json:"id"`
@@ -25,6 +29,7 @@ type ShippingQuery struct {
}
// ShippingAddress represents a shipping address.
// Since: Bot API 3.0
// See https://core.telegram.org/bots/api#shippingaddress
type ShippingAddress struct {
CountryCode string `json:"country_code"`
@@ -36,6 +41,7 @@ type ShippingAddress struct {
}
// OrderInfo represents information about an order.
// Since: Bot API 3.0
// See https://core.telegram.org/bots/api#orderinfo
type OrderInfo struct {
Name string `json:"name"`
@@ -45,6 +51,7 @@ type OrderInfo struct {
}
// PreCheckoutQuery represents an incoming pre-checkout query.
// Since: Bot API 3.0
// See https://core.telegram.org/bots/api#precheckoutquery
type PreCheckoutQuery struct {
ID string `json:"id"`
@@ -57,6 +64,7 @@ type PreCheckoutQuery struct {
}
// PaidMediaPurchased represents a purchased paid media.
// Since: Bot API 7.10
// See https://core.telegram.org/bots/api#paidmediapurchased
type PaidMediaPurchased struct {
From User `json:"from"`
@@ -64,6 +72,7 @@ type PaidMediaPurchased struct {
}
// ShippingOption represents one shipping option.
// Since: Bot API 3.0
// See https://core.telegram.org/bots/api#shippingoption
type ShippingOption struct {
ID string `json:"id"`
@@ -71,14 +80,16 @@ type ShippingOption struct {
Prices []LabeledPrice `json:"prices"`
}
// SuccessfulPayment contains basic information about a successful payment.
// Since: Bot API 3.0
type SuccessfulPayment struct {
Currency string `json:"currency"`
TotalAmount int `json:"total_amount"`
InvoicePayload string `json:"invoice_payload"`
SubscriptionExpirationDate int `json:"subscription_expiration_date,omitempty"`
IsRecurring bool `json:"is_recurring,omitempty"`
IsFirstRecurring bool `json:"is_first_recurring,omitempty"`
SubscriptionExpirationDate int `json:"subscription_expiration_date,omitempty"` // Since: Bot API 8.0
IsRecurring bool `json:"is_recurring,omitempty"` // Since: Bot API 8.0
IsFirstRecurring bool `json:"is_first_recurring,omitempty"` // Since: Bot API 8.0
ShippingOptionID string `json:"shipping_option_id,omitempty"`
OrderInfo *OrderInfo `json:"order_info,omitempty"`
@@ -86,6 +97,8 @@ type SuccessfulPayment struct {
ProviderPaymentChargeID string `json:"proviced_payment_charge_id"`
}
// RefundedPayment contains basic information about a refunded payment.
// Since: Bot API 7.7
type RefundedPayment struct {
Currency string `json:"currency"`
TotalAmount int `json:"total_amount"`
+11
View File
@@ -3,6 +3,7 @@ package tgapi
import "context"
// GetStarTransactions holds parameters for the getStarTransactions method.
// Since: Bot API 7.5
// See https://core.telegram.org/bots/api#getstartransactions
type GetStarTransactions struct {
Offset int `json:"offset,omitempty"`
@@ -10,6 +11,7 @@ type GetStarTransactions struct {
}
// GetMyStarBalance returns the bot's Telegram Star balance.
// Since: Bot API 7.5
// See https://core.telegram.org/bots/api#getmystarbalance
func (api *API) GetMyStarBalance() (StarAmount, error) {
req := NewRequest[StarAmount]("getMyStarBalance", NoParams)
@@ -17,6 +19,7 @@ func (api *API) GetMyStarBalance() (StarAmount, error) {
}
// GetMyStarBalanceWithContext is the context-aware variant of GetMyStarBalance.
// Since: Bot API 7.5
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#getmystarbalance
func (api *API) GetMyStarBalanceWithContext(ctx context.Context) (StarAmount, error) {
@@ -25,6 +28,7 @@ func (api *API) GetMyStarBalanceWithContext(ctx context.Context) (StarAmount, er
}
// GetStarTransactions returns Telegram Star transactions for the bot.
// Since: Bot API 7.5
// See https://core.telegram.org/bots/api#getstartransactions
func (api *API) GetStarTransactions(params GetStarTransactions) (StarTransactions, error) {
req := NewRequest[StarTransactions]("getStarTransactions", params)
@@ -32,6 +36,7 @@ func (api *API) GetStarTransactions(params GetStarTransactions) (StarTransaction
}
// GetStarTransactionsWithContext is the context-aware variant of GetStarTransactions.
// Since: Bot API 7.5
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#getstartransactions
func (api *API) GetStarTransactionsWithContext(ctx context.Context, params GetStarTransactions) (StarTransactions, error) {
@@ -40,6 +45,7 @@ func (api *API) GetStarTransactionsWithContext(ctx context.Context, params GetSt
}
// RefundStarPayment holds parameters for the refundStarPayment method.
// Since: Bot API 7.4
// See https://core.telegram.org/bots/api#refundstarpayment
type RefundStarPayment struct {
UserID int64 `json:"user_id"`
@@ -47,6 +53,7 @@ type RefundStarPayment struct {
}
// RefundStarPayment refunds a successful Telegram Stars payment.
// Since: Bot API 7.4
// Returns true on success.
// See https://core.telegram.org/bots/api#refundstarpayment
func (api *API) RefundStarPayment(params RefundStarPayment) (bool, error) {
@@ -55,6 +62,7 @@ func (api *API) RefundStarPayment(params RefundStarPayment) (bool, error) {
}
// RefundStarPaymentWithContext is the context-aware variant of RefundStarPayment.
// Since: Bot API 7.4
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#refundstarpayment
func (api *API) RefundStarPaymentWithContext(ctx context.Context, params RefundStarPayment) (bool, error) {
@@ -63,6 +71,7 @@ func (api *API) RefundStarPaymentWithContext(ctx context.Context, params RefundS
}
// EditUserStarSubscription holds parameters for the editUserStarSubscription method.
// Since: Bot API 8.0
// See https://core.telegram.org/bots/api#edituserstarsubscription
type EditUserStarSubscription struct {
UserID int64 `json:"user_id"`
@@ -71,6 +80,7 @@ type EditUserStarSubscription struct {
}
// EditUserStarSubscription cancels or re-enables a user star subscription extension.
// Since: Bot API 8.0
// Returns true on success.
// See https://core.telegram.org/bots/api#edituserstarsubscription
func (api *API) EditUserStarSubscription(params EditUserStarSubscription) (bool, error) {
@@ -79,6 +89,7 @@ func (api *API) EditUserStarSubscription(params EditUserStarSubscription) (bool,
}
// EditUserStarSubscriptionWithContext is the context-aware variant of EditUserStarSubscription.
// Since: Bot API 8.0
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#edituserstarsubscription
func (api *API) EditUserStarSubscriptionWithContext(ctx context.Context, params EditUserStarSubscription) (bool, error) {
+2
View File
@@ -1,6 +1,7 @@
package tgapi
// StarTransaction describes a Telegram Star transaction.
// Since: Bot API 7.5
// See https://core.telegram.org/bots/api#startransaction
type StarTransaction struct {
ID string `json:"id"`
@@ -12,6 +13,7 @@ type StarTransaction struct {
}
// StarTransactions contains a list of Telegram Star transactions.
// Since: Bot API 7.5
// See https://core.telegram.org/bots/api#startransactions
type StarTransactions struct {
Transactions []StarTransaction `json:"transactions"`
+48
View File
@@ -3,6 +3,7 @@ package tgapi
import "context"
// SendSticker holds parameters for the sendSticker method.
// Since: Bot API 1.3
// See https://core.telegram.org/bots/api#sendsticker
type SendSticker struct {
BusinessConnectionID string `json:"business_connection_id,omitempty"`
@@ -23,6 +24,7 @@ type SendSticker struct {
}
// SendSticker sends a static .WEBP, animated .TGS, or video .WEBM sticker.
// Since: Bot API 1.3
// See https://core.telegram.org/bots/api#sendsticker
func (api *API) SendSticker(params SendSticker) (Message, error) {
req := NewRequestWithChatID[Message]("sendSticker", params, params.ChatID)
@@ -30,6 +32,7 @@ func (api *API) SendSticker(params SendSticker) (Message, error) {
}
// SendStickerWithContext is the context-aware variant of SendSticker.
// Since: Bot API 1.3
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#sendsticker
func (api *API) SendStickerWithContext(ctx context.Context, params SendSticker) (Message, error) {
@@ -38,12 +41,14 @@ func (api *API) SendStickerWithContext(ctx context.Context, params SendSticker)
}
// GetStickerSet holds parameters for the getStickerSet method.
// Since: Bot API 3.2
// See https://core.telegram.org/bots/api#getstickerset
type GetStickerSet struct {
Name string `json:"name"`
}
// GetStickerSet returns a sticker set by its name.
// Since: Bot API 3.2
// See https://core.telegram.org/bots/api#getstickerset
func (api *API) GetStickerSet(params GetStickerSet) (StickerSet, error) {
req := NewRequest[StickerSet]("getStickerSet", params)
@@ -51,6 +56,7 @@ func (api *API) GetStickerSet(params GetStickerSet) (StickerSet, error) {
}
// GetStickerSetWithContext is the context-aware variant of GetStickerSet.
// Since: Bot API 3.2
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#getstickerset
func (api *API) GetStickerSetWithContext(ctx context.Context, params GetStickerSet) (StickerSet, error) {
@@ -59,12 +65,14 @@ func (api *API) GetStickerSetWithContext(ctx context.Context, params GetStickerS
}
// GetCustomEmojiStickers holds parameters for the getCustomEmojiStickers method.
// Since: Bot API 6.2
// See https://core.telegram.org/bots/api#getcustomemojistickers
type GetCustomEmojiStickers struct {
CustomEmojiIDs []string `json:"custom_emoji_ids"`
}
// GetCustomEmojiStickers returns information about custom emoji stickers by their IDs.
// Since: Bot API 6.2
// See https://core.telegram.org/bots/api#getcustomemojistickers
func (api *API) GetCustomEmojiStickers(params GetCustomEmojiStickers) ([]Sticker, error) {
req := NewRequest[[]Sticker]("getCustomEmojiStickers", params)
@@ -72,6 +80,7 @@ func (api *API) GetCustomEmojiStickers(params GetCustomEmojiStickers) ([]Sticker
}
// GetCustomEmojiStickersWithContext is the context-aware variant of GetCustomEmojiStickers.
// Since: Bot API 6.2
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#getcustomemojistickers
func (api *API) GetCustomEmojiStickersWithContext(ctx context.Context, params GetCustomEmojiStickers) ([]Sticker, error) {
@@ -80,6 +89,7 @@ func (api *API) GetCustomEmojiStickersWithContext(ctx context.Context, params Ge
}
// UploadStickerFile holds parameters for the uploadStickerFile method.
// Since: Bot API 3.2
// See https://core.telegram.org/bots/api#uploadstickerfile
type UploadStickerFile struct {
UserID int64 `json:"user_id"`
@@ -87,6 +97,7 @@ type UploadStickerFile struct {
}
// UploadStickerFile uploads a sticker file for later use in sticker set methods.
// Since: Bot API 3.2
// sticker is the file to upload.
// See https://core.telegram.org/bots/api#uploadstickerfile
func (api *API) UploadStickerFile(params UploadStickerFile, sticker UploaderFile) (File, error) {
@@ -99,6 +110,7 @@ func (api *API) UploadStickerFile(params UploadStickerFile, sticker UploaderFile
}
// UploadStickerFileWithContext is the context-aware variant of UploadStickerFile.
// Since: Bot API 3.2
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#uploadstickerfile
func (api *API) UploadStickerFileWithContext(ctx context.Context, params UploadStickerFile, sticker UploaderFile) (File, error) {
@@ -111,6 +123,7 @@ func (api *API) UploadStickerFileWithContext(ctx context.Context, params UploadS
}
// CreateNewStickerSet holds parameters for the createNewStickerSet method.
// Since: Bot API 3.2
// See https://core.telegram.org/bots/api#createnewstickerset
type CreateNewStickerSet struct {
UserID int64 `json:"user_id"`
@@ -123,6 +136,7 @@ type CreateNewStickerSet struct {
}
// CreateNewStickerSet creates a new sticker set owned by a user.
// Since: Bot API 3.2
// Returns True on success.
// See https://core.telegram.org/bots/api#createnewstickerset
func (api *API) CreateNewStickerSet(params CreateNewStickerSet) (bool, error) {
@@ -131,6 +145,7 @@ func (api *API) CreateNewStickerSet(params CreateNewStickerSet) (bool, error) {
}
// CreateNewStickerSetWithContext is the context-aware variant of CreateNewStickerSet.
// Since: Bot API 3.2
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#createnewstickerset
func (api *API) CreateNewStickerSetWithContext(ctx context.Context, params CreateNewStickerSet) (bool, error) {
@@ -139,6 +154,7 @@ func (api *API) CreateNewStickerSetWithContext(ctx context.Context, params Creat
}
// AddStickerToSet holds parameters for the addStickerToSet method.
// Since: Bot API 3.2
// See https://core.telegram.org/bots/api#addstickertoset
type AddStickerToSet struct {
UserID int64 `json:"user_id"`
@@ -147,6 +163,7 @@ type AddStickerToSet struct {
}
// AddStickerToSet adds a new sticker to a set created by the bot.
// Since: Bot API 3.2
// Returns True on success.
// See https://core.telegram.org/bots/api#addstickertoset
func (api *API) AddStickerToSet(params AddStickerToSet) (bool, error) {
@@ -155,6 +172,7 @@ func (api *API) AddStickerToSet(params AddStickerToSet) (bool, error) {
}
// AddStickerToSetWithContext is the context-aware variant of AddStickerToSet.
// Since: Bot API 3.2
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#addstickertoset
func (api *API) AddStickerToSetWithContext(ctx context.Context, params AddStickerToSet) (bool, error) {
@@ -163,6 +181,7 @@ func (api *API) AddStickerToSetWithContext(ctx context.Context, params AddSticke
}
// SetStickerPositionInSet holds parameters for the setStickerPositionInSet method.
// Since: Bot API 3.2
// See https://core.telegram.org/bots/api#setstickerpositioninset
type SetStickerPositionInSet struct {
Sticker string `json:"sticker"`
@@ -170,6 +189,7 @@ type SetStickerPositionInSet struct {
}
// SetStickerPositionInSet moves a sticker in a set to a specific position.
// Since: Bot API 3.2
// Returns True on success.
// See https://core.telegram.org/bots/api#setstickerpositioninset
func (api *API) SetStickerPositionInSet(params SetStickerPositionInSet) (bool, error) {
@@ -178,6 +198,7 @@ func (api *API) SetStickerPositionInSet(params SetStickerPositionInSet) (bool, e
}
// SetStickerPositionInSetWithContext is the context-aware variant of SetStickerPositionInSet.
// Since: Bot API 3.2
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#setstickerpositioninset
func (api *API) SetStickerPositionInSetWithContext(ctx context.Context, params SetStickerPositionInSet) (bool, error) {
@@ -186,12 +207,14 @@ func (api *API) SetStickerPositionInSetWithContext(ctx context.Context, params S
}
// DeleteStickerFromSet holds parameters for the deleteStickerFromSet method.
// Since: Bot API 3.2
// See https://core.telegram.org/bots/api#deletestickerfromset
type DeleteStickerFromSet struct {
Sticker string `json:"sticker"`
}
// DeleteStickerFromSet deletes a sticker from a set created by the bot.
// Since: Bot API 3.2
// Returns True on success.
// See https://core.telegram.org/bots/api#deletestickerfromset
func (api *API) DeleteStickerFromSet(params DeleteStickerFromSet) (bool, error) {
@@ -200,6 +223,7 @@ func (api *API) DeleteStickerFromSet(params DeleteStickerFromSet) (bool, error)
}
// DeleteStickerFromSetWithContext is the context-aware variant of DeleteStickerFromSet.
// Since: Bot API 3.2
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#deletestickerfromset
func (api *API) DeleteStickerFromSetWithContext(ctx context.Context, params DeleteStickerFromSet) (bool, error) {
@@ -208,6 +232,7 @@ func (api *API) DeleteStickerFromSetWithContext(ctx context.Context, params Dele
}
// ReplaceStickerInSet holds parameters for the replaceStickerInSet method.
// Since: Bot API 7.2
// See https://core.telegram.org/bots/api#replacestickerinset
type ReplaceStickerInSet struct {
UserID int64 `json:"user_id"`
@@ -217,6 +242,7 @@ type ReplaceStickerInSet struct {
}
// ReplaceStickerInSet replaces an existing sticker in a set with a new one.
// Since: Bot API 7.2
// Returns True on success.
// See https://core.telegram.org/bots/api#replacestickerinset
func (api *API) ReplaceStickerInSet(params ReplaceStickerInSet) (bool, error) {
@@ -225,6 +251,7 @@ func (api *API) ReplaceStickerInSet(params ReplaceStickerInSet) (bool, error) {
}
// ReplaceStickerInSetWithContext is the context-aware variant of ReplaceStickerInSet.
// Since: Bot API 7.2
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#replacestickerinset
func (api *API) ReplaceStickerInSetWithContext(ctx context.Context, params ReplaceStickerInSet) (bool, error) {
@@ -233,6 +260,7 @@ func (api *API) ReplaceStickerInSetWithContext(ctx context.Context, params Repla
}
// SetStickerEmojiList holds parameters for the setStickerEmojiList method.
// Since: Bot API 6.6
// See https://core.telegram.org/bots/api#setstickeremojilist
type SetStickerEmojiList struct {
Sticker string `json:"sticker"`
@@ -240,6 +268,7 @@ type SetStickerEmojiList struct {
}
// SetStickerEmojiList changes the list of emoji associated with a sticker.
// Since: Bot API 6.6
// Returns True on success.
// See https://core.telegram.org/bots/api#setstickeremojilist
func (api *API) SetStickerEmojiList(params SetStickerEmojiList) (bool, error) {
@@ -248,6 +277,7 @@ func (api *API) SetStickerEmojiList(params SetStickerEmojiList) (bool, error) {
}
// SetStickerEmojiListWithContext is the context-aware variant of SetStickerEmojiList.
// Since: Bot API 6.6
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#setstickeremojilist
func (api *API) SetStickerEmojiListWithContext(ctx context.Context, params SetStickerEmojiList) (bool, error) {
@@ -256,6 +286,7 @@ func (api *API) SetStickerEmojiListWithContext(ctx context.Context, params SetSt
}
// SetStickerKeywords holds parameters for the setStickerKeywords method.
// Since: Bot API 6.6
// See https://core.telegram.org/bots/api#setstickerkeywords
type SetStickerKeywords struct {
Sticker string `json:"sticker"`
@@ -263,6 +294,7 @@ type SetStickerKeywords struct {
}
// SetStickerKeywords changes the keywords of a sticker.
// Since: Bot API 6.6
// Returns True on success.
// See https://core.telegram.org/bots/api#setstickerkeywords
func (api *API) SetStickerKeywords(params SetStickerKeywords) (bool, error) {
@@ -271,6 +303,7 @@ func (api *API) SetStickerKeywords(params SetStickerKeywords) (bool, error) {
}
// SetStickerKeywordsWithContext is the context-aware variant of SetStickerKeywords.
// Since: Bot API 6.6
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#setstickerkeywords
func (api *API) SetStickerKeywordsWithContext(ctx context.Context, params SetStickerKeywords) (bool, error) {
@@ -279,6 +312,7 @@ func (api *API) SetStickerKeywordsWithContext(ctx context.Context, params SetSti
}
// SetStickerMaskPosition holds parameters for the setStickerMaskPosition method.
// Since: Bot API 6.6
// See https://core.telegram.org/bots/api#setstickermaskposition
type SetStickerMaskPosition struct {
Sticker string `json:"sticker"`
@@ -286,6 +320,7 @@ type SetStickerMaskPosition struct {
}
// SetStickerMaskPosition changes the mask position of a mask sticker.
// Since: Bot API 6.6
// Returns True on success.
// See https://core.telegram.org/bots/api#setstickermaskposition
func (api *API) SetStickerMaskPosition(params SetStickerMaskPosition) (bool, error) {
@@ -294,6 +329,7 @@ func (api *API) SetStickerMaskPosition(params SetStickerMaskPosition) (bool, err
}
// SetStickerMaskPositionWithContext is the context-aware variant of SetStickerMaskPosition.
// Since: Bot API 6.6
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#setstickermaskposition
func (api *API) SetStickerMaskPositionWithContext(ctx context.Context, params SetStickerMaskPosition) (bool, error) {
@@ -302,6 +338,7 @@ func (api *API) SetStickerMaskPositionWithContext(ctx context.Context, params Se
}
// SetStickerSetTitle holds parameters for the setStickerSetTitle method.
// Since: Bot API 6.6
// See https://core.telegram.org/bots/api#setstickersettitle
type SetStickerSetTitle struct {
Name string `json:"name"`
@@ -309,6 +346,7 @@ type SetStickerSetTitle struct {
}
// SetStickerSetTitle sets the title of a sticker set created by the bot.
// Since: Bot API 6.6
// Returns True on success.
// See https://core.telegram.org/bots/api#setstickersettitle
func (api *API) SetStickerSetTitle(params SetStickerSetTitle) (bool, error) {
@@ -317,6 +355,7 @@ func (api *API) SetStickerSetTitle(params SetStickerSetTitle) (bool, error) {
}
// SetStickerSetTitleWithContext is the context-aware variant of SetStickerSetTitle.
// Since: Bot API 6.6
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#setstickersettitle
func (api *API) SetStickerSetTitleWithContext(ctx context.Context, params SetStickerSetTitle) (bool, error) {
@@ -325,6 +364,7 @@ func (api *API) SetStickerSetTitleWithContext(ctx context.Context, params SetSti
}
// SetStickerSetThumbnail holds parameters for the setStickerSetThumbnail method.
// Since: Bot API 6.6
// See https://core.telegram.org/bots/api#setstickersetthumbnail
type SetStickerSetThumbnail struct {
Name string `json:"name"`
@@ -334,6 +374,7 @@ type SetStickerSetThumbnail struct {
}
// SetStickerSetThumbnail sets the thumbnail of a sticker set.
// Since: Bot API 6.6
// Returns True on success.
// See https://core.telegram.org/bots/api#setstickersetthumbnail
func (api *API) SetStickerSetThumbnail(params SetStickerSetThumbnail) (bool, error) {
@@ -342,6 +383,7 @@ func (api *API) SetStickerSetThumbnail(params SetStickerSetThumbnail) (bool, err
}
// SetStickerSetThumbnailWithContext is the context-aware variant of SetStickerSetThumbnail.
// Since: Bot API 6.6
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#setstickersetthumbnail
func (api *API) SetStickerSetThumbnailWithContext(ctx context.Context, params SetStickerSetThumbnail) (bool, error) {
@@ -350,6 +392,7 @@ func (api *API) SetStickerSetThumbnailWithContext(ctx context.Context, params Se
}
// SetCustomEmojiStickerSetThumbnail holds parameters for the setCustomEmojiStickerSetThumbnail method.
// Since: Bot API 6.6
// See https://core.telegram.org/bots/api#setcustomemojistickersetthumbnail
type SetCustomEmojiStickerSetThumbnail struct {
Name string `json:"name"`
@@ -357,6 +400,7 @@ type SetCustomEmojiStickerSetThumbnail struct {
}
// SetCustomEmojiStickerSetThumbnail sets the thumbnail of a custom emoji sticker set.
// Since: Bot API 6.6
// Returns True on success.
// See https://core.telegram.org/bots/api#setcustomemojistickersetthumbnail
func (api *API) SetCustomEmojiStickerSetThumbnail(params SetCustomEmojiStickerSetThumbnail) (bool, error) {
@@ -365,6 +409,7 @@ func (api *API) SetCustomEmojiStickerSetThumbnail(params SetCustomEmojiStickerSe
}
// SetCustomEmojiStickerSetThumbnailWithContext is the context-aware variant of SetCustomEmojiStickerSetThumbnail.
// Since: Bot API 6.6
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#setcustomemojistickersetthumbnail
func (api *API) SetCustomEmojiStickerSetThumbnailWithContext(ctx context.Context, params SetCustomEmojiStickerSetThumbnail) (bool, error) {
@@ -373,12 +418,14 @@ func (api *API) SetCustomEmojiStickerSetThumbnailWithContext(ctx context.Context
}
// DeleteStickerSet holds parameters for the deleteStickerSet method.
// Since: Bot API 6.6
// See https://core.telegram.org/bots/api#deletestickerset
type DeleteStickerSet struct {
Name string `json:"name"`
}
// DeleteStickerSet deletes a sticker set created by the bot.
// Since: Bot API 6.6
// Returns True on success.
// See https://core.telegram.org/bots/api#deletestickerset
func (api *API) DeleteStickerSet(params DeleteStickerSet) (bool, error) {
@@ -387,6 +434,7 @@ func (api *API) DeleteStickerSet(params DeleteStickerSet) (bool, error) {
}
// DeleteStickerSetWithContext is the context-aware variant of DeleteStickerSet.
// Since: Bot API 6.6
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#deletestickerset
func (api *API) DeleteStickerSetWithContext(ctx context.Context, params DeleteStickerSet) (bool, error) {
+16 -12
View File
@@ -15,6 +15,7 @@ const (
)
// MaskPosition describes the position on faces where a mask should be placed by default.
// Since: Bot API 3.2
// See https://core.telegram.org/bots/api#maskposition
type MaskPosition struct {
Point MaskPositionPoint `json:"point"`
@@ -36,26 +37,28 @@ const (
)
// Sticker represents a sticker.
// Since: Bot API 1.0
// See https://core.telegram.org/bots/api#sticker
type Sticker struct {
FileID string `json:"file_id"`
FileUniqueID string `json:"file_unique_id"`
Type StickerType `json:"type"`
Width int `json:"width"`
Height int `json:"height"`
IsAnimated bool `json:"is_animated"`
IsVideo bool `json:"is_video"`
FileID string `json:"file_id"`
FileUniqueID string `json:"file_unique_id"`
Width int `json:"width"`
Height int `json:"height"`
Thumbnail *PhotoSize `json:"thumbnail,omitempty"`
Type StickerType `json:"type"` // Since: Bot API 6.2
IsAnimated bool `json:"is_animated"` // Since: Bot API 4.4
IsVideo bool `json:"is_video"` // Since: Bot API 5.7
Thumbnail *PhotoSize `json:"thumbnail,omitempty"` // Since: Bot API 6.6
Emoji *string `json:"emoji,omitempty"`
SetName *string `json:"set_name,omitempty"`
MaskPosition *MaskPosition `json:"mask_position,omitempty"`
CustomEmojiID *string `json:"custom_emoji_id,omitempty"`
NeedRepainting *bool `json:"need_repainting,omitempty"`
SetName *string `json:"set_name,omitempty"` // Since: Bot API 3.2
MaskPosition *MaskPosition `json:"mask_position,omitempty"` // Since: Bot API 3.2
CustomEmojiID *string `json:"custom_emoji_id,omitempty"` // Since: Bot API 6.2
NeedRepainting *bool `json:"need_repainting,omitempty"` // Since: Bot API 6.6
FileSize *int64 `json:"file_size,omitempty"`
}
// StickerSet represents a sticker set.
// Since: Bot API 3.2
// See https://core.telegram.org/bots/api#stickerset
type StickerSet struct {
Name string `json:"name"`
@@ -78,6 +81,7 @@ const (
)
// InputSticker describes a sticker to be added to a sticker set.
// Since: Bot API 6.6
// See https://core.telegram.org/bots/api#inputsticker
type InputSticker struct {
Sticker string `json:"sticker"`
+89 -22
View File
@@ -58,10 +58,15 @@ const (
// UpdateTypeRemovedChatBoost is a removed chat boost update.
UpdateTypeRemovedChatBoost UpdateType = "removed_chat_boost"
// UpdateTypeManagedBot is a managed bot update.
UpdateTypeManagedBot UpdateType = "managed_bot"
// UpdateTypeGuestMessage is a guest message update.
UpdateTypeGuestMessage UpdateType = "guest_message"
)
// Update represents an incoming update from Telegram.
// Since: Bot API 1.0
// See https://core.telegram.org/bots/api#update
type Update struct {
Type UpdateType `json:"-"`
@@ -69,32 +74,33 @@ type Update struct {
UpdateID int `json:"update_id"`
Message *Message `json:"message,omitempty"`
EditedMessage *Message `json:"edited_message,omitempty"`
ChannelPost *Message `json:"channel_post,omitempty"`
EditedChannelPost *Message `json:"edited_channel_post,omitempty"`
ChannelPost *Message `json:"channel_post,omitempty"` // Since: Bot API 2.3
EditedChannelPost *Message `json:"edited_channel_post,omitempty"` // Since: Bot API 2.3
BusinessConnection *BusinessConnection `json:"business_connection,omitempty"`
BusinessMessage *Message `json:"business_message,omitempty"`
EditedBusinessMessage *Message `json:"edited_business_message,omitempty"`
DeletedBusinessMessages *BusinessMessagesDeleted `json:"deleted_business_messages,omitempty"`
MessageReaction *MessageReactionUpdated `json:"message_reaction,omitempty"`
MessageReactionCount *MessageReactionCountUpdated `json:"message_reaction_count,omitempty"`
BusinessConnection *BusinessConnection `json:"business_connection,omitempty"` // Since: Bot API 7.2
BusinessMessage *Message `json:"business_message,omitempty"` // Since: Bot API 7.2
EditedBusinessMessage *Message `json:"edited_business_message,omitempty"` // Since: Bot API 7.2
DeletedBusinessMessages *BusinessMessagesDeleted `json:"deleted_business_messages,omitempty"` // Since: Bot API 7.2
GuestMessage *Message `json:"guest_message,omitempty"` // Since: Bot API 10.0
MessageReaction *MessageReactionUpdated `json:"message_reaction,omitempty"` // Since: Bot API 7.0
MessageReactionCount *MessageReactionCountUpdated `json:"message_reaction_count,omitempty"` // Since: Bot API 7.0
InlineQuery *InlineQuery `json:"inline_query,omitempty"`
ChosenInlineResult *ChosenInlineResult `json:"chosen_inline_result,omitempty"`
CallbackQuery *CallbackQuery `json:"callback_query,omitempty"`
ShippingQuery *ShippingQuery `json:"shipping_query,omitempty"`
PreCheckoutQuery *PreCheckoutQuery `json:"pre_checkout_query,omitempty"`
PurchasedPaidMedia *PaidMediaPurchased `json:"purchased_paid_media,omitempty"`
InlineQuery *InlineQuery `json:"inline_query,omitempty"` // Since: Bot API 1.7
ChosenInlineResult *ChosenInlineResult `json:"chosen_inline_result,omitempty"` // Since: Bot API 1.8
CallbackQuery *CallbackQuery `json:"callback_query,omitempty"` // Since: Bot API 2.0
ShippingQuery *ShippingQuery `json:"shipping_query,omitempty"` // Since: Bot API 3.0
PreCheckoutQuery *PreCheckoutQuery `json:"pre_checkout_query,omitempty"` // Since: Bot API 3.0
PurchasedPaidMedia *PaidMediaPurchased `json:"purchased_paid_media,omitempty"` // Since: Bot API 7.10
Poll *Poll `json:"poll,omitempty"`
PollAnswer *PollAnswer `json:"poll_answer,omitempty"`
MyChatMember *ChatMemberUpdated `json:"my_chat_member,omitempty"`
ChatMember *ChatMemberUpdated `json:"chat_member,omitempty"`
ChatJoinRequest *ChatJoinRequest `json:"chat_join_request,omitempty"`
ChatBoost *ChatBoostUpdated `json:"chat_boost,omitempty"`
RemovedChatBoost *ChatBoostRemoved `json:"removed_chat_boost,omitempty"`
Poll *Poll `json:"poll,omitempty"` // Since: Bot API 4.2
PollAnswer *PollAnswer `json:"poll_answer,omitempty"` // Since: Bot API 4.6
MyChatMember *ChatMemberUpdated `json:"my_chat_member,omitempty"` // Since: Bot API 5.1
ChatMember *ChatMemberUpdated `json:"chat_member,omitempty"` // Since: Bot API 5.1
ChatJoinRequest *ChatJoinRequest `json:"chat_join_request,omitempty"` // Since: Bot API 5.4
ChatBoost *ChatBoostUpdated `json:"chat_boost,omitempty"` // Since: Bot API 7.0
RemovedChatBoost *ChatBoostRemoved `json:"removed_chat_boost,omitempty"` // Since: Bot API 7.0
ManagedBot *ManagedBotUpdated `json:"managed_bot,omitempty"`
ManagedBot *ManagedBotUpdated `json:"managed_bot,omitempty"` // Since: Bot API 9.6
}
// UnmarshalJSON decodes an update and derives its Type from the populated payload field.
@@ -126,6 +132,8 @@ func (u *Update) UnmarshalJSON(data []byte) error {
u.Type = UpdateTypeEditedBusinessMessage
case u.DeletedBusinessMessages != nil:
u.Type = UpdateTypeDeletedBusinessMessages
case u.GuestMessage != nil:
u.Type = UpdateTypeGuestMessage
case u.MessageReaction != nil:
u.Type = UpdateTypeMessageReaction
case u.MessageReactionCount != nil:
@@ -168,6 +176,7 @@ func (u *Update) UnmarshalJSON(data []byte) error {
}
// WebhookInfo describes the current webhook status.
// Since: Bot API 2.2
// See https://core.telegram.org/bots/api#webhookinfo
type WebhookInfo struct {
URL string `json:"url"`
@@ -181,6 +190,8 @@ type WebhookInfo struct {
AllowedUpdates []string `json:"allowed_updates,omitempty"`
}
// ProximityAlertTriggered represents the content of a service message sent when a user triggers a proximity alert.
// Since: Bot API 5.0
type ProximityAlertTriggered struct {
Traveler User `json:"traveler"`
Watcher User `json:"watcher"`
@@ -188,6 +199,7 @@ type ProximityAlertTriggered struct {
}
// InlineQuery represents an incoming inline query.
// Since: Bot API 1.7
// See https://core.telegram.org/bots/api#inlinequery
type InlineQuery struct {
ID string `json:"id"`
@@ -199,6 +211,7 @@ type InlineQuery struct {
}
// ChosenInlineResult represents a result of an inline query that was chosen by the user.
// Since: Bot API 1.8
// See https://core.telegram.org/bots/api#choseninlineresult
type ChosenInlineResult struct {
ResultID string `json:"result_id"`
@@ -209,6 +222,7 @@ type ChosenInlineResult struct {
}
// File represents a file ready to be downloaded.
// Since: Bot API 1.0
// See https://core.telegram.org/bots/api#file
type File struct {
FileID string `json:"file_id"`
@@ -218,6 +232,7 @@ type File struct {
}
// ChatMemberUpdated represents changes in the status of a chat member.
// Since: Bot API 5.1
// See https://core.telegram.org/bots/api#chatmemberupdated
type ChatMemberUpdated struct {
Chat Chat `json:"chat"`
@@ -231,6 +246,7 @@ type ChatMemberUpdated struct {
}
// ChatJoinRequest represents a join request sent to a chat.
// Since: Bot API 5.4
// See https://core.telegram.org/bots/api#chatjoinrequest
type ChatJoinRequest struct {
Chat Chat `json:"chat"`
@@ -242,6 +258,7 @@ type ChatJoinRequest struct {
}
// Location represents a point on the map.
// Since: Bot API 1.0
// See https://core.telegram.org/bots/api#location
type Location struct {
Latitude float64 `json:"latitude"`
@@ -253,6 +270,7 @@ type Location struct {
}
// LocationAddress represents a human-readable address of a location.
// Since: Bot API 8.0
type LocationAddress struct {
CountryCode string `json:"country_code"`
State *string `json:"state,omitempty"`
@@ -261,6 +279,7 @@ type LocationAddress struct {
}
// Venue represents a venue.
// Since: Bot API 2.0
// See https://core.telegram.org/bots/api#venue
type Venue struct {
Location Location `json:"location"`
@@ -273,23 +292,28 @@ type Venue struct {
}
// WebAppInfo contains information about a Web App.
// Since: Bot API 6.0
// See https://core.telegram.org/bots/api#webappinfo
type WebAppInfo struct {
URL string `json:"url"`
}
// WebAppData represents data sent from a Web App to the bot.
// Since: Bot API 6.0
type WebAppData struct {
Data string `json:"data"`
ButtonText string `json:"button_text"`
}
// StarAmount represents an amount of Telegram Stars.
// Since: Bot API 7.5
type StarAmount struct {
Amount int `json:"amount"`
NanostarAmount int `json:"nanostar_amount"`
}
// AcceptedGiftTypes represents the types of gifts accepted by a user or chat.
// Since: Bot API 9.0
type AcceptedGiftTypes struct {
UnlimitedGifts bool `json:"unlimited_gifts"`
LimitedGifts bool `json:"limited_gifts"`
@@ -299,6 +323,7 @@ type AcceptedGiftTypes struct {
}
// GiftBackground represents the background of a gift.
// Since: Bot API 9.0
type GiftBackground struct {
CenterColor int `json:"center_color"`
EdgeColor int `json:"edge_color"`
@@ -306,6 +331,7 @@ type GiftBackground struct {
}
// Gift represents a gift that can be sent.
// Since: Bot API 9.0
type Gift struct {
ID string `json:"id"`
Sticker Sticker `json:"sticker"`
@@ -323,27 +349,39 @@ type Gift struct {
}
// Gifts represents a list of gifts.
// Since: Bot API 9.0
type Gifts struct {
Gifts []Gift `json:"gifts"`
}
// UniqueGiftModel describes the model component of a unique gift.
// Since: Bot API 9.0
type UniqueGiftModel struct {
Name string `json:"name"`
Sticker Sticker `json:"sticker"`
RarityPerMille int `json:"rarity_per_mille"`
Rarity string `json:"rarity,omitempty"`
}
// UniqueGiftSymbol describes the symbol component of a unique gift.
// Since: Bot API 9.0
type UniqueGiftSymbol struct {
Name string `json:"name"`
Sticker Sticker `json:"sticker"`
RarityPerMille int `json:"rarity_per_mille"`
}
// UniqueGiftBackdropColors describes the colors of a unique gift backdrop.
// Since: Bot API 9.0
type UniqueGiftBackdropColors struct {
CenterColor int `json:"center_color"`
EdgeColor int `json:"edge_color"`
SymbolColor int `json:"symbol_color"`
TextColor int `json:"text_color"`
}
// UniqueGiftBackdrop describes the backdrop of a unique gift.
// Since: Bot API 9.0
type UniqueGiftBackdrop struct {
Name string `json:"name"`
Colors UniqueGiftBackdropColors `json:"colors"`
@@ -351,6 +389,7 @@ type UniqueGiftBackdrop struct {
}
// UniqueGiftColors represents color information for a unique gift.
// Since: Bot API 9.3
type UniqueGiftColors struct {
ModelCustomEmojiID string `json:"model_custom_emoji_id"`
SymbolCustomEmojiID string `json:"symbol_custom_emoji_id"`
@@ -360,6 +399,8 @@ type UniqueGiftColors struct {
DarkThemeOtherColors []int `json:"dark_theme_other_colors"`
}
// UniqueGift represents a unique gift.
// Since: Bot API 9.0
type UniqueGift struct {
GiftID string `json:"gift_id"`
BaseName string `json:"base_name"`
@@ -376,6 +417,8 @@ type UniqueGift struct {
PublisherChat *Chat `json:"publisher_chat,omitempty"`
}
// GiftInfo contains information about a received gift.
// Since: Bot API 9.0
type GiftInfo struct {
Gift Gift `json:"gift"`
@@ -389,6 +432,9 @@ type GiftInfo struct {
IsPrivate bool `json:"is_private,omitempty"`
UniqueGiftNumber int `json:"unique_gift_number,omitempty"`
}
// UniqueGiftInfo contains information about a received unique gift.
// Since: Bot API 9.0
type UniqueGiftInfo struct {
Gift UniqueGift `json:"gift"`
Origin string `json:"origin"`
@@ -400,6 +446,7 @@ type UniqueGiftInfo struct {
}
// OwnedGiftType represents the type of an owned gift.
// Since: Bot API 9.0
type OwnedGiftType string
const (
@@ -410,6 +457,7 @@ const (
)
// OwnedGift represents a gift owned by a user or chat.
// Since: Bot API 9.0
type OwnedGift struct {
Type OwnedGiftType `json:"type"`
OwnedGiftID string `json:"ownen_gift_id,omitempty"`
@@ -436,16 +484,21 @@ type OwnedGift struct {
}
// OwnedGifts represents a list of owned gifts with pagination.
// Since: Bot API 9.0
type OwnedGifts struct {
TotalCount int `json:"total_count"`
Gifts []OwnedGift `json:"gifts"`
NextOffset string `json:"next_offset"`
}
// GiveawayCreated represents a service message about a giveaway being created.
// Since: Bot API 7.0
type GiveawayCreated struct {
PrizeStarCount int `json:"prize_star_count,omitempty"`
}
// Giveaway represents a message about a scheduled giveaway.
// Since: Bot API 7.0
type Giveaway struct {
Chats []Chat `json:"chats"`
WinnersSelectionDate int `json:"winners_selection_date"`
@@ -459,6 +512,8 @@ type Giveaway struct {
PremiumSubscriptionMonthCount int `json:"premium_subscription_month_count,omitempty"`
}
// GiveawayWinners represents a message about the completion of a giveaway with public winners.
// Since: Bot API 7.0
type GiveawayWinners struct {
Chat Chat `json:"chat"`
GiveawayMessageID int `json:"giveaway_message_id"`
@@ -475,6 +530,8 @@ type GiveawayWinners struct {
PrizeDescription string `json:"prize_description,omitempty"`
}
// GiveawayCompleted represents a service message about the completion of a giveaway without public winners.
// Since: Bot API 7.0
type GiveawayCompleted struct {
WinnerCount int `json:"winner_count"`
UnclaimedPrizeCount int `json:"unclaimed_prize_count,omitempty"`
@@ -482,12 +539,16 @@ type GiveawayCompleted struct {
IsStarGiveaway bool `json:"is_star_giveaway,omitempty"`
}
// WriteAccessAllowed represents a service message about a user allowing a bot to write messages.
// Since: Bot API 6.4
type WriteAccessAllowed struct {
FromRequest bool `json:"from_request,omitempty"`
WebAppName string `json:"web_app_name,omitempty"`
FromAttachmentMenu bool `json:"from_attachment_menu,omitempty"`
}
// BackgroundFillType represents the type of a background fill.
// Since: Bot API 7.5
type BackgroundFillType string
const (
@@ -496,6 +557,8 @@ const (
BackgroundFillFreeformGradientType BackgroundFillType = "freeform_gradient"
)
// BackgroundFill describes the way a background is filled.
// Since: Bot API 7.5
type BackgroundFill struct {
Type BackgroundFillType `json:"type"`
@@ -508,6 +571,8 @@ type BackgroundFill struct {
Colors []int `json:"colors,omitempty"`
}
// BackgroundTypeType represents the type of a chat background.
// Since: Bot API 7.5
type BackgroundTypeType string
const (
@@ -517,6 +582,8 @@ const (
BackgroundTypeChatThemeType BackgroundTypeType = "chat_theme"
)
// BackgroundType describes the type of a background.
// Since: Bot API 7.5
type BackgroundType struct {
Type BackgroundTypeType `json:"type"`
+14 -9
View File
@@ -33,6 +33,8 @@ const (
UploaderStickerType UploaderFileType = "sticker"
// UploaderCertificateType is the multipart field name for webhook certificate uploads.
UploaderCertificateType UploaderFileType = "certificate"
// UploaderLivePhotoType is the multipart field name for live photo uploads.
UploaderLivePhotoType UploaderFileType = "live_photo"
)
// UploaderFileType represents the Telegram form field name for a file upload.
@@ -77,6 +79,7 @@ func NewUploader(api *API) *Uploader {
"UPLOADER", utils.GetLoggerLevel(),
api.logFormat, api.logFormatter,
)
logger.AddReplacer(api.token, "<TOKEN>")
return &Uploader{api, logger}
}
@@ -141,7 +144,7 @@ func (r UploaderRequest[R, P]) doRequest(ctx context.Context, up *Uploader) (R,
req.Header.Set("User-Agent", fmt.Sprintf("Laniakea/%s", utils.VersionString))
req.ContentLength = int64(buf.Len())
up.logger.Debugln("UPLOADER REQ", r.method)
up.logger.Debugln("UPLOADER REQ", url)
resp, err := up.api.client.Do(req)
if err != nil {
return zero, err
@@ -152,7 +155,7 @@ func (r UploaderRequest[R, P]) doRequest(ctx context.Context, up *Uploader) (R,
if err != nil {
return zero, err
}
up.logger.Debugln("UPLOADER RES", r.method, string(body))
up.logger.Debugln("UPLOADER RES", url, string(body))
response, err := parseBody[R](body)
if err != nil {
@@ -175,10 +178,14 @@ func (r UploaderRequest[R, P]) doRequest(ctx context.Context, up *Uploader) (R,
case <-ctx.Done():
return zero, ctx.Err()
case <-time.After(time.Duration(after) * time.Second):
continue // Повторяем запрос
continue
}
}
return zero, fmt.Errorf("[%d] %s", response.ErrorCode, response.Description)
return zero, &ResponseError{
Code: response.ErrorCode,
Description: response.Description,
Parameters: response.Parameters,
}
}
return response.Result, nil
}
@@ -216,7 +223,6 @@ func (r UploaderRequest[R, P]) Do(up *Uploader) (R, error) {
return r.DoWithContext(context.Background(), up)
}
// Internal helper that builds a finalized multipart body from files and params.
func prepareMultipart[P any](files []UploaderFile, params P) (*bytes.Buffer, string, error) {
buf := bytes.NewBuffer(nil)
w := multipart.NewWriter(buf)
@@ -224,7 +230,7 @@ func prepareMultipart[P any](files []UploaderFile, params P) (*bytes.Buffer, str
for _, file := range files {
fw, err := w.CreateFormFile(string(file.field), file.filename)
if err != nil {
_ = w.Close() // Закрываем, чтобы не было утечки
_ = w.Close()
return nil, "", err
}
@@ -235,13 +241,13 @@ func prepareMultipart[P any](files []UploaderFile, params P) (*bytes.Buffer, str
}
}
err := utils.Encode(w, params) // Предполагается, что это записывает в w
err := utils.Encode(w, params)
if err != nil {
_ = w.Close()
return nil, "", err
}
err = w.Close() // ✅ ОБЯЗАТЕЛЬНО вызвать в конце — иначе запрос битый!
err = w.Close()
if err != nil {
return nil, "", err
}
@@ -249,7 +255,6 @@ func prepareMultipart[P any](files []UploaderFile, params P) (*bytes.Buffer, str
return buf, w.FormDataContentType(), nil
}
// Internal helper that infers an upload field name from a file extension.
func uploaderTypeByExt(filename string) UploaderFileType {
ext := strings.ToLower(filepath.Ext(filename))
switch ext {
+52
View File
@@ -1,6 +1,7 @@
package tgapi
import (
"errors"
"fmt"
"io"
"mime"
@@ -104,6 +105,57 @@ func TestUploaderEncodesJSONFieldsAndLeavesAcceptEncodingToHTTPTransport(t *test
}
}
func TestUploaderSurfacesResponseErrorForTelegramFailure(t *testing.T) {
const responseBody = `{"ok":false,"error_code":400,"description":"Bad Request: chat not found"}`
client := &http.Client{
Transport: roundTripFunc(func(_ *http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: http.StatusOK,
Header: http.Header{"Content-Type": []string{"application/json"}},
Body: io.NopCloser(strings.NewReader(responseBody)),
}, nil
}),
}
api := NewAPI(
NewAPIOpts("token").
SetAPIURL("https://example.test").
SetHTTPClient(client),
)
defer func() {
if err := api.Close(); err != nil {
t.Fatalf("Close returned error: %v", err)
}
}()
uploader := NewUploader(api)
defer func() {
if err := uploader.Close(); err != nil {
t.Fatalf("Close returned error: %v", err)
}
}()
_, err := uploader.SendPhoto(
UploadPhoto{ChatID: 42},
NewUploaderFile("photo.jpg", []byte("img")),
)
if err == nil {
t.Fatal("expected error, got nil")
}
var respErr *ResponseError
if !errors.As(err, &respErr) {
t.Fatalf("expected *ResponseError, got %T: %v", err, err)
}
if respErr.Code != 400 {
t.Fatalf("unexpected ResponseError.Code: got %d want 400", respErr.Code)
}
if !strings.Contains(respErr.Description, "chat not found") {
t.Fatalf("unexpected ResponseError.Description: %q", respErr.Description)
}
}
func TestNewUploaderFileDetectsFileTypeCaseInsensitively(t *testing.T) {
tests := []struct {
name string
+71 -17
View File
@@ -3,6 +3,7 @@ package tgapi
import "context"
// UploadPhoto holds parameters for uploading a photo using the Uploader.
// Since: Bot API 1.0
// See https://core.telegram.org/bots/api#sendphoto
type UploadPhoto struct {
BusinessConnectionID string `json:"business_connection_id,omitempty"`
@@ -27,6 +28,7 @@ type UploadPhoto struct {
}
// SendPhoto uploads a photo via multipart and sends it as a message.
// Since: Bot API 1.0
// file is the photo file to upload.
// See https://core.telegram.org/bots/api#sendphoto
func (u *Uploader) SendPhoto(params UploadPhoto, file UploaderFile) (Message, error) {
@@ -35,8 +37,7 @@ func (u *Uploader) SendPhoto(params UploadPhoto, file UploaderFile) (Message, er
}
// SendPhotoWithContext is the context-aware variant of SendPhoto.
// It executes the same request but uses ctx for cancellation and deadlines.
// SendPhotoWithContext is the context-aware variant of SendPhoto.
// Since: Bot API 1.0
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#sendphoto
func (u *Uploader) SendPhotoWithContext(ctx context.Context, params UploadPhoto, file UploaderFile) (Message, error) {
@@ -45,6 +46,7 @@ func (u *Uploader) SendPhotoWithContext(ctx context.Context, params UploadPhoto,
}
// UploadAudio holds parameters for uploading an audio file using the Uploader.
// Since: Bot API 1.2
// See https://core.telegram.org/bots/api#sendaudio
type UploadAudio struct {
BusinessConnectionID string `json:"business_connection_id,omitempty"`
@@ -71,6 +73,7 @@ type UploadAudio struct {
}
// SendAudio uploads an audio file via multipart and sends it as a message.
// Since: Bot API 1.2
// files are the audio file(s) to upload (typically one file).
// See https://core.telegram.org/bots/api#sendaudio
func (u *Uploader) SendAudio(params UploadAudio, files ...UploaderFile) (Message, error) {
@@ -79,8 +82,7 @@ func (u *Uploader) SendAudio(params UploadAudio, files ...UploaderFile) (Message
}
// SendAudioWithContext is the context-aware variant of SendAudio.
// It executes the same request but uses ctx for cancellation and deadlines.
// SendAudioWithContext is the context-aware variant of SendAudio.
// Since: Bot API 1.2
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#sendaudio
func (u *Uploader) SendAudioWithContext(ctx context.Context, params UploadAudio, files ...UploaderFile) (Message, error) {
@@ -89,6 +91,7 @@ func (u *Uploader) SendAudioWithContext(ctx context.Context, params UploadAudio,
}
// UploadDocument holds parameters for uploading a document using the Uploader.
// Since: Bot API 1.0
// See https://core.telegram.org/bots/api#senddocument
type UploadDocument struct {
BusinessConnectionID string `json:"business_connection_id,omitempty"`
@@ -112,6 +115,7 @@ type UploadDocument struct {
}
// SendDocument uploads a document via multipart and sends it as a message.
// Since: Bot API 1.0
// files are the document file(s) to upload (typically one file).
// See https://core.telegram.org/bots/api#senddocument
func (u *Uploader) SendDocument(params UploadDocument, files ...UploaderFile) (Message, error) {
@@ -120,8 +124,7 @@ func (u *Uploader) SendDocument(params UploadDocument, files ...UploaderFile) (M
}
// SendDocumentWithContext is the context-aware variant of SendDocument.
// It executes the same request but uses ctx for cancellation and deadlines.
// SendDocumentWithContext is the context-aware variant of SendDocument.
// Since: Bot API 1.0
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#senddocument
func (u *Uploader) SendDocumentWithContext(ctx context.Context, params UploadDocument, files ...UploaderFile) (Message, error) {
@@ -130,6 +133,7 @@ func (u *Uploader) SendDocumentWithContext(ctx context.Context, params UploadDoc
}
// UploadVideo holds parameters for uploading a video using the Uploader.
// Since: Bot API 1.0
// See https://core.telegram.org/bots/api#sendvideo
type UploadVideo struct {
BusinessConnectionID string `json:"business_connection_id,omitempty"`
@@ -160,6 +164,7 @@ type UploadVideo struct {
}
// SendVideo uploads a video via multipart and sends it as a message.
// Since: Bot API 1.0
// files are the video file(s) to upload (typically one file).
// See https://core.telegram.org/bots/api#sendvideo
func (u *Uploader) SendVideo(params UploadVideo, files ...UploaderFile) (Message, error) {
@@ -168,8 +173,7 @@ func (u *Uploader) SendVideo(params UploadVideo, files ...UploaderFile) (Message
}
// SendVideoWithContext is the context-aware variant of SendVideo.
// It executes the same request but uses ctx for cancellation and deadlines.
// SendVideoWithContext is the context-aware variant of SendVideo.
// Since: Bot API 1.0
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#sendvideo
func (u *Uploader) SendVideoWithContext(ctx context.Context, params UploadVideo, files ...UploaderFile) (Message, error) {
@@ -178,6 +182,7 @@ func (u *Uploader) SendVideoWithContext(ctx context.Context, params UploadVideo,
}
// UploadAnimation holds parameters for uploading an animation using the Uploader.
// Since: Bot API 4.0
// See https://core.telegram.org/bots/api#sendanimation
type UploadAnimation struct {
BusinessConnectionID string `json:"business_connection_id,omitempty"`
@@ -206,6 +211,7 @@ type UploadAnimation struct {
}
// SendAnimation uploads an animation via multipart and sends it as a message.
// Since: Bot API 4.0
// files are the animation file(s) to upload (typically one file).
// See https://core.telegram.org/bots/api#sendanimation
func (u *Uploader) SendAnimation(params UploadAnimation, files ...UploaderFile) (Message, error) {
@@ -214,8 +220,7 @@ func (u *Uploader) SendAnimation(params UploadAnimation, files ...UploaderFile)
}
// SendAnimationWithContext is the context-aware variant of SendAnimation.
// It executes the same request but uses ctx for cancellation and deadlines.
// SendAnimationWithContext is the context-aware variant of SendAnimation.
// Since: Bot API 4.0
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#sendanimation
func (u *Uploader) SendAnimationWithContext(ctx context.Context, params UploadAnimation, files ...UploaderFile) (Message, error) {
@@ -224,6 +229,7 @@ func (u *Uploader) SendAnimationWithContext(ctx context.Context, params UploadAn
}
// UploadVoice holds parameters for uploading a voice note using the Uploader.
// Since: Bot API 1.2
// See https://core.telegram.org/bots/api#sendvoice
type UploadVoice struct {
BusinessConnectionID string `json:"business_connection_id,omitempty"`
@@ -247,6 +253,7 @@ type UploadVoice struct {
}
// SendVoice uploads a voice note via multipart and sends it as a message.
// Since: Bot API 1.2
// files are the voice file(s) to upload (typically one file).
// See https://core.telegram.org/bots/api#sendvoice
func (u *Uploader) SendVoice(params UploadVoice, files ...UploaderFile) (Message, error) {
@@ -255,8 +262,7 @@ func (u *Uploader) SendVoice(params UploadVoice, files ...UploaderFile) (Message
}
// SendVoiceWithContext is the context-aware variant of SendVoice.
// It executes the same request but uses ctx for cancellation and deadlines.
// SendVoiceWithContext is the context-aware variant of SendVoice.
// Since: Bot API 1.2
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#sendvoice
func (u *Uploader) SendVoiceWithContext(ctx context.Context, params UploadVoice, files ...UploaderFile) (Message, error) {
@@ -265,6 +271,7 @@ func (u *Uploader) SendVoiceWithContext(ctx context.Context, params UploadVoice,
}
// UploadVideoNote holds parameters for uploading a video note (rounded video) using the Uploader.
// Since: Bot API 3.0
// See https://core.telegram.org/bots/api#sendvideonote
type UploadVideoNote struct {
BusinessConnectionID string `json:"business_connection_id,omitempty"`
@@ -286,6 +293,7 @@ type UploadVideoNote struct {
}
// SendVideoNote uploads a video note via multipart and sends it as a message.
// Since: Bot API 3.0
// files are the video note file(s) to upload (typically one file).
// See https://core.telegram.org/bots/api#sendvideonote
func (u *Uploader) SendVideoNote(params UploadVideoNote, files ...UploaderFile) (Message, error) {
@@ -294,8 +302,7 @@ func (u *Uploader) SendVideoNote(params UploadVideoNote, files ...UploaderFile)
}
// SendVideoNoteWithContext is the context-aware variant of SendVideoNote.
// It executes the same request but uses ctx for cancellation and deadlines.
// SendVideoNoteWithContext is the context-aware variant of SendVideoNote.
// Since: Bot API 3.0
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#sendvideonote
func (u *Uploader) SendVideoNoteWithContext(ctx context.Context, params UploadVideoNote, files ...UploaderFile) (Message, error) {
@@ -304,12 +311,14 @@ func (u *Uploader) SendVideoNoteWithContext(ctx context.Context, params UploadVi
}
// UploadChatPhoto holds parameters for uploading a chat photo using the Uploader.
// Since: Bot API 3.1
// See https://core.telegram.org/bots/api#setchatphoto
type UploadChatPhoto struct {
ChatID int64 `json:"chat_id"`
}
// SetChatPhoto uploads a new chat photo.
// Since: Bot API 3.1
// photo is the photo file to upload.
// See https://core.telegram.org/bots/api#setchatphoto
func (u *Uploader) SetChatPhoto(params UploadChatPhoto, photo UploaderFile) (bool, error) {
@@ -318,8 +327,7 @@ func (u *Uploader) SetChatPhoto(params UploadChatPhoto, photo UploaderFile) (boo
}
// SetChatPhotoWithContext is the context-aware variant of SetChatPhoto.
// It executes the same request but uses ctx for cancellation and deadlines.
// SetChatPhotoWithContext is the context-aware variant of SetChatPhoto.
// Since: Bot API 3.1
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#setchatphoto
func (u *Uploader) SetChatPhotoWithContext(ctx context.Context, params UploadChatPhoto, photo UploaderFile) (bool, error) {
@@ -328,6 +336,7 @@ func (u *Uploader) SetChatPhotoWithContext(ctx context.Context, params UploadCha
}
// UploadSetWebhook holds multipart parameters for the setWebhook method.
// Since: Bot API 1.0
// Use this type when uploading a self-signed certificate file.
// See https://core.telegram.org/bots/api#setwebhook
type UploadSetWebhook struct {
@@ -340,7 +349,8 @@ type UploadSetWebhook struct {
}
// SetWebhook uploads a certificate and sets a webhook URL.
// certificate maps to the multipart field \"certificate\".
// Since: Bot API 1.0
// certificate maps to the multipart field "certificate".
// See https://core.telegram.org/bots/api#setwebhook
func (u *Uploader) SetWebhook(params UploadSetWebhook, certificate UploaderFile) (bool, error) {
req := NewUploaderRequest[bool]("setWebhook", params, certificate.SetType(UploaderCertificateType))
@@ -348,9 +358,53 @@ func (u *Uploader) SetWebhook(params UploadSetWebhook, certificate UploaderFile)
}
// SetWebhookWithContext is the context-aware variant of SetWebhook.
// Since: Bot API 1.0
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#setwebhook
func (u *Uploader) SetWebhookWithContext(ctx context.Context, params UploadSetWebhook, certificate UploaderFile) (bool, error) {
req := NewUploaderRequest[bool]("setWebhook", params, certificate.SetType(UploaderCertificateType))
return req.DoWithContext(ctx, u)
}
// UploadLivePhoto holds parameters for uploading a live photo using the Uploader.
// Since: Bot API 10.0
// See https://core.telegram.org/bots/api#sendlivephoto
type UploadLivePhoto struct {
BusinessConnectionID string `json:"business_connection_id,omitempty"`
ChatID int64 `json:"chat_id"`
MessageThreadID int `json:"message_thread_id,omitempty"`
DirectMessagesTopicID int `json:"direct_messages_topic_id,omitempty"`
Caption string `json:"caption,omitempty"`
ParseMode ParseMode `json:"parse_mode,omitempty"`
CaptionEntities []MessageEntity `json:"caption_entities,omitempty"`
ShowCaptionAboveMedia bool `json:"show_caption_above_media,omitempty"`
HasSpoiler bool `json:"has_spoiler,omitempty"`
DisableNotification bool `json:"disable_notification,omitempty"`
ProtectContent bool `json:"protect_content,omitempty"`
AllowPaidBroadcast bool `json:"allow_paid_broadcast,omitempty"`
MessageEffectID string `json:"message_effect_id,omitempty"`
SuggestedPostParameters *SuggestedPostParameters `json:"suggested_post_parameters,omitempty"`
ReplyParameters *ReplyParameters `json:"reply_parameters,omitempty"`
ReplyMarkup *ReplyMarkup `json:"reply_markup,omitempty"`
}
// SendLivePhoto uploads a live photo via multipart and sends it as a message.
// Since: Bot API 10.0
// file is the live photo file to upload.
// See https://core.telegram.org/bots/api#sendlivephoto
func (u *Uploader) SendLivePhoto(params UploadLivePhoto, file UploaderFile) (Message, error) {
req := NewUploaderRequestWithChatID[Message]("sendLivePhoto", params, params.ChatID, file.SetType(UploaderLivePhotoType))
return req.Do(u)
}
// SendLivePhotoWithContext is the context-aware variant of SendLivePhoto.
// Since: Bot API 10.0
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#sendlivephoto
func (u *Uploader) SendLivePhotoWithContext(ctx context.Context, params UploadLivePhoto, file UploaderFile) (Message, error) {
req := NewUploaderRequestWithChatID[Message]("sendLivePhoto", params, params.ChatID, file.SetType(UploaderLivePhotoType))
return req.DoWithContext(ctx, u)
}
+38
View File
@@ -3,6 +3,7 @@ package tgapi
import "context"
// GetUserProfilePhotos holds parameters for the GetUserProfilePhotos method.
// Since: Bot API 1.4
// See https://core.telegram.org/bots/api#getuserprofilephotos
type GetUserProfilePhotos struct {
UserID int64 `json:"user_id"`
@@ -11,6 +12,7 @@ type GetUserProfilePhotos struct {
}
// GetUserProfilePhotos returns a list of profile pictures for a user.
// Since: Bot API 1.4
// See https://core.telegram.org/bots/api#getuserprofilephotos
func (api *API) GetUserProfilePhotos(params GetUserProfilePhotos) (UserProfilePhotos, error) {
req := NewRequest[UserProfilePhotos]("getUserProfilePhotos", params)
@@ -18,6 +20,7 @@ func (api *API) GetUserProfilePhotos(params GetUserProfilePhotos) (UserProfilePh
}
// GetUserProfilePhotosWithContext is the context-aware variant of GetUserProfilePhotos.
// Since: Bot API 1.4
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#getuserprofilephotos
func (api *API) GetUserProfilePhotosWithContext(ctx context.Context, params GetUserProfilePhotos) (UserProfilePhotos, error) {
@@ -26,6 +29,7 @@ func (api *API) GetUserProfilePhotosWithContext(ctx context.Context, params GetU
}
// GetUserProfileAudios holds parameters for the GetUserProfileAudios method.
// Since: Bot API 9.3
// See https://core.telegram.org/bots/api#getuserprofileaudios
type GetUserProfileAudios struct {
UserID int64 `json:"user_id"`
@@ -34,6 +38,7 @@ type GetUserProfileAudios struct {
}
// GetUserProfileAudios returns a list of profile audios for a user.
// Since: Bot API 9.3
// See https://core.telegram.org/bots/api#getuserprofileaudios
func (api *API) GetUserProfileAudios(params GetUserProfileAudios) (UserProfileAudios, error) {
req := NewRequest[UserProfileAudios]("getUserProfileAudios", params)
@@ -41,6 +46,7 @@ func (api *API) GetUserProfileAudios(params GetUserProfileAudios) (UserProfileAu
}
// GetUserProfileAudiosWithContext is the context-aware variant of GetUserProfileAudios.
// Since: Bot API 9.3
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#getuserprofileaudios
func (api *API) GetUserProfileAudiosWithContext(ctx context.Context, params GetUserProfileAudios) (UserProfileAudios, error) {
@@ -49,6 +55,7 @@ func (api *API) GetUserProfileAudiosWithContext(ctx context.Context, params GetU
}
// SetUserEmojiStatus holds parameters for the SetUserEmojiStatus method.
// Since: Bot API 8.0
// See https://core.telegram.org/bots/api#setuseremojistatus
type SetUserEmojiStatus struct {
UserID int64 `json:"user_id"`
@@ -57,6 +64,7 @@ type SetUserEmojiStatus struct {
}
// SetUserEmojiStatus sets a custom emoji status for a user.
// Since: Bot API 8.0
// Returns true on success.
// See https://core.telegram.org/bots/api#setuseremojistatus
func (api *API) SetUserEmojiStatus(params SetUserEmojiStatus) (bool, error) {
@@ -65,6 +73,7 @@ func (api *API) SetUserEmojiStatus(params SetUserEmojiStatus) (bool, error) {
}
// SetUserEmojiStatusWithContext is the context-aware variant of SetUserEmojiStatus.
// Since: Bot API 8.0
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#setuseremojistatus
func (api *API) SetUserEmojiStatusWithContext(ctx context.Context, params SetUserEmojiStatus) (bool, error) {
@@ -73,6 +82,7 @@ func (api *API) SetUserEmojiStatusWithContext(ctx context.Context, params SetUse
}
// GetUserGifts holds parameters for the GetUserGifts method.
// Since: Bot API 9.3
// See https://core.telegram.org/bots/api#getusergifts
type GetUserGifts struct {
UserID int64 `json:"user_id"`
@@ -87,6 +97,7 @@ type GetUserGifts struct {
}
// GetUserGifts returns gifts owned by a user.
// Since: Bot API 9.3
// See https://core.telegram.org/bots/api#getusergifts
func (api *API) GetUserGifts(params GetUserGifts) (OwnedGifts, error) {
req := NewRequest[OwnedGifts]("getUserGifts", params)
@@ -94,9 +105,36 @@ func (api *API) GetUserGifts(params GetUserGifts) (OwnedGifts, error) {
}
// GetUserGiftsWithContext is the context-aware variant of GetUserGifts.
// Since: Bot API 9.3
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#getusergifts
func (api *API) GetUserGiftsWithContext(ctx context.Context, params GetUserGifts) (OwnedGifts, error) {
req := NewRequest[OwnedGifts]("getUserGifts", params)
return req.DoWithContext(ctx, api)
}
// GetUserPersonalChatMessages holds parameters for the getUserPersonalChatMessages method.
// Since: Bot API 10.0
// See https://core.telegram.org/bots/api#getuserpersonalchatmessages
type GetUserPersonalChatMessages struct {
UserID int64 `json:"user_id"`
Offset int `json:"offset,omitempty"`
Limit int `json:"limit,omitempty"`
}
// GetUserPersonalChatMessages returns messages from the personal chat of the user with the bot.
// Since: Bot API 10.0
// See https://core.telegram.org/bots/api#getuserpersonalchatmessages
func (api *API) GetUserPersonalChatMessages(params GetUserPersonalChatMessages) ([]Message, error) {
req := NewRequest[[]Message]("getUserPersonalChatMessages", params)
return req.Do(api)
}
// GetUserPersonalChatMessagesWithContext is the context-aware variant of GetUserPersonalChatMessages.
// Since: Bot API 10.0
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#getuserpersonalchatmessages
func (api *API) GetUserPersonalChatMessagesWithContext(ctx context.Context, params GetUserPersonalChatMessages) ([]Message, error) {
req := NewRequest[[]Message]("getUserPersonalChatMessages", params)
return req.DoWithContext(ctx, api)
}
+23 -16
View File
@@ -1,27 +1,31 @@
package tgapi
// User represents a Telegram user or bot.
// Since: Bot API 1.0
// See https://core.telegram.org/bots/api#user
type User struct {
ID int64 `json:"id"`
IsBot bool `json:"is_bot"`
FirstName string `json:"first_name"`
LastName *string `json:"last_name,omitempty"`
Username *string `json:"username,omitempty"`
LanguageCode *string `json:"language_code,omitempty"`
IsPremium *bool `json:"is_premium,omitempty"`
AddedToAttachmentMenu *bool `json:"added_to_attachment_menu,omitempty"`
CanJoinGroups *bool `json:"can_join_groups,omitempty"`
CanReadAllGroupMessages *bool `json:"can_read_all_group_messages,omitempty"`
CanManageBots *bool `json:"can_manage_bots,omitempty"`
SupportsInlineQueries *bool `json:"supports_inline_queries,omitempty"`
CanConnectToBusiness *bool `json:"can_connect_to_business,omitempty"`
HasMainWebApp *bool `json:"has_main_web_app,omitempty"`
HasTopicsEnabled *bool `json:"has_topics_enabled,omitempty"`
AllowsUsersToCreateTopics *bool `json:"allows_users_to_create_topics,omitempty"`
ID int64 `json:"id"`
FirstName string `json:"first_name"`
LastName *string `json:"last_name,omitempty"`
Username *string `json:"username,omitempty"`
IsBot bool `json:"is_bot"` // Since: Bot API 3.3
LanguageCode *string `json:"language_code,omitempty"` // Since: Bot API 3.0
IsPremium *bool `json:"is_premium,omitempty"` // Since: Bot API 6.1
AddedToAttachmentMenu *bool `json:"added_to_attachment_menu,omitempty"` // Since: Bot API 6.1
CanJoinGroups *bool `json:"can_join_groups,omitempty"` // Since: Bot API 4.6
CanReadAllGroupMessages *bool `json:"can_read_all_group_messages,omitempty"` // Since: Bot API 4.6
SupportsInlineQueries *bool `json:"supports_inline_queries,omitempty"` // Since: Bot API 4.6
CanConnectToBusiness *bool `json:"can_connect_to_business,omitempty"` // Since: Bot API 7.2
HasMainWebApp *bool `json:"has_main_web_app,omitempty"` // Since: Bot API 7.8
HasTopicsEnabled *bool `json:"has_topics_enabled,omitempty"` // Since: Bot API 9.3
AllowsUsersToCreateTopics *bool `json:"allows_users_to_create_topics,omitempty"` // Since: Bot API 9.4
CanManageBots *bool `json:"can_manage_bots,omitempty"` // Since: Bot API 9.6
SupportsGuestQueries *bool `json:"supports_guest_queries,omitempty"` // Since: Bot API 10.0
}
// UserProfilePhotos represents a user's profile photos.
// Since: Bot API 1.4
// See https://core.telegram.org/bots/api#userprofilephotos
type UserProfilePhotos struct {
TotalCount int `json:"total_count"`
@@ -29,6 +33,7 @@ type UserProfilePhotos struct {
}
// UserProfileAudios represents a user's profile audios.
// Since: Bot API 9.3
// See https://core.telegram.org/bots/api#userprofileaudios
type UserProfileAudios struct {
TotalCount int `json:"total_count"`
@@ -36,6 +41,7 @@ type UserProfileAudios struct {
}
// UserRating represents a user's rating with level progression.
// Since: Bot API 9.3
// See https://core.telegram.org/bots/api#userrating
type UserRating struct {
Level int `json:"level"`
@@ -45,6 +51,7 @@ type UserRating struct {
}
// Birthdate represents a user's birthdate.
// Since: Bot API 7.2
// See https://core.telegram.org/bots/api#birthdate
type Birthdate struct {
Day int `json:"day"`
+2
View File
@@ -0,0 +1,2 @@
// Package tgfmt provides small helpers for Telegram text formatting.
package tgfmt
+99
View File
@@ -0,0 +1,99 @@
package tgfmt
import (
"strconv"
"strings"
)
// HTML is an escaped Telegram HTML fragment.
//
// Methods on HTML compose formatting without escaping the fragment again.
type HTML string
// EscapeHTML escapes special characters for Telegram HTML parse mode.
func EscapeHTML(s string) HTML {
s = strings.ReplaceAll(s, "&", "&amp;")
s = strings.ReplaceAll(s, "<", "&lt;")
s = strings.ReplaceAll(s, ">", "&gt;")
s = strings.ReplaceAll(s, `"`, "&quot;")
return HTML(s)
}
// Bold returns h wrapped as bold Telegram HTML text.
func (h HTML) Bold() HTML {
return "<b>" + h + "</b>"
}
// Italic returns h wrapped as italic Telegram HTML text.
func (h HTML) Italic() HTML {
return "<i>" + h + "</i>"
}
// Underline returns h wrapped as underlined Telegram HTML text.
func (h HTML) Underline() HTML {
return "<u>" + h + "</u>"
}
// Strikethrough returns h wrapped as strikethrough Telegram HTML text.
func (h HTML) Strikethrough() HTML {
return "<s>" + h + "</s>"
}
// Spoiler returns h wrapped as spoiler Telegram HTML text.
func (h HTML) Spoiler() HTML {
return "<tg-spoiler>" + h + "</tg-spoiler>"
}
// Link returns h as a Telegram HTML text link.
func (h HTML) Link(url string) HTML {
return `<a href="` + escapeHTMLAttr(url) + `">` + h + "</a>"
}
// Mention returns h as a Telegram HTML user mention.
func (h HTML) Mention(userID int64) HTML {
return `<a href="tg://user?id=` + HTML(strconv.FormatInt(userID, 10)) + `">` + h + "</a>"
}
// Emoji returns h as a Telegram HTML custom emoji.
func (h HTML) Emoji(emojiID string) HTML {
return `<tg-emoji emoji-id="` + escapeHTMLAttr(emojiID) + `">` + h + "</tg-emoji>"
}
// Time returns h as a Telegram HTML localized timestamp.
func (h HTML) Time(unix int64) HTML {
return `<tg-time unix="` + HTML(strconv.FormatInt(unix, 10)) + `">` + h + "</tg-time>"
}
// TimeFormat returns h as a Telegram HTML localized timestamp with format.
func (h HTML) TimeFormat(unix int64, format string) HTML {
return `<tg-time unix="` + HTML(strconv.FormatInt(unix, 10)) + `" format="` + escapeHTMLAttr(format) + `">` + h + "</tg-time>"
}
// InlineCode returns h wrapped as inline code Telegram HTML text.
func (h HTML) InlineCode() HTML {
return "<code>" + h + "</code>"
}
// BlockCode returns h wrapped as a Telegram HTML code block.
func (h HTML) BlockCode() HTML {
return "<pre>" + h + "</pre>"
}
// BlockCodeLanguage returns h wrapped as a Telegram HTML code block with language.
func (h HTML) BlockCodeLanguage(lang string) HTML {
return `<pre><code class="language-` + escapeHTMLAttr(lang) + `">` + h + "</code></pre>"
}
// Quote returns h as a Telegram HTML blockquote.
func (h HTML) Quote() HTML {
return "<blockquote>" + h + "</blockquote>"
}
// QuoteExpandable returns h as a Telegram HTML expandable blockquote.
func (h HTML) QuoteExpandable() HTML {
return "<blockquote expandable>" + h + "</blockquote>"
}
func escapeHTMLAttr(s string) HTML {
return EscapeHTML(s)
}
+78
View File
@@ -0,0 +1,78 @@
package tgfmt
import "testing"
func TestEscapeHTML(t *testing.T) {
got := EscapeHTML(`<tag attr="a&b">`)
want := HTML(`&lt;tag attr=&quot;a&amp;b&quot;&gt;`)
if got != want {
t.Fatalf("EscapeHTML() = %q, want %q", got, want)
}
}
func TestHTMLComposesWithoutDoubleEscaping(t *testing.T) {
got := EscapeHTML("<b>").Bold().Italic()
want := HTML("<i><b>&lt;b&gt;</b></i>")
if got != want {
t.Fatalf("formatted HTML = %q, want %q", got, want)
}
}
func TestHTMLFormattingMethods(t *testing.T) {
tests := []struct {
name string
got HTML
want HTML
}{
{name: "bold", got: EscapeHTML("text").Bold(), want: "<b>text</b>"},
{name: "italic", got: EscapeHTML("text").Italic(), want: "<i>text</i>"},
{name: "underline", got: EscapeHTML("text").Underline(), want: "<u>text</u>"},
{name: "strikethrough", got: EscapeHTML("text").Strikethrough(), want: "<s>text</s>"},
{name: "spoiler", got: EscapeHTML("text").Spoiler(), want: "<tg-spoiler>text</tg-spoiler>"},
{name: "inline code", got: EscapeHTML("text").InlineCode(), want: "<code>text</code>"},
{name: "block code", got: EscapeHTML("text").BlockCode(), want: "<pre>text</pre>"},
{name: "block code language", got: EscapeHTML("text").BlockCodeLanguage(`go"`), want: `<pre><code class="language-go&quot;">text</code></pre>`},
{name: "quote", got: EscapeHTML("text").Quote(), want: "<blockquote>text</blockquote>"},
{name: "expandable quote", got: EscapeHTML("text").QuoteExpandable(), want: "<blockquote expandable>text</blockquote>"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.got != tt.want {
t.Fatalf("formatted HTML = %q, want %q", tt.got, tt.want)
}
})
}
}
func TestHTMLLinkEscapesAttributes(t *testing.T) {
got := EscapeHTML("Laniakea").Link(`https://example.test/?q="a&b"`)
want := HTML(`<a href="https://example.test/?q=&quot;a&amp;b&quot;">Laniakea</a>`)
if got != want {
t.Fatalf("Link() = %q, want %q", got, want)
}
}
func TestHTMLSpecialLinks(t *testing.T) {
tests := []struct {
name string
got HTML
want HTML
}{
{name: "mention", got: EscapeHTML("User").Mention(123), want: `<a href="tg://user?id=123">User</a>`},
{name: "emoji", got: EscapeHTML("emoji").Emoji(`12"3`), want: `<tg-emoji emoji-id="12&quot;3">emoji</tg-emoji>`},
{name: "time", got: EscapeHTML("date").Time(1772323200), want: `<tg-time unix="1772323200">date</tg-time>`},
{name: "time format", got: EscapeHTML("date").TimeFormat(1772323200, `MMM " yyyy`), want: `<tg-time unix="1772323200" format="MMM &quot; yyyy">date</tg-time>`},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.got != tt.want {
t.Fatalf("formatted HTML link = %q, want %q", tt.got, tt.want)
}
})
}
}
+56
View File
@@ -0,0 +1,56 @@
package tgfmt
import (
"strconv"
"strings"
)
// Markdown is an escaped legacy Telegram Markdown fragment.
//
// Deprecated: Use MarkdownV2 instead.
type Markdown string
// EscapeMarkdown escapes special characters for legacy Telegram Markdown.
//
// Deprecated: Use EscapeMarkdownV2 instead.
func EscapeMarkdown(s string) Markdown {
s = strings.ReplaceAll(s, "_", `\_`)
s = strings.ReplaceAll(s, "*", `\*`)
s = strings.ReplaceAll(s, "[", `\[`)
return Markdown(strings.ReplaceAll(s, "`", "\\`"))
}
// Bold returns s wrapped as bold legacy Telegram Markdown text.
func (s Markdown) Bold() Markdown {
return "*" + s + "*"
}
// Italic returns s wrapped as italic legacy Telegram Markdown text.
func (s Markdown) Italic() Markdown {
return "_" + s + "_"
}
// Link returns s as a legacy Telegram Markdown text link.
func (s Markdown) Link(url string) Markdown {
return "[" + s + "](" + Markdown(url) + ")"
}
// Mention returns s as a legacy Telegram Markdown user mention.
func (s Markdown) Mention(userID int64) Markdown {
return "[" + s + "](tg://user?id=" + Markdown(strconv.FormatInt(userID, 10)) + ")"
}
// InlineCode returns s wrapped as inline code legacy Telegram Markdown text.
func (s Markdown) InlineCode() Markdown {
return "`" + s + "`"
}
// BlockCode returns s wrapped as a legacy Telegram Markdown code block.
func (s Markdown) BlockCode() Markdown {
return "```\n" + s + "\n```"
}
// BlockCodeLanguage returns s wrapped as a legacy Telegram Markdown code block.
func (s Markdown) BlockCodeLanguage(lang string) Markdown {
return "```" + Markdown(lang) + "\n" + s + "\n```"
}
+36
View File
@@ -0,0 +1,36 @@
package tgfmt
import "testing"
func TestEscapeMarkdown(t *testing.T) {
got := EscapeMarkdown("a_b*c[1]`x`")
want := Markdown("a\\_b\\*c\\[1]\\`x\\`")
if got != want {
t.Fatalf("EscapeMarkdown() = %q, want %q", got, want)
}
}
func TestMarkdownFormattingMethods(t *testing.T) {
tests := []struct {
name string
got Markdown
want Markdown
}{
{name: "bold", got: EscapeMarkdown("text").Bold(), want: "*text*"},
{name: "italic", got: EscapeMarkdown("text").Italic(), want: "_text_"},
{name: "link", got: EscapeMarkdown("Laniakea").Link("https://example.test"), want: "[Laniakea](https://example.test)"},
{name: "mention", got: EscapeMarkdown("User").Mention(123), want: "[User](tg://user?id=123)"},
{name: "inline code", got: EscapeMarkdown("text").InlineCode(), want: "`text`"},
{name: "block code", got: EscapeMarkdown("text").BlockCode(), want: "```\ntext\n```"},
{name: "block code language", got: EscapeMarkdown("text").BlockCodeLanguage("go"), want: "```go\ntext\n```"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.got != tt.want {
t.Fatalf("formatted Markdown = %q, want %q", tt.got, tt.want)
}
})
}
}
+103
View File
@@ -0,0 +1,103 @@
package tgfmt
import (
"strconv"
"strings"
)
// MarkdownV2 is an escaped Telegram MarkdownV2 fragment.
//
// Methods on MarkdownV2 compose formatting without escaping the fragment again.
type MarkdownV2 string
// EscapeMarkdownV2 escapes special characters for Telegram MarkdownV2.
// https://core.telegram.org/bots/api#markdownv2-style
func EscapeMarkdownV2(s string) MarkdownV2 {
symbols := []string{"\\", "_", "*", "[", "]", "(", ")", "~", "`", ">", "#", "+", "-", "=", "|", "{", "}", ".", "!"}
for _, symbol := range symbols {
s = strings.ReplaceAll(s, symbol, "\\"+symbol)
}
return MarkdownV2(s)
}
// Bold returns s wrapped as bold Telegram MarkdownV2 text.
func (s MarkdownV2) Bold() MarkdownV2 {
return "*" + s + "*"
}
// Italic returns s wrapped as italic Telegram MarkdownV2 text.
func (s MarkdownV2) Italic() MarkdownV2 {
return "_" + s + "_"
}
// Underline returns s wrapped as underlined Telegram MarkdownV2 text.
func (s MarkdownV2) Underline() MarkdownV2 {
return "__" + s + "__"
}
// Strikethrough returns s wrapped as strikethrough Telegram MarkdownV2 text.
func (s MarkdownV2) Strikethrough() MarkdownV2 {
return "~" + s + "~"
}
// Spoiler returns s wrapped as spoiler Telegram MarkdownV2 text.
func (s MarkdownV2) Spoiler() MarkdownV2 {
return "||" + s + "||"
}
// Link returns s as a Telegram MarkdownV2 text link.
func (s MarkdownV2) Link(url string) MarkdownV2 {
return "[" + s + "](" + escapeMarkdownV2LinkDestination(url) + ")"
}
// Mention returns s as a Telegram MarkdownV2 user mention.
func (s MarkdownV2) Mention(userID uint64) MarkdownV2 {
return "[" + s + "](tg://user?id=" + MarkdownV2(strconv.FormatUint(userID, 10)) + ")"
}
// Emoji returns s as a Telegram MarkdownV2 custom emoji.
func (s MarkdownV2) Emoji(emojiID string) MarkdownV2 {
return "[" + s + "](tg://emoji?id=" + escapeMarkdownV2LinkDestination(emojiID) + ")"
}
// Time returns s as a Telegram MarkdownV2 localized timestamp.
func (s MarkdownV2) Time(unix uint64) MarkdownV2 {
return "![" + s + "](tg://time?unix=" + MarkdownV2(strconv.FormatUint(unix, 10)) + ")"
}
// TimeFormat returns s as a Telegram MarkdownV2 localized timestamp with format.
func (s MarkdownV2) TimeFormat(unix uint64, format string) MarkdownV2 {
dest := "tg://time?unix=" + strconv.FormatUint(unix, 10) + "&format=" + format
return "![" + s + "](" + escapeMarkdownV2LinkDestination(dest) + ")"
}
// InlineCode returns s wrapped as inline code Telegram MarkdownV2 text.
func (s MarkdownV2) InlineCode() MarkdownV2 {
return "`" + s + "`"
}
// BlockCode returns s wrapped as a Telegram MarkdownV2 code block.
func (s MarkdownV2) BlockCode() MarkdownV2 {
return "```\n" + s + "\n```"
}
// BlockCodeLanguage returns s wrapped as a Telegram MarkdownV2 code block with language.
func (s MarkdownV2) BlockCodeLanguage(lang string) MarkdownV2 {
return "```" + MarkdownV2(lang) + "\n" + s + "\n```"
}
// Quote returns s as a Telegram MarkdownV2 blockquote.
func (s MarkdownV2) Quote() MarkdownV2 {
return MarkdownV2(">" + strings.ReplaceAll(string(s), "\n", "\n>"))
}
// QuoteExpandable returns s as a Telegram MarkdownV2 expandable blockquote.
func (s MarkdownV2) QuoteExpandable() MarkdownV2 {
return "**>" + s
}
func escapeMarkdownV2LinkDestination(s string) MarkdownV2 {
s = strings.ReplaceAll(s, "\\", "\\\\")
s = strings.ReplaceAll(s, ")", "\\)")
return MarkdownV2(s)
}
+78
View File
@@ -0,0 +1,78 @@
package tgfmt
import "testing"
func TestEscapeMarkdownV2(t *testing.T) {
got := EscapeMarkdownV2(`a_b*c[1](x)!`)
want := MarkdownV2(`a\_b\*c\[1\]\(x\)\!`)
if got != want {
t.Fatalf("EscapeMarkdownV2() = %q, want %q", got, want)
}
}
func TestMarkdownV2ComposesWithoutDoubleEscaping(t *testing.T) {
got := EscapeMarkdownV2("a*b").Bold().Italic()
want := MarkdownV2(`_*a\*b*_`)
if got != want {
t.Fatalf("formatted text = %q, want %q", got, want)
}
}
func TestMarkdownV2FormattingMethods(t *testing.T) {
tests := []struct {
name string
got MarkdownV2
want MarkdownV2
}{
{name: "bold", got: EscapeMarkdownV2("text").Bold(), want: "*text*"},
{name: "italic", got: EscapeMarkdownV2("text").Italic(), want: "_text_"},
{name: "underline", got: EscapeMarkdownV2("text").Underline(), want: "__text__"},
{name: "strikethrough", got: EscapeMarkdownV2("text").Strikethrough(), want: "~text~"},
{name: "spoiler", got: EscapeMarkdownV2("text").Spoiler(), want: "||text||"},
{name: "inline code", got: EscapeMarkdownV2("text").InlineCode(), want: "`text`"},
{name: "block code", got: EscapeMarkdownV2("text").BlockCode(), want: "```\ntext\n```"},
{name: "block code language", got: EscapeMarkdownV2("text").BlockCodeLanguage("go"), want: "```go\ntext\n```"},
{name: "quote", got: EscapeMarkdownV2("a\nb").Quote(), want: ">a\n>b"},
{name: "expandable quote", got: EscapeMarkdownV2("text").QuoteExpandable(), want: "**>text"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.got != tt.want {
t.Fatalf("formatted text = %q, want %q", tt.got, tt.want)
}
})
}
}
func TestMarkdownV2LinkEscapesDestination(t *testing.T) {
got := EscapeMarkdownV2("Laniakea").Link(`https://example.test/a)b\c`)
want := MarkdownV2(`[Laniakea](https://example.test/a\)b\\c)`)
if got != want {
t.Fatalf("Link() = %q, want %q", got, want)
}
}
func TestMarkdownV2SpecialLinks(t *testing.T) {
tests := []struct {
name string
got MarkdownV2
want MarkdownV2
}{
{name: "mention", got: EscapeMarkdownV2("User").Mention(123), want: "[User](tg://user?id=123)"},
{name: "emoji", got: EscapeMarkdownV2("emoji").Emoji(`12)3`), want: `[emoji](tg://emoji?id=12\)3)`},
{name: "time", got: EscapeMarkdownV2("date").Time(1772323200), want: "![date](tg://time?unix=1772323200)"},
{name: "time format", got: EscapeMarkdownV2("date").TimeFormat(1772323200, `MMM ) yyyy`), want: `![date](tg://time?unix=1772323200&format=MMM \) yyyy)`},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.got != tt.want {
t.Fatalf("formatted link = %q, want %q", tt.got, tt.want)
}
})
}
}
+333
View File
@@ -0,0 +1,333 @@
package tgfmt
import (
"strings"
"time"
"git.scuroneko.dev/scuroneko/extypes"
"git.scuroneko.dev/scuroneko/laniakea/tgapi"
)
// MessageBuilder builds Telegram message text with explicit message entities.
// MessageBuilder is not safe for concurrent use.
type MessageBuilder struct {
str string
offset int
entities extypes.Slice[tgapi.MessageEntity]
entries extypes.Slice[*MessageBuilderEntry]
isDirty bool
}
// NewMessageBuilder returns an empty MessageBuilder.
func NewMessageBuilder() *MessageBuilder {
return &MessageBuilder{
entities: make([]tgapi.MessageEntity, 0),
entries: make(extypes.Slice[*MessageBuilderEntry], 0),
isDirty: false,
}
}
// String returns the built message text.
func (b *MessageBuilder) String() string {
if b.isDirty {
b.update()
}
return b.str
}
// Entities returns a copy of the built message entities.
func (b *MessageBuilder) Entities() []tgapi.MessageEntity {
if b.isDirty {
b.update()
}
return append([]tgapi.MessageEntity(nil), b.entities...)
}
// Build returns the built message text and a copy of its entities.
func (b *MessageBuilder) Build() (string, []tgapi.MessageEntity) {
if b.isDirty {
b.update()
}
return b.str, append([]tgapi.MessageEntity(nil), b.entities...)
}
// Reset clears the builder and keeps it ready for reuse.
func (b *MessageBuilder) Reset() {
b.str = ""
b.offset = 0
b.entities = b.entities[:0]
b.entries = b.entries[:0]
b.isDirty = false
}
func (b *MessageBuilder) update() *MessageBuilder {
b.offset = 0
var textLen int
var entitiesLen int
for _, e := range b.entries {
textLen += len(e.text)
entitiesLen += len(e.entities)
}
b.entities = make(extypes.Slice[tgapi.MessageEntity], 0, entitiesLen)
var sb strings.Builder
sb.Grow(textLen)
for _, e := range b.entries {
sb.WriteString(e.text)
for _, entity := range e.entities {
entity.Offset += b.offset
b.entities = append(b.entities, entity)
}
b.offset += e.length
}
b.str = sb.String()
b.isDirty = false
return b
}
func (b *MessageBuilder) markDirty() {
b.isDirty = true
}
// MessageBuilderEntry represents text appended to a MessageBuilder.
type MessageBuilderEntry struct {
text string
length int
b *MessageBuilder
entities extypes.Slice[tgapi.MessageEntity]
}
// Add appends plain text to the message and returns its entry for formatting.
func (b *MessageBuilder) Add(text string) *MessageBuilderEntry {
e := &MessageBuilderEntry{
b: b,
entities: make(extypes.Slice[tgapi.MessageEntity], 0),
text: text,
length: telegramTextLen(text),
}
b.entries = b.entries.Push(e)
b.markDirty()
return e
}
// Mention marks the entry as a Telegram mention.
func (e *MessageBuilderEntry) Mention() *MessageBuilderEntry {
e.addEntity(tgapi.MessageEntity{
Type: tgapi.MessageEntityMention,
Offset: 0, Length: e.length,
})
return e
}
// Hashtag marks the entry as a Telegram hashtag.
func (e *MessageBuilderEntry) Hashtag() *MessageBuilderEntry {
e.addEntity(tgapi.MessageEntity{
Type: tgapi.MessageEntityHashtag,
Offset: 0, Length: e.length,
})
return e
}
// Cashtag marks the entry as a Telegram cashtag.
func (e *MessageBuilderEntry) Cashtag() *MessageBuilderEntry {
e.addEntity(tgapi.MessageEntity{
Type: tgapi.MessageEntityCashtag,
Offset: 0, Length: e.length,
})
return e
}
// BotCommand marks the entry as a Telegram bot command.
func (e *MessageBuilderEntry) BotCommand() *MessageBuilderEntry {
e.addEntity(tgapi.MessageEntity{
Type: tgapi.MessageEntityBotCommand,
Offset: 0, Length: e.length,
})
return e
}
// Email marks the entry as an email address.
func (e *MessageBuilderEntry) Email() *MessageBuilderEntry {
e.addEntity(tgapi.MessageEntity{
Type: tgapi.MessageEntityEmail,
Offset: 0, Length: e.length,
})
return e
}
// Phone marks the entry as a phone number.
func (e *MessageBuilderEntry) Phone() *MessageBuilderEntry {
e.addEntity(tgapi.MessageEntity{
Type: tgapi.MessageEntityPhoneNumber,
Offset: 0, Length: e.length,
})
return e
}
// Bold marks the entry as bold text.
func (e *MessageBuilderEntry) Bold() *MessageBuilderEntry {
e.addEntity(tgapi.MessageEntity{
Type: tgapi.MessageEntityBold,
Offset: 0, Length: e.length,
})
return e
}
// Italic marks the entry as italic text.
func (e *MessageBuilderEntry) Italic() *MessageBuilderEntry {
e.addEntity(tgapi.MessageEntity{
Type: tgapi.MessageEntityItalic,
Offset: 0, Length: e.length,
})
return e
}
// Underline marks the entry as underlined text.
func (e *MessageBuilderEntry) Underline() *MessageBuilderEntry {
e.addEntity(tgapi.MessageEntity{
Type: tgapi.MessageEntityUnderline,
Offset: 0, Length: e.length,
})
return e
}
// Strikethrough marks the entry as strikethrough text.
func (e *MessageBuilderEntry) Strikethrough() *MessageBuilderEntry {
e.addEntity(tgapi.MessageEntity{
Type: tgapi.MessageEntityStrike,
Offset: 0, Length: e.length,
})
return e
}
// Spoiler marks the entry as spoiler text.
func (e *MessageBuilderEntry) Spoiler() *MessageBuilderEntry {
e.addEntity(tgapi.MessageEntity{
Type: tgapi.MessageEntitySpoiler,
Offset: 0, Length: e.length,
})
return e
}
// Quote marks the entry as a blockquote.
func (e *MessageBuilderEntry) Quote() *MessageBuilderEntry {
e.addEntity(tgapi.MessageEntity{
Type: tgapi.MessageEntityBlockquote,
Offset: 0, Length: e.length,
})
return e
}
// ExpandableQuote marks the entry as an expandable blockquote.
func (e *MessageBuilderEntry) ExpandableQuote() *MessageBuilderEntry {
e.addEntity(tgapi.MessageEntity{
Type: tgapi.MessageEntityExpandableBlockquote,
Offset: 0, Length: e.length,
})
return e
}
// InlineCode marks the entry as inline code.
func (e *MessageBuilderEntry) InlineCode() *MessageBuilderEntry {
e.addEntity(tgapi.MessageEntity{
Type: tgapi.MessageEntityCode,
Offset: 0, Length: e.length,
})
return e
}
// CodeBlock marks the entry as a preformatted code block.
func (e *MessageBuilderEntry) CodeBlock() *MessageBuilderEntry {
e.addEntity(tgapi.MessageEntity{
Type: tgapi.MessageEntityPre,
Offset: 0, Length: e.length,
})
return e
}
// CodeBlockWithLanguage marks the entry as a preformatted code block with language.
func (e *MessageBuilderEntry) CodeBlockWithLanguage(lang string) *MessageBuilderEntry {
e.addEntity(tgapi.MessageEntity{
Type: tgapi.MessageEntityPre,
Offset: 0, Length: e.length, Language: lang,
})
return e
}
// Link marks the entry as a text link.
func (e *MessageBuilderEntry) Link(url string) *MessageBuilderEntry {
e.addEntity(tgapi.MessageEntity{
Type: tgapi.MessageEntityTextLink,
Offset: 0, Length: e.length, URL: url,
})
return e
}
// TextMention marks the entry as a mention of user.
func (e *MessageBuilderEntry) TextMention(user *tgapi.User) *MessageBuilderEntry {
e.addEntity(tgapi.MessageEntity{
Type: tgapi.MessageEntityTextMention,
Offset: 0, Length: e.length, User: user,
})
return e
}
// CustomEmoji marks the entry as a custom emoji.
func (e *MessageBuilderEntry) CustomEmoji(emojiID string) *MessageBuilderEntry {
e.addEntity(tgapi.MessageEntity{
Type: tgapi.MessageEntityCustomEmoji,
Offset: 0, Length: e.length, CustomEmojiID: emojiID,
})
return e
}
// DateTime marks the entry as a localized timestamp.
func (e *MessageBuilderEntry) DateTime(time time.Time) *MessageBuilderEntry {
e.addEntity(tgapi.MessageEntity{
Type: tgapi.MessageEntityDateTime,
Offset: 0, Length: e.length, UnixTime: time.Unix(),
})
return e
}
// DateTimeFormat marks the entry as a localized timestamp with format.
func (e *MessageBuilderEntry) DateTimeFormat(time time.Time, format string) *MessageBuilderEntry {
e.addEntity(tgapi.MessageEntity{
Type: tgapi.MessageEntityDateTime,
Offset: 0, Length: e.length,
UnixTime: time.Unix(), DateTimeFormat: format,
})
return e
}
func telegramTextLen(text string) int {
n := 0
for _, r := range text {
if r <= 0xFFFF {
n++
} else {
n += 2
}
}
return n
}
func (e *MessageBuilderEntry) addEntity(entity tgapi.MessageEntity) {
if entity.Length <= 0 {
return
}
e.entities = append(e.entities, entity)
if e.b != nil {
e.b.markDirty()
}
}
+416
View File
@@ -0,0 +1,416 @@
package tgfmt
import (
"reflect"
"testing"
"time"
"git.scuroneko.dev/scuroneko/laniakea/tgapi"
)
func TestMessageBuilder_BuildPlainText(t *testing.T) {
b := NewMessageBuilder()
b.Add("Hello")
b.Add(", ")
b.Add("world")
text, entities := b.Build()
if text != "Hello, world" {
t.Fatalf("text = %q, want %q", text, "Hello, world")
}
if len(entities) != 0 {
t.Fatalf("entities len = %d, want 0", len(entities))
}
}
func TestMessageBuilder_EntityOffsetsAreUTF16(t *testing.T) {
b := NewMessageBuilder()
b.Add("Hi ")
b.Add("👋") // 2 UTF-16 code units
b.Add(" ")
b.Add("world").Bold()
text, entities := b.Build()
if text != "Hi 👋 world" {
t.Fatalf("text = %q, want %q", text, "Hi 👋 world")
}
want := []tgapi.MessageEntity{
{
Type: tgapi.MessageEntityBold,
Offset: 6, // H i space = 3, 👋 = 2, space = 1
Length: 5,
},
}
if !reflect.DeepEqual(entities, want) {
t.Fatalf("entities = %#v, want %#v", entities, want)
}
}
func TestMessageBuilder_EntityLengthIsUTF16(t *testing.T) {
b := NewMessageBuilder()
b.Add("👋").Bold()
text, entities := b.Build()
if text != "👋" {
t.Fatalf("text = %q, want %q", text, "👋")
}
want := []tgapi.MessageEntity{
{
Type: tgapi.MessageEntityBold,
Offset: 0,
Length: 2,
},
}
if !reflect.DeepEqual(entities, want) {
t.Fatalf("entities = %#v, want %#v", entities, want)
}
}
func TestMessageBuilder_MultipleEntitiesOnSameEntry(t *testing.T) {
b := NewMessageBuilder()
b.Add("hello").Bold().Italic()
_, entities := b.Build()
want := []tgapi.MessageEntity{
{
Type: tgapi.MessageEntityBold,
Offset: 0,
Length: 5,
},
{
Type: tgapi.MessageEntityItalic,
Offset: 0,
Length: 5,
},
}
if !reflect.DeepEqual(entities, want) {
t.Fatalf("entities = %#v, want %#v", entities, want)
}
}
func TestMessageBuilder_DoesNotDuplicateAfterRepeatedReads(t *testing.T) {
b := NewMessageBuilder()
b.Add("hello").Bold()
text1 := b.String()
entities1 := b.Entities()
text2 := b.String()
entities2 := b.Entities()
if text1 != text2 {
t.Fatalf("texts differ: %q != %q", text1, text2)
}
if !reflect.DeepEqual(entities1, entities2) {
t.Fatalf("entities differ: %#v != %#v", entities1, entities2)
}
want := []tgapi.MessageEntity{
{
Type: tgapi.MessageEntityBold,
Offset: 0,
Length: 5,
},
}
if !reflect.DeepEqual(entities2, want) {
t.Fatalf("entities = %#v, want %#v", entities2, want)
}
}
func TestMessageBuilder_AddEntityAfterStringMarksDirty(t *testing.T) {
b := NewMessageBuilder()
entry := b.Add("hello")
if got := b.String(); got != "hello" {
t.Fatalf("String() = %q, want %q", got, "hello")
}
entry.Bold()
entities := b.Entities()
want := []tgapi.MessageEntity{
{
Type: tgapi.MessageEntityBold,
Offset: 0,
Length: 5,
},
}
if !reflect.DeepEqual(entities, want) {
t.Fatalf("entities = %#v, want %#v", entities, want)
}
}
func TestMessageBuilder_EntitiesReturnsCopy(t *testing.T) {
b := NewMessageBuilder()
b.Add("hello").Bold()
entities1 := b.Entities()
entities1[0].Offset = 999
entities2 := b.Entities()
if entities2[0].Offset != 0 {
t.Fatalf("Entities() did not return copy: offset = %d, want 0", entities2[0].Offset)
}
}
func TestMessageBuilder_BuildReturnsEntitiesCopy(t *testing.T) {
b := NewMessageBuilder()
b.Add("hello").Bold()
_, entities1 := b.Build()
entities1[0].Offset = 999
_, entities2 := b.Build()
if entities2[0].Offset != 0 {
t.Fatalf("Build() did not return entities copy: offset = %d, want 0", entities2[0].Offset)
}
}
func TestMessageBuilder_Reset(t *testing.T) {
b := NewMessageBuilder()
b.Add("hello").Bold()
if got := b.String(); got != "hello" {
t.Fatalf("String() before Reset = %q, want %q", got, "hello")
}
b.Reset()
text, entities := b.Build()
if text != "" {
t.Fatalf("text after Reset = %q, want empty", text)
}
if len(entities) != 0 {
t.Fatalf("entities len after Reset = %d, want 0", len(entities))
}
b.Add("world").Italic()
text, entities = b.Build()
if text != "world" {
t.Fatalf("text after reuse = %q, want %q", text, "world")
}
want := []tgapi.MessageEntity{
{
Type: tgapi.MessageEntityItalic,
Offset: 0,
Length: 5,
},
}
if !reflect.DeepEqual(entities, want) {
t.Fatalf("entities after reuse = %#v, want %#v", entities, want)
}
}
func TestMessageBuilder_EmptyEntryDoesNotCreateEntity(t *testing.T) {
b := NewMessageBuilder()
b.Add("").Bold()
b.Add("x")
text, entities := b.Build()
if text != "x" {
t.Fatalf("text = %q, want %q", text, "x")
}
if len(entities) != 0 {
t.Fatalf("entities len = %d, want 0: %#v", len(entities), entities)
}
}
func TestMessageBuilder_Link(t *testing.T) {
b := NewMessageBuilder()
b.Add("OpenAI").Link("https://openai.com")
_, entities := b.Build()
want := []tgapi.MessageEntity{
{
Type: tgapi.MessageEntityTextLink,
Offset: 0,
Length: 6,
URL: "https://openai.com",
},
}
if !reflect.DeepEqual(entities, want) {
t.Fatalf("entities = %#v, want %#v", entities, want)
}
}
func TestMessageBuilder_CodeBlockWithLanguage(t *testing.T) {
b := NewMessageBuilder()
b.Add("fmt.Println(\"hi\")").CodeBlockWithLanguage("go")
_, entities := b.Build()
want := []tgapi.MessageEntity{
{
Type: tgapi.MessageEntityPre,
Offset: 0,
Length: 17,
Language: "go",
},
}
if !reflect.DeepEqual(entities, want) {
t.Fatalf("entities = %#v, want %#v", entities, want)
}
}
func TestMessageBuilder_DateTimeFormat(t *testing.T) {
b := NewMessageBuilder()
ts := time.Unix(1772323200, 0)
b.Add("date").DateTimeFormat(ts, "MMMM d, yyyy")
_, entities := b.Build()
want := []tgapi.MessageEntity{
{
Type: tgapi.MessageEntityDateTime,
Offset: 0,
Length: 4,
UnixTime: 1772323200,
DateTimeFormat: "MMMM d, yyyy",
},
}
if !reflect.DeepEqual(entities, want) {
t.Fatalf("entities = %#v, want %#v", entities, want)
}
}
func TestTelegramTextLen(t *testing.T) {
tests := []struct {
name string
text string
want int
}{
{
name: "ascii",
text: "hello",
want: 5,
},
{
name: "cyrillic",
text: "привет",
want: 6,
},
{
name: "emoji",
text: "👋",
want: 2,
},
{
name: "mixed",
text: "a👋b",
want: 4,
},
{
name: "zwj sequence",
text: "👨‍👩‍👧‍👦",
want: 11,
},
{
name: "flag",
text: "🇫🇮",
want: 4,
},
{
name: "variation selector",
text: "❤️",
want: 2,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := telegramTextLen(tt.text)
if got != tt.want {
t.Fatalf("telegramTextLen(%q) = %d, want %d", tt.text, got, tt.want)
}
})
}
}
func TestMessageBuilder_SimpleEntityTypes(t *testing.T) {
tests := []struct {
name string
add func(*MessageBuilderEntry)
want tgapi.MessageEntityType
}{
{"mention", func(e *MessageBuilderEntry) { e.Mention() }, tgapi.MessageEntityMention},
{"hashtag", func(e *MessageBuilderEntry) { e.Hashtag() }, tgapi.MessageEntityHashtag},
{"cashtag", func(e *MessageBuilderEntry) { e.Cashtag() }, tgapi.MessageEntityCashtag},
{"bot command", func(e *MessageBuilderEntry) { e.BotCommand() }, tgapi.MessageEntityBotCommand},
{"email", func(e *MessageBuilderEntry) { e.Email() }, tgapi.MessageEntityEmail},
{"phone", func(e *MessageBuilderEntry) { e.Phone() }, tgapi.MessageEntityPhoneNumber},
{"bold", func(e *MessageBuilderEntry) { e.Bold() }, tgapi.MessageEntityBold},
{"italic", func(e *MessageBuilderEntry) { e.Italic() }, tgapi.MessageEntityItalic},
{"underline", func(e *MessageBuilderEntry) { e.Underline() }, tgapi.MessageEntityUnderline},
{"strikethrough", func(e *MessageBuilderEntry) { e.Strikethrough() }, tgapi.MessageEntityStrike},
{"spoiler", func(e *MessageBuilderEntry) { e.Spoiler() }, tgapi.MessageEntitySpoiler},
{"quote", func(e *MessageBuilderEntry) { e.Quote() }, tgapi.MessageEntityBlockquote},
{"expandable quote", func(e *MessageBuilderEntry) { e.ExpandableQuote() }, tgapi.MessageEntityExpandableBlockquote},
{"inline code", func(e *MessageBuilderEntry) { e.InlineCode() }, tgapi.MessageEntityCode},
{"code block", func(e *MessageBuilderEntry) { e.CodeBlock() }, tgapi.MessageEntityPre},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
b := NewMessageBuilder()
e := b.Add("hello")
tt.add(e)
_, entities := b.Build()
want := []tgapi.MessageEntity{
{
Type: tt.want,
Offset: 0,
Length: 5,
},
}
if !reflect.DeepEqual(entities, want) {
t.Fatalf("entities = %#v, want %#v", entities, want)
}
})
}
}
+12
View File
@@ -0,0 +1,12 @@
package tgfmt
import "strings"
// EscapePunctuation escapes '.', '!' and '-' for MarkdownV2 fragments.
func EscapePunctuation(s string) string {
symbols := []string{".", "!", "-"}
for _, symbol := range symbols {
s = strings.ReplaceAll(s, symbol, "\\"+symbol)
}
return s
}
+2 -2
View File
@@ -6,7 +6,7 @@ import (
"git.scuroneko.dev/scuroneko/laniakea/tgapi"
)
func (bot *Bot[T]) handleUpdate(u *tgapi.Update, ctx *MsgContext) bool {
func (bot *Bot[T]) handleUpdate(u *tgapi.Update, ctx *MessageContext) bool {
handled := false
for _, plugin := range bot.plugins {
handler, ok := plugin.handlers[u.Type]
@@ -66,7 +66,7 @@ func (bot *Bot[T]) handleUpdate(u *tgapi.Update, ctx *MsgContext) bool {
return handled
}
func (bot *Bot[T]) prepareUpdateCtx(u *tgapi.Update, ctx *MsgContext) {
func (bot *Bot[T]) prepareUpdateCtx(u *tgapi.Update, ctx *MessageContext) {
var from *tgapi.User
var chat *tgapi.Chat
switch u.Type {
+10 -37
View File
@@ -1,7 +1,8 @@
package laniakea
import (
"strings"
"crypto/rand"
"encoding/base64"
"git.scuroneko.dev/scuroneko/laniakea/utils"
)
@@ -17,42 +18,6 @@ func Val[T any](p *T, def T) T {
return def
}
// EscapeMarkdown escapes special characters for legacy Telegram Markdown.
// Deprecated: Use EscapeMarkdownV2.
func EscapeMarkdown(s string) string {
s = strings.ReplaceAll(s, "_", `\_`)
s = strings.ReplaceAll(s, "*", `\*`)
s = strings.ReplaceAll(s, "[", `\[`)
return strings.ReplaceAll(s, "`", "\\`")
}
// EscapeHTML escapes special characters for Telegram HTML parse mode.
func EscapeHTML(s string) string {
s = strings.ReplaceAll(s, "&", "&amp;")
s = strings.ReplaceAll(s, "<", "&lt;")
s = strings.ReplaceAll(s, ">", "&gt;")
return s
}
// EscapeMarkdownV2 escapes special characters for Telegram MarkdownV2.
// https://core.telegram.org/bots/api#markdownv2-style
func EscapeMarkdownV2(s string) string {
symbols := []string{"\\", "_", "*", "[", "]", "(", ")", "~", "`", ">", "#", "+", "-", "=", "|", "{", "}", ".", "!"}
for _, symbol := range symbols {
s = strings.ReplaceAll(s, symbol, "\\"+symbol)
}
return s
}
// EscapePunctuation escapes '.', '!' and '-' for MarkdownV2 fragments.
func EscapePunctuation(s string) string {
symbols := []string{".", "!", "-"}
for _, symbol := range symbols {
s = strings.ReplaceAll(s, symbol, "\\"+symbol)
}
return s
}
const (
// VersionString re-exports the module version string.
VersionString = utils.VersionString
@@ -65,3 +30,11 @@ const (
// VersionBeta re-exports the module prerelease counter.
VersionBeta = utils.VersionBeta
)
func generateToken(b int) (string, error) {
bytes := make([]byte, b)
if _, err := rand.Read(bytes); err != nil {
return "", err
}
return base64.URLEncoding.EncodeToString(bytes), nil
}
+50 -9
View File
@@ -16,6 +16,10 @@ var ErrDropOverflow = errors.New("drop overflow limit")
// It supports two modes:
// - "drop" mode: immediately reject if limits are exceeded.
// - "wait" mode: block until capacity is available.
//
// Per-chat limiters are created lazily and accumulate indefinitely. Call Cleanup
// periodically (e.g. from a background runner) to evict idle entries and prevent
// unbounded memory growth in bots that serve many distinct chats.
type RateLimiter struct {
globalLockUntil time.Time // global cooldown timestamp (set by API errors)
globalLimiter *rate.Limiter // global token bucket (30 req/sec)
@@ -23,7 +27,8 @@ type RateLimiter struct {
chatLocks map[int64]time.Time // per-chat cooldown timestamps
chatLimiters map[int64]*rate.Limiter // per-chat token buckets (1 req/sec)
chatMu sync.RWMutex // protects chatLocks and chatLimiters
chatLastSeen map[int64]time.Time // last access timestamp per chat, for Cleanup eviction
chatMu sync.RWMutex // protects chatLocks, chatLimiters, and chatLastSeen
}
// NewRateLimiter creates a new RateLimiter with default limits.
@@ -34,6 +39,32 @@ func NewRateLimiter() *RateLimiter {
globalLimiter: rate.NewLimiter(30, 30),
chatLimiters: make(map[int64]*rate.Limiter),
chatLocks: make(map[int64]time.Time),
chatLastSeen: make(map[int64]time.Time),
}
}
// Cleanup removes per-chat limiter state that has not been touched within
// idleThreshold and chat cooldowns whose expiry has already passed.
//
// Safe to call concurrently with Wait/Allow. Intended for periodic invocation
// from a background runner (e.g. once a minute) to bound memory in long-running
// bots that serve many distinct chats.
func (rl *RateLimiter) Cleanup(idleThreshold time.Duration) {
now := time.Now()
rl.chatMu.Lock()
defer rl.chatMu.Unlock()
for chatID, lastSeen := range rl.chatLastSeen {
if now.Sub(lastSeen) <= idleThreshold {
continue
}
delete(rl.chatLimiters, chatID)
delete(rl.chatLastSeen, chatID)
}
for chatID, until := range rl.chatLocks {
if !until.After(now) {
delete(rl.chatLocks, chatID)
}
}
}
@@ -103,7 +134,6 @@ func (rl *RateLimiter) Wait(ctx context.Context, chatID int64) error {
return chatLimiter.Wait(ctx)
}
// Internal helper that returns the global limiter under read lock.
func (rl *RateLimiter) getGlobalLimiter() *rate.Limiter {
rl.globalMu.RLock()
defer rl.globalMu.RUnlock()
@@ -191,7 +221,6 @@ func (rl *RateLimiter) Check(ctx context.Context, dropOverflow bool, chatID int6
return nil
}
// Internal helper that waits for the global cooldown to expire.
func (rl *RateLimiter) waitForGlobalUnlock(ctx context.Context) error {
rl.globalMu.RLock()
until := rl.globalLockUntil
@@ -209,7 +238,6 @@ func (rl *RateLimiter) waitForGlobalUnlock(ctx context.Context) error {
}
}
// Internal helper that waits for a chat-specific cooldown to expire.
func (rl *RateLimiter) waitForChatUnlock(ctx context.Context, chatID int64) error {
rl.chatMu.RLock()
until, ok := rl.chatLocks[chatID]
@@ -227,15 +255,28 @@ func (rl *RateLimiter) waitForChatUnlock(ctx context.Context, chatID int64) erro
}
}
// Internal helper that returns or creates a per-chat limiter.
// Updates chatLastSeen so Cleanup can evict idle entries.
func (rl *RateLimiter) getChatLimiter(chatID int64) *rate.Limiter {
rl.chatMu.Lock()
defer rl.chatMu.Unlock()
now := time.Now()
if lim, ok := rl.chatLimiters[chatID]; ok {
rl.chatMu.RLock()
lim, ok := rl.chatLimiters[chatID]
rl.chatMu.RUnlock()
if ok {
rl.chatMu.Lock()
rl.chatLastSeen[chatID] = now
rl.chatMu.Unlock()
return lim
}
lim := rate.NewLimiter(1, 1)
rl.chatMu.Lock()
defer rl.chatMu.Unlock()
if lim, ok := rl.chatLimiters[chatID]; ok {
rl.chatLastSeen[chatID] = now
return lim
}
lim = rate.NewLimiter(1, 1)
rl.chatLimiters[chatID] = lim
rl.chatLastSeen[chatID] = now
return lim
}
+49
View File
@@ -39,3 +39,52 @@ func TestRateLimiterGlobalWaitRespectsContextCancellation(t *testing.T) {
t.Fatalf("expected DeadlineExceeded, got %v", err)
}
}
// TestRateLimiterCleanupEvictsIdleChats guards the memory-leak fix: per-chat
// limiter and lastSeen state must be reclaimed by Cleanup once the entry has
// been idle for longer than the threshold, while still-active chats and
// unexpired cooldowns must survive.
func TestRateLimiterCleanupEvictsIdleChats(t *testing.T) {
rl := NewRateLimiter()
// Touch chat 1 to make it tracked, then backdate its last-seen marker
// so it looks idle from Cleanup's perspective.
if !rl.Allow(1) {
t.Fatal("expected initial Allow for chat 1 to succeed")
}
rl.chatMu.Lock()
rl.chatLastSeen[1] = time.Now().Add(-time.Hour)
rl.chatMu.Unlock()
// Touch chat 2 so it stays "active".
if !rl.Allow(2) {
t.Fatal("expected initial Allow for chat 2 to succeed")
}
// Expired cooldown should be evicted; future cooldown should survive.
rl.SetChatLock(10, 1)
rl.chatMu.Lock()
rl.chatLocks[10] = time.Now().Add(-time.Second)
rl.chatLocks[11] = time.Now().Add(time.Hour)
rl.chatMu.Unlock()
rl.Cleanup(time.Minute)
rl.chatMu.RLock()
defer rl.chatMu.RUnlock()
if _, ok := rl.chatLimiters[1]; ok {
t.Fatal("expected idle chat 1 limiter to be evicted")
}
if _, ok := rl.chatLastSeen[1]; ok {
t.Fatal("expected idle chat 1 lastSeen to be evicted")
}
if _, ok := rl.chatLimiters[2]; !ok {
t.Fatal("expected active chat 2 limiter to remain")
}
if _, ok := rl.chatLocks[10]; ok {
t.Fatal("expected expired chat 10 lock to be evicted")
}
if _, ok := rl.chatLocks[11]; !ok {
t.Fatal("expected future chat 11 lock to remain")
}
}
+5 -2
View File
@@ -6,10 +6,13 @@ import (
"git.scuroneko.dev/scuroneko/sneklog/v2"
)
// LogFormat selects the writer format used by framework loggers.
type LogFormat string
const (
// LogFormatText writes human-readable text logs.
LogFormatText LogFormat = "text"
// LogFormatJSON writes structured JSON logs.
LogFormatJSON LogFormat = "json"
)
@@ -22,8 +25,8 @@ func GetLoggerLevel() sneklog.LogLevel {
return level
}
// CreateLogger creates a logger with the shared default policy:
// JSON stdout output, provided prefix, and provided level.
// CreateLogger creates a logger with stdout output, the provided name, level,
// format, and optional formatter.
func CreateLogger(
name string, level sneklog.LogLevel,
format LogFormat, formatter *sneklog.Formatter,
+3 -3
View File
@@ -2,13 +2,13 @@ package utils
const (
// VersionString is the module version string.
VersionString = "1.0.0-rc.16"
VersionString = "1.0.2"
// VersionMajor is the module major version.
VersionMajor = 1
// VersionMinor is the module minor version.
VersionMinor = 0
// VersionPatch is the module patch version.
VersionPatch = 0
VersionPatch = 2
// VersionBeta is the prerelease counter for the current version.
VersionBeta = 16
VersionBeta = 0
)