REPOSITORY / ScuroNeko/Laniakea

Compare commits

DIFF REPOSITORY

Compare commits

...
Author SHA1 Message Date
ScuroNeko 83bcab6415 Rename tgapi request structs
Add MaybeInaccessibleMessage tests and godoc
Update wiki and Bot API docs for the new naming
2026-04-06 16:06:57 +03:00
ScuroNeko d55f58c092 Add shared webhook runtime support
Share queue and runner lifecycle between polling and webhook

Keep Close() local-only and harden webhook startup validation

Document runtime transitions and add webhook regression tests
2026-04-01 17:36:23 +03:00
ScuroNeko ba25dab6b1 Refine backlog policy and runtime guards
Move speculative framework items into Ideas and bump rc.14

Guard tgapi API/uploader setup and nil plugin registrations

Add missing observer Event godoc
2026-03-31 16:57:08 +03:00
ScuroNeko a818174fbf add observer runtime model
Instrument command, payload, update, scene, runner, polling, and policy flows
Add observer regression coverage and mark observability backlog item done
2026-03-31 13:50:21 +03:00
ScuroNeko 140f3397b2 add authorization policies
Introduce first-class policy helpers and composition through middleware

Normalize chat context for more updates and add regression coverage

Document the completed backlog item in changelog, TODO, and wiki
2026-03-30 18:17:28 +03:00
ScuroNeko e2444752c2 rename app data API
Align docs, examples, and tests with AppData and NoData

Update handler, scene, runner, and plugin generics to the new naming

Ignore .codex and record the cleanup in the changelog
2026-03-30 16:43:59 +03:00
ScuroNeko 66eb72cb3c Finalize rc.13 framework contracts
Formalize update routing, config freeze, and error visibility
Add regression coverage and sync backlog records
2026-03-30 01:01:13 +03:00
ScuroNeko f74496a3e8 Release v1.0.0-rc.12
Finalize scenes, typed arg binding, and request-scoped context plumbing

Refresh docs, backlog state, and regression coverage for the rc.12 release
2026-03-30 00:12:24 +03:00
ScuroNeko a4d70e1510 Stabilize scene session skeleton
Hide internal scene runtime methods
Refresh docs, tests, and changelog for scenes
2026-03-28 12:58:05 +00:00
ScuroNeko 3ad9e48d71 fix: guard nil logger in prefix check 2026-03-27 16:06:24 +03:00
ScuroNeko 4f8d583b03 wip: scene sessions 2026-03-27 16:05:20 +03:00
ScuroNeko 68e7529f16 Add framework backlog and signed-commit policy
Document scene modal-chat flow and other missing core concepts in TODO.md
Require agent-created commits to be GPG-signed and fail fast if signing cannot complete
2026-03-26 23:27:54 +03:00
ScuroNeko 0ee0917af5 Update repo agent documentation rules
Require paired English and Russian docs when expanding project documentation
Clarify changelog handling for main repo changes and keep rc.12 notes aligned
Ignore local editor and Codex config directories in git
2026-03-26 23:06:05 +03:00
ScuroNeko 8618397bc1 wip: expand docs and wiki structure
add wiki links to README and README_RU
fill Start here pages for setup, commands/plugins, and MsgContext
add wiki page-priority tracker and local wiki AGENTS rules
2026-03-26 18:35:35 +03:00
ScuroNeko 945b8240e6 wip: add long message helpers and payload type controls
- add centralized message validation errors
- switch command handlers to return error
- add explicit long plain-text reply helpers
- support strict payload type policy with debug logging
- document versioning and changelog workflow
2026-03-26 18:15:06 +03:00
ScuroNeko 5d3199dc21 fix(tgapi): accept string chat boost IDs 2026-03-25 18:20:20 +03:00
ScuroNeko 158625c220 fix bot lifecycle and docs 2026-03-25 18:07:41 +03:00
ScuroNeko 7901fb659e fix bot safety and tgapi edge cases 2026-03-25 13:41:30 +03:00
ScuroNeko eda635e72c fix: enforce required command arg positions 2026-03-25 13:17:44 +03:00
ScuroNeko f0da64c7af logger now initialize before middleware 2026-03-25 12:57:11 +03:00
ScuroNeko 3861746a3e logger now initialize before middleware 2026-03-25 12:56:26 +03:00
ScuroNeko 401173714e refactor(logging): use context logger in MsgContext 2026-03-24 14:02:51 +03:00
96 changed files with 10597 additions and 1987 deletions
+5 -1
View File
@@ -1,2 +1,6 @@
.idea/ .idea/
test/ .wiki/
.vscode/
test/
.codex/
.codex
+162
View File
@@ -0,0 +1,162 @@
# AGENTS.md
## Purpose
This repository uses Codex 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.
## Review priorities
Review the codebase with focus on:
- correctness and reliability;
- maintainability and architecture;
- idiomatic Go;
- testability;
- performance where justified by code evidence;
- security;
- godoc quality.
## Scope rules
- Always review the whole repository unless the prompt explicitly narrows scope.
- Check cross-package interactions, public APIs, package boundaries, and shared patterns.
- Prefer concrete fixes over generic advice.
- When feasible, make small, high-confidence improvements directly.
- When uncertain, state confidence level and evidence.
## Documentation languages
- When creating or expanding project documentation, generate and maintain both English and Russian versions in the same turn whenever reasonably possible.
- For wiki pages, prefer paired pages such as `Page.md` and `Page-RU.md`.
- Keep English and Russian pages aligned in structure, major examples, and user-facing guidance.
- If only one language can be updated safely in the current turn, explicitly say which language is lagging and why.
## Wiki and backlog workflow
- Treat the wiki as the primary place for large design ideas, architectural drafts, and framework backlog notes.
- If the agent identifies a substantial new concept or design direction, such as scenes, callback agents, a webhook model, or another framework-level abstraction, the agent must ask the user whether it should also formalize that idea as a draft wiki page.
- When the user agrees, prefer paired wiki pages such as `Page.md` and `Page-RU.md`, and clearly mark draft design pages with `DRAFT` when the API is not implemented or not yet stable.
- Keep `TODO.md`, the wiki backlog pages, and `CHANGELOG.md` aligned when framework-level items move between planned and completed states in the main repository.
- Wiki-only edits must never be added to `CHANGELOG.md`.
- `AGENTS.md`-only edits must never be added to `CHANGELOG.md`.
## Go review expectations
Check for:
- bugs, fragile logic, invalid assumptions, nil handling issues, resource leaks;
- poor error handling;
- misuse of context, cancellation, timeouts, retries, and cleanup;
- race risks, deadlocks, blocking hazards, unsafe shared state;
- non-idiomatic naming, APIs, interfaces, package structure, and error patterns;
- unnecessary complexity, duplication, or weak abstractions;
- obvious performance problems supported by the code;
- security risks such as unsafe input handling, secret leakage, insecure logging, injection risks, and risky file or network operations.
## Godoc rules
Review comments for all declarations.
### Exported declarations
Exported types, funcs, methods, vars, and consts must have godoc comments.
Each exported godoc comment must:
- start with the identifier name;
- explain the purpose or behavior;
- be as short as possible without losing important meaning;
- avoid repeating the signature mechanically;
- stay high-signal and informative.
### Unexported declarations
Unexported types, funcs, methods, vars, and consts should generally not have godoc-style comments unless there is a strong reason.
### Always report
- missing godoc on exported declarations;
- unnecessary godoc on unexported declarations;
- comments that are too long, vague, redundant, or low-value;
- comments that should be shortened or rewritten.
When feasible, rewrite bad godoc into better versions.
## Testing expectations
Treat tests as a required part of review.
- Assess existing test quality, not only test presence.
- Add or propose as many useful tests as reasonably possible.
- Prioritize public APIs, critical flows, edge cases, negative paths, boundary conditions, and concurrency-sensitive logic.
- Prefer table-driven tests where appropriate.
- Add regression tests for bugs you find.
- If a case is hard to test directly, explain the gap and the best test strategy.
## Commands
Before finalizing changes, run the relevant project checks when available:
- build
- tests
- lint
- static analysis
Prefer the repositorys documented commands. If multiple choices exist, use the most standard and least destructive ones first.
## Versioning and changelog
- After every code or documentation change in the main repository, update `CHANGELOG.md`.
- Changes made only inside the `.wiki/` repository must not be added to `CHANGELOG.md`.
- 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.
- 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`
2. `v1.1.0`
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.
- 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`;
3. keep the item title and descriptive notes aligned with the corresponding `CHANGELOG.md` entry.
- The agent must treat `TODO.md` and `CHANGELOG.md` as linked records: a completed backlog item should not be left in one file as done and in the other as still pending or undocumented.
## Breaking changes policy
- The agent must detect potential breaking changes before editing public APIs.
- Breaking changes are forbidden unless the selected target version is a new major version.
- If the requested change is breaking and the user did not bump the major version, the agent must stop and warn that the change is not allowed under the current version.
- In that case, the agent must offer only these options:
1. do not make the breaking change;
2. introduce a backward-compatible alternative such as a new method, function, type, or struct, but only if that keeps the codebase reasonably small and clear;
3. bump the major version and then apply the breaking change.
- Prefer additive compatibility over signature changes when the additive option is small and maintainable.
- Example: if a method like `ctx.answer(...)` needs an extra parameter, the agent must either require a major-version bump or add a new method that keeps the old method working.
## Commit message format
- When the user asks for a commit message, the agent must produce it in this format:
1. a short summary line;
2. up to three additional lines with only the most important changes;
3. each additional line must start on its own new line.
- The agent must output the commit message as a plain multiline block that the user can copy directly.
- Do not collapse the lines into a paragraph, bullet list, or wrapped prose explanation.
- Keep commit text concise and high-signal.
- Do not turn commit messages into changelogs.
## Commit signing
- All commits created by the agent must be GPG-signed.
- If commit signing or pushing requires leaving the sandbox, the agent must request escalation explicitly before running the command.
- If a signed commit cannot be created successfully, the agent must report the failure clearly and stop instead of creating an unsigned fallback commit.
## Output format
For repo-wide review tasks, structure the result as:
1. Overall summary
2. Critical findings
3. Major findings
4. Minor findings
5. Godoc issues
6. Test gaps and added/proposed tests
7. Good decisions worth keeping
8. Summary of concrete changes made
For each finding include:
- location;
- issue;
- why it matters;
- recommended fix.
## Working style
- Be direct, specific, and action-oriented.
- Do not stop at style-only feedback.
- Use full repository context before drawing conclusions.
- Prefer minimal, high-confidence patches.
- Preserve behavior unless intentionally fixing a bug.
+291
View File
@@ -0,0 +1,291 @@
# Changelog
## v1.0.0-rc.14
### Bot API 9.6
#### Managed Bots
- Added the field can_manage_bots to the class User.
- Added the class KeyboardButtonRequestManagedBot and the field request_managed_bot to the class KeyboardButton.
- Added the class ManagedBotCreated and the field managed_bot_created to the class Message.
- Added updates about the creation of managed bots and the change of their token, represented by the class ManagedBotUpdated and the field managed_bot in the class Update.
- Added the methods getManagedBotToken and replaceManagedBotToken.
- Added the class PreparedKeyboardButton and the method savePreparedKeyboardButton, allowing bots to request users, chats and managed bots from Mini Apps.
- Added the method requestChat to the class WebApp.
- Added support for https://t.me/newbot/{manager_bot_username}/{suggested_bot_username}[?name={suggested_bot_name}] links, allowing bots to request the creation of a managed bot via a link.
### Polls
- Added support for quizzes with multiple correct answers.
- Replaced the field correct_option_id with the field correct_option_ids in the class Poll.
- Replaced the parameter correct_option_id with the parameter correct_option_ids in the method sendPoll.
- Allowed to pass allows_multiple_answers for quizzes in the method sendPoll.
- Increased the maximum time for automatic poll closure to 2628000 seconds.
- Added the field allows_revoting to the class Poll.
- Added the parameter allows_revoting to the method sendPoll.
- Added the parameter shuffle_options to the method sendPoll.
- Added the parameter allow_adding_options to the method sendPoll.
- Added the parameter hide_results_until_closes to the method sendPoll.
- Added the fields description and description_entities to the class Poll.
- Added the parameters description, description_parse_mode, and description_entities to the method sendPoll.
- Added the field persistent_id to the class PollOption, representing a persistent identifier for the option.
- Added the field option_persistent_ids to the class PollAnswer.
- Added the fields added_by_user and added_by_chat to the class PollOption, denoting the user and the chat which added the option.
- Added the field addition_date to the class PollOption, describing the date when the option was added.
- Added the class PollOptionAdded and the field poll_option_added to the class Message.
- Added the class PollOptionDeleted and the field poll_option_deleted to the class Message.
- Added the field poll_option_id to the class ReplyParameters, allowing bots to reply to a specific poll option.
- Added the field reply_to_poll_option_id to the class Message.
- Allowed “date_time” entities in checklist title, checklist task text, TextQuote, ReplyParameters quote, sendGift, and giftPremiumSubscription.
**More info**: https://core.telegram.org/bots/api#april-3-2026
### Breaking Changes
- Exported `tgapi` request parameter structs were renamed from the `*P` suffix to their method names. Update code such as `tgapi.SendMessageP{...}` to `tgapi.SendMessage{...}`.
### Changed
- *Support for Bot API 9.6*
- Added missing godoc for recently introduced Telegram Bot API managed-bot, prepared-button, chat-owner, and video-quality exported declarations.
- Renamed exported `tgapi` request parameter structs from the `*P` suffix to their method names, for example `SendMessageP` -> `SendMessage` and `SetWebhookP` -> `SetWebhook`.
- Added missing godoc for the exported observer `Event` marker interface.
- Webhook execution now shares the bot's queued update-dispatch path with polling, including worker-pool delivery, runner startup, single-use run semantics, and default fallback to bot-level update type filters when webhook-specific filters are not set.
- Webhook godoc and the English and Russian READMEs now describe the bot-level webhook runtime, its single-use lifecycle, and the main `RunWebHookWithContext(...)` entry points more explicitly.
- `Bot.Close()` once again releases only local resources and no longer deletes remote webhook registrations implicitly; explicit remote webhook teardown remains opt-in through `CloseWebHook()`.
- Polling and webhook docs now explicitly state that a deployment must delete its webhook before switching from webhook delivery to long polling.
- Webhook startup now validates path shape and TLS file count before remote webhook setup, and the shared webhook mux now serves both HTTP and TLS runtime paths consistently.
- Webhook-related `tgapi` request params now use `int8` for `max_connections`, matching Telegram's `1..100` range and the higher-level webhook options API.
- Webhook startup now also requires a non-empty `SecretToken` when the optional `/status` endpoint is enabled, preventing anonymous exposure of webhook operational metadata.
- Webhook debug logging now records update metadata instead of dumping raw request bodies.
- Package docs, README guidance, and core wiki pages now align with the current public API and runtime model, including `NoData`, `SetAppData(...)`, `SetL10n(...)`, `AddAppDataLoggerWriter(...)`, shared runner startup semantics, and the webhook runtime entry points.
### Tests
- Added regression coverage for webhook queue delivery, webhook runtime single-use behavior, runner startup in webhook mode, and default webhook `allowed_updates` inheritance from bot-level update type configuration.
- Added regression coverage proving `Bot.Close()` does not make remote webhook delete requests.
- Added webhook regression coverage for path validation, TLS file-count validation, oversized-body rejection, status-endpoint secret checks, and invalid TLS startup arguments.
- Added webhook regression coverage proving `/status` cannot be enabled without a non-empty `SecretToken`.
- Added regression coverage for Bot API 9.6 poll decoding, `managed_bot` update decoding, and structured `setChatMenuButton(...)` request serialization.
- Added regression coverage for `MaybeInaccessibleMessage` accessible and inaccessible JSON decoding.
## v1.0.0-rc.13
### Added
- `AsUserError(...)`, `AsInternalError(...)`, `IsUserError(...)`, and `IsInternalError(...)` for explicitly marking centralized handler errors as user-visible or internal-only without breaking the existing default error flow.
- `Policy[T]`, `RequirePolicy(...)`, and built-in chat and callback policy helpers for expressing reusable authorization rules through the existing middleware pipeline.
- `Bot.UsePolicy(...)` and `Plugin.UsePolicy(...)` as shorthand for registering policies as middleware.
- `AllPolicies(...)`, `AnyPolicy(...)`, and `NotPolicy(...)` for composing reusable authorization rules without introducing a second execution pipeline.
### Changed
- Bot configuration mutators now treat the bot as configuration-frozen after the first run begins and ignore late mutation attempts for bot-level config such as prefixes, payload defaults, plugins, middleware, runners, localization, scene session wiring, and database context injection.
- `MsgContext` godoc and field comments now describe the normalized update contract more explicitly, including when `Msg`, `From`, callback target fields, `Text`, and `Args` are expected to be populated.
- `MsgContext` normalization now also carries `Chat` and `ChatID` for more Telegram update kinds, allowing policy and update handlers to rely on normalized chat identity outside message-only flows.
- `MsgContext.Error(...)` and returned handler errors now suppress the automatic user reply when the error is explicitly marked with `AsInternalError(...)`, while keeping the previous user-visible default for unclassified errors.
- Godoc, README examples, and regression-test naming now consistently describe the shared generic dependency model as app data, including `NoData` and `SetAppData(...)`.
- Observer configuration now treats `SetObserver(nil)` as clearing instrumentation instead of leaving the previous observer attached.
- Observer lifecycle events now cover generic update handlers and scene command, step, and message-fallback handlers with logical handler names and durations.
- `RequirePolicy(...)` now emits `PolicyCheckedEvent` for both passed and denied policy decisions.
- Scene command, step, and message-fallback flows now emit observer `ErrorEvent`s with scene-specific handler kinds and logical handler names.
- Scene transition observer events now use the same transition payload for scene command, step, and message-fallback flows.
- Observer error emission now also covers generic update handlers, callback payload decode failures, runner failures, and polling retries, including dedicated runner and polling handler kinds in `ErrorEvent`.
- `TODO.md` and the framework backlog pages now mark the observability model as completed for `v1.0.0-rc.13`.
- `tgapi.Chat.Type` now uses the typed `tgapi.ChatType` enum in public DTOs and tests instead of raw string casts.
### Tests
- Added regression coverage for the bot configuration freeze model, including ignored post-run mutations for core bot configuration methods and late registration paths.
- Added table-driven update-contract coverage for `prepareUpdateCtx(...)`, including message-backed, callback-backed, user-backed, and no-user update kinds.
- Added regression tests for policy middleware blocking, built-in private-chat policy decisions, normalized chat identity, and admin checks that use normalized `ChatID` and `FromID`.
- Added regression tests for policy composition semantics, including all-of, any-of, and deny inversion with preserved internal failures.
- Added regression tests for `SetObserver(...)`, `GetObserver()`, and clearing the observer with `SetObserver(nil)`.
- Added observer regression tests for generic update-handler errors, callback payload decode failures, runner failure events, and polling retry emission.
- Added observer regression tests for update and scene handler lifecycle events and `PolicyCheckedEvent` emission.
- Added regression tests proving that `edited_message` and `edited_channel_post` stay out of command routing and continue through generic update handlers.
- Added callback-routing regression tests for both chat-message and inline-message callback targets, including `CallbackQueryId`, `CallbackMsgId`, `InlineMsgId`, and payload-argument guarantees.
- Added regression tests for the new error-visibility model in both message and callback flows, including silent internal-only errors and explicit user-visible callback replies.
## v1.0.0-rc.12
### Added
- `AnswerLong(...)`, `AnswerLongf(...)`, `KeyboardLong(...)`, and `SplitMessageText(...)` for explicit plain-text splitting of long replies without changing the semantics of existing single-message helpers.
- Centralized library-level validation errors in `errors.go`, including `ErrEmptyMessage`, `ErrMessageTooLong`, `ErrCaptionTooLong`, and context/target validation sentinels.
- `Bot.GetPayloadType()`, `InlineKeyboard.GetPayloadType()`, and optional strict payload decoding via `BotOpts.StrictPayloadType` / `Bot.SetStrictPayloadType(...)`.
- `MsgContext.BindArgs(...)` for binding positional command arguments into exported struct fields.
- Binding sentinels `ErrBindArgsTargetNotPointer`, `ErrBindArgsTargetNotStruct`, `ErrBindArgsUnsupportedFieldType`, and `ErrBindArgsConversion`.
- Work-in-progress scene/session support, including plugin scene registration, scoped scene sessions, scene entry/exit APIs on `MsgContext`, default in-memory session storage, scene-local routing before normal command handling, and state helpers on `SceneContext`.
### Changed
- `CommandExecutor` now returns `error`, and command, payload, and non-command update handlers now use centralized bot error handling for returned errors.
- README and README_RU examples now use the new handler signature and document the long-message helpers.
- README and README_RU now link to the project wiki, and the wiki now includes a page-priority tracker while content is being filled in.
- README and README_RU now document scenes, session scopes, scene state helpers, and `SceneActionPass` semantics.
- `TODO.md` and the framework backlog pages now group the remaining framework work into explicit priority 1, 2, and 3 buckets.
- Payload-type comments and docs now distinguish between the bot's default payload type and keyboard-local overrides.
- Scene runtime sentinel errors now have explicit godoc comments.
- Public scene structs now document their exported fields more explicitly.
- `MsgContext.Context()` now safely falls back to `context.Background()` when no request-scoped context is attached.
- `MsgContext` reply, edit, callback, delete, action, and draft-limiter paths now use the context accessor instead of reaching into raw internal state.
- Version constants were bumped to `v1.0.0-rc.12`.
### Fixed
- Message and caption validation now runs before Telegram API calls, rejecting empty messages, oversized message text, and oversized captions with stable sentinel errors.
- Draft flushing and draft updates now reject oversized messages before sending invalid requests.
- Callback payload decoding now optionally enforces strict type matching, while the default tolerant mode logs Base64-to-JSON decoding in debug mode and still accepts keyboard-local payload overrides.
- Positional argument binding now leaves missing trailing struct fields at zero values, joins the remaining arguments into the final string field, and returns clearer binding errors.
- Request-scoped contexts are now created per update handler execution and safely reused through `MsgContext.Context()` even for manually constructed test contexts.
- Command and payload handlers now have regression coverage for end-to-end typed argument binding through the normal routing path.
### Breaking Changes
- `CommandExecutor[T]` changed from `func(ctx *MsgContext, db T)` to `func(ctx *MsgContext, db T) error`.
- `Plugin.NewCommand(...)`, `Plugin.NewPayload(...)`, and `Plugin.AddUpdateHandler(...)` now require handlers with the new error-returning signature.
### Tests
- Added regression tests for `MsgContext.BindArgs(...)`, including scalar conversion, tail-string binding, zero-value trailing fields, invalid targets, unsupported field types, and end-to-end command/payload binding.
- Added scene regression tests for runtime guards, scene-local command handling, and `SceneActionPass` preserving session state.
- Added scene regression tests for message fallback handling, user-scoped session lookup without `Msg`, and custom `SessionStore` error propagation.
## v1.0.0-rc.11
### Fixed
- `chat_boost` update decoding now accepts string `boost_id` values, matching the current Telegram Bot API schema and preventing polling failures on boosted-chat updates.
## v1.0.0-rc.10
### Added
- `Plugin.AddUpdateHandler` for routing non-command Telegram updates by `tgapi.UpdateType`.
- Derived `tgapi.Update.Type` assignment during JSON decoding, plus `tgapi.UpdateTypeUnknown` for unmatched payloads.
- `tgapi.API.OpenFileByLink(...)` and `OpenFileByLinkWithContext(...)` for streaming downloads from Telegram's file server.
- Regression tests for update dispatch, keyboard builders, localization fallback, runners, rate limiting, parse mode encoding, streaming downloads, and context isolation.
- Regression tests for bot single-run enforcement, nil plugin registration, `L10n` concurrent access, `API.Close()` idle-connection cleanup, and `tgapi` worker-pool edge cases.
- `SEMVER.md` documenting versioning expectations for the project.
### Changed
- `NewBot` now returns `(*Bot[T], error)` instead of terminating the host process on configuration or startup failures.
- `Run` and `RunWithContext` now return errors; `RunWithContext` returns `ErrNoPrefixes` and `ErrNoPlugins` for invalid bot configuration.
- Polling retries now use exponential backoff instead of busy-looping on repeated `getUpdates` failures.
- `Bot` is now explicitly single-use; repeated `Run()` or `RunWithContext(...)` calls return `ErrBotAlreadyRun`.
- Database context wiring now uses `T` consistently instead of forcing `*T`; shared dependencies should typically use pointer types such as `*sql.DB`.
- `DatabaseContext`, `GetDBContext`, and `DbLogger` were updated to the new `T`-based dependency model.
- `DatabaseContext(...)` now warns once when `T` is a value type, to highlight likely unintended copying of shared dependencies.
- `AddDatabaseLoggerWriter(...)` now skips unset and nil database contexts instead of calling the writer with invalid values.
- `L10n` is now safe for concurrent use and copies added dictionary entries to avoid external mutation after registration.
- Plugin registration now snapshots commands, payloads, middlewares, and update handlers so later mutations of the original `*Plugin` do not leak into the bot.
- `AddPlugins(...)` now skips nil plugin pointers instead of panicking.
- `GetUpdateTypes()` now returns a copy instead of exposing internal slice state.
- Update handling now normalizes `MsgContext` for more Telegram update kinds and routes plugin-level update handlers with isolated context copies.
- `message`, `channel_post`, and `callback_query` remain on the command/payload flow; non-command updates can be handled through plugin update handlers.
- Command auto-generation now validates Telegram command names with the correct character set and `1..32` length limit, and emits commands in deterministic sorted order.
- Builder-style APIs were normalized to value returns for `NewCommandArg`, `NewMiddleware`, `NewRunner`, and `NewCallbackData`.
- `MenuButton` replaced `BaseMenuButton`, and `GetChatMenuButton(...)` now returns the renamed type.
- Several Telegram DTOs were tightened for optionality and serialization correctness, including `InputPaidMedia`, `MenuButton`, optional gift fields, and message entity slices.
- `tgapi.NewRequest(...)`, `NewRequestWithChatID(...)`, `NewUploaderRequest(...)`, and `NewUploaderRequestWithChatID(...)` are now documented as low-level unsafe escape hatches rather than internal helpers.
- `tgapi.API.Close()` now closes idle HTTP connections before releasing logger resources.
- Multipart form encoding now writes scalar field bytes directly instead of converting through temporary strings.
- README, README_RU, package docs, and exported godoc were updated to match the current APIs and concurrency/lifecycle model.
- Version constants were bumped to `v1.0.0-rc.10`.
### Fixed
- Required command arguments are now enforced by declared argument index, not only by total required count.
- `ParseNone` now omits `parse_mode` from JSON requests instead of serializing `"None"`.
- Upload file type detection is now case-insensitive for file extensions.
- Draft creation no longer panics when no limiter is configured, and draft flushing now rejects zero chat IDs before sending invalid requests.
- Channel posts with `SenderChat` no longer panic in the command path and now preserve the expected `MsgContext` fields.
- File logger initialization now falls back to stdout loggers instead of terminating the process on logger setup failures.
- `GetChatMenuButton` and `SetChatMenuButton` now serialize `chat_id` correctly when omitted.
- Update decoding tests now match the canonical `deleted_business_messages` model and no longer rely on the removed singular alias.
### Breaking Changes
- `NewBot[T](opts)` now returns `(*Bot[T], error)`.
- `Run()` now returns `error`.
- `RunWithContext(ctx)` now returns `error`.
- `Run()` and `RunWithContext(ctx)` are now single-use per bot instance; create a new `Bot` after they return.
- Database context handlers now receive `T` instead of `*T`. For shared dependencies, instantiate the bot with a pointer type, for example `Bot[*sql.DB]`.
- `DatabaseContext(...)` now takes `T` instead of `*T`.
- `GetDBContext()` now returns `T` instead of `*T`.
- `DbLogger[T]` now receives `T` instead of `*T`.
- `NewCommandArg(...)`, `NewMiddleware(...)`, `NewRunner(...)`, and `NewCallbackData(...)` now return values instead of pointers.
- `BaseMenuButton` was renamed to `MenuButton`, and `GetChatMenuButton(...)` now returns `MenuButton`.
- `tgapi.Update` no longer exposes the deprecated `DeletedBusinessMessage` alias; use `DeletedBusinessMessages`.
### Tests
- Added coverage for polling backoff helpers, command sorting, database logger safety checks, update handler routing, update-context isolation, channel posts with `SenderChat`, parse mode encoding, streaming downloads, and rate limiter behavior.
## v1.0.0-rc.7
### Added
- Package-level logger helpers: `utils.CreateLogger(prefix, level)` and `utils.CreateFileLogger(prefix, level, filePath)`.
- `MsgContext.Logger`, populated from the matched plugin and falling back to the bot logger.
- Plugin lifecycle/configuration APIs: `SetLogger`, `RemoveLogger`, `SetOnClose`, and `Close`.
- `Bot.CloseRemote(ctx)` as the explicit wrapper for Telegram Bot API close.
### Changed
- Logger initialization is now unified across `Bot`, `tgapi.API`, and `tgapi.Uploader`.
- `Bot.Close()` now performs local resource teardown only and invokes `Plugin.Close()` for registered plugins.
- Local `tgapi.API` shutdown was renamed to `Close()`.
- Telegram Bot API close wrappers in `tgapi.API` were renamed to `CloseRemote()` and `CloseRemoteWithContext()`.
- `Bot.Debug()` now updates log levels for the bot logger, request logger, and already registered plugin loggers.
- `Bot.AddPlugins()` now creates a default plugin logger automatically when one is not provided.
- `Bot.AddDatabaseLoggerWriter()` now also attaches the writer to already registered plugin loggers.
- GoDoc was expanded for the new shutdown and logging APIs, and plugin registration is now documented as a configuration commit point.
### Breaking Changes
- `(*Bot).Close(ctx context.Context)` was replaced with `(*Bot).Close()`.
- `(*tgapi.API).CloseApi()` was renamed to `(*tgapi.API).Close()`.
- `(*tgapi.API).Close()` was renamed to `(*tgapi.API).CloseRemote()`.
- `(*tgapi.API).CloseWithContext()` was renamed to `(*tgapi.API).CloseRemoteWithContext(ctx)`.
### Migration
- Replace `bot.Close(ctx)` with `bot.Close()`.
- If you need Telegram Bot API close, use `bot.CloseRemote(ctx)`.
- Replace `api.CloseApi()` with `api.Close()`.
- Replace `api.Close()` with `api.CloseRemote()`.
- Replace `api.CloseWithContext(ctx)` with `api.CloseRemoteWithContext(ctx)`.
- Configure plugin loggers and `OnClose` hooks before calling `bot.AddPlugins(...)`.
### Tests
- Updated tests for the new shutdown and logging behavior.
### Notes
- Registering a plugin via `AddPlugins(...)` is a configuration commit point; the plugin should not be mutated through the original `*Plugin` afterward.
- If plugin loggers must receive a database writer, call `AddDatabaseLoggerWriter(...)` after registering plugins.
## v1.0.0-rc.4
### Added
- `WithContext` variants across `tgapi` API and uploader methods so callers can pass cancellation and deadline contexts consistently.
- `UploaderCertificateType`, `UploadSetWebhookP`, `Uploader.SetWebhook(...)`, and `Uploader.SetWebhookWithContext(...)` for multipart webhook certificate uploads.
- Missing media thumbnail fields where applicable.
### Changed
- GoDoc for context-aware methods was improved, and `See` references now point to method-specific Telegram Bot API anchors.
- `EditMessageTextP` now includes `entities` and `link_preview_options`.
- `EditMessageCaptionP` now includes `caption_entities` and `show_caption_above_media`.
- `StopPollP` now uses `reply_markup` and no longer carries `inline_message_id`.
- `SendStickerP` now includes reply and suggested-post related fields.
- `SendDocumentP` now includes `disable_content_type_detection`.
- `SendInvoiceP` no longer includes unsupported `business_connection_id`.
- `SetWebhookP` no longer carries `certificate`; GoDoc now points to uploader-based certificate upload.
- Existing non-context methods remain available, and the `Do(...)` call style is preserved.
### Breaking Changes
- Users sending webhook certificates through JSON `SetWebhookP.Certificate` must migrate to `Uploader.SetWebhook(...)`.
## v1.0.0-rc.3
### Fixed
- The update polling loop no longer logs or retries after `context.Canceled` during shutdown.
- Extra retry delay was removed from canceled polling requests so `RunWithContext` can exit immediately while stopping.
### Changed
- Shutdown behavior remains explicit: callers are still responsible for invoking `Close()` after `RunWithContext` returns.
## v1.0.0-rc.2
### Fixed
- Fixed a shutdown crash caused by `DatabaseWriter` calling `Close()` through an uninitialized embedded logger writer.
- Fixed bot shutdown hanging during Telegram long polling by making update polling use a cancelable context.
- Reduced the chance of container termination with exit code `137` during shutdown by allowing `getUpdates` to stop promptly on cancellation.
### Changed
- Switched the project to use the local `laniakea` replacement for the shutdown fix.
- Documentation now clarifies that `RunWithContext` does not close resources automatically and callers must invoke `Close()` explicitly.
- `Updates` documentation now describes context-driven cancellation behavior.
### Tests
- Added regression tests for database logger writer shutdown behavior.
+118 -31
View File
@@ -4,12 +4,14 @@
[![Go Version](https://img.shields.io/badge/Go-1.24+-00ADD8?logo=go&style=flat-square)](https://go.dev/) [![Go Version](https://img.shields.io/badge/Go-1.24+-00ADD8?logo=go&style=flat-square)](https://go.dev/)
[![License: GPL-3.0](https://img.shields.io/badge/License-GPL%203.0-blue.svg?style=flat-square)](LICENSE) [![License: GPL-3.0](https://img.shields.io/badge/License-GPL%203.0-blue.svg?style=flat-square)](LICENSE)
![Gitea Release](https://img.shields.io/gitea/v/release/ScuroNeko/Laniakea?gitea_url=https%3A%2F%2Fgit.nix13.pw&sort=semver&display_name=release&style=flat-square&color=purple&link=https%3A%2F%2Fgit.nix13.pw%2FScuroNeko%2FLaniakea%2Freleases) ![Gitea Release](https://img.shields.io/gitea/v/release/ScuroNeko/Laniakea?gitea_url=https%3A%2F%2Fgit.scuroneko.dev&sort=semver&display_name=release&style=flat-square&color=purple&link=https%3A%2F%2Fgit.scuroneko.dev%2FScuroNeko%2FLaniakea%2Freleases)
A lightweight, easy-to-use, and performant Telegram Bot API wrapper for Go. It simplifies bot development with a clean plugin system, middleware support, automatic command generation, and built-in rate limiting. A lightweight, easy-to-use, and performant Telegram Bot API wrapper for Go. It simplifies bot development with a clean plugin system, middleware support, automatic command generation, and built-in rate limiting.
[На русском](README_RU.md) [На русском](README_RU.md)
[Wiki](https://git.scuroneko.dev/ScuroNeko/Laniakea/wiki)
--- ---
## ✨ Features ## ✨ Features
@@ -19,15 +21,16 @@ A lightweight, easy-to-use, and performant Telegram Bot API wrapper for Go. It s
* **Middleware Support:** Run code before or after commands (e.g., logging, access control). * **Middleware Support:** Run code before or after commands (e.g., logging, access control).
* **Automatic Command Generation:** Generate help and command lists automatically. * **Automatic Command Generation:** Generate help and command lists automatically.
* **Built-in Rate Limiting:** Protect your bot from hitting Telegram API limits (supports `retry_after` handling). * **Built-in Rate Limiting:** Protect your bot from hitting Telegram API limits (supports `retry_after` handling).
* **Context-Aware:** Pass custom database or state contexts to your handlers. * **Context-Aware:** Pass custom application data or state contexts to your handlers.
* **Fluent Interface:** Chain methods for clean configuration (e.g., `bot.ErrorTemplate(...).AddPlugins(...)`). * **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(...)`.
--- ---
## 📦 Installation ## 📦 Installation
```bash ```bash
go get git.nix13.pw/scuroneko/laniakea go get git.scuroneko.dev/scuroneko/laniakea
``` ```
or or
@@ -45,17 +48,18 @@ package main
import ( import (
"log" "log"
"git.nix13.pw/scuroneko/laniakea" // Import the Laniakea library "git.scuroneko.dev/scuroneko/laniakea" // Import the Laniakea library
) )
// echo is a command handler function. // echo is a command handler function.
// It receives two parameters: // It receives two parameters:
// - ctx: the message context (contains info about the message, sender, chat, etc.) // - ctx: the message context (contains info about the message, sender, chat, etc.)
// - db: your custom database context (here we use NoDB, a placeholder for no database) // - data: your shared application data (here we use NoData, a placeholder for no shared data)
func echo(ctx *laniakea.MsgContext, db *laniakea.NoDB) { func echo(ctx *laniakea.MsgContext, data laniakea.NoData) error {
// Answer the user with the text they sent, without any command prefix. // 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.Text contains the user's message with the command part stripped off.
ctx.Answer(ctx.Text) // User input WITHOUT command ctx.Answer(ctx.Text) // User input WITHOUT command
return nil
} }
func main() { func main() {
@@ -63,14 +67,17 @@ func main() {
opts := &laniakea.BotOpts{Token: "TOKEN"} opts := &laniakea.BotOpts{Token: "TOKEN"}
// 2. Initialize a new bot instance. // 2. Initialize a new bot instance.
// We use laniakea.NoDB as the database context type (no database needed for this example). // We use laniakea.NoData as the application data type (no shared data needed for this example).
bot := laniakea.NewBot[laniakea.NoDB](opts) bot, err := laniakea.NewBot[laniakea.NoData](opts)
if err != nil {
log.Fatal(err)
}
// Ensure bot resources are cleaned up on exit. // Ensure bot resources are cleaned up on exit.
defer bot.Close() defer bot.Close()
// 3. Create a new plugin named "ping". // 3. Create a new plugin named "ping".
// Plugins help group related commands and middlewares. // Plugins help group related commands and middlewares.
p := laniakea.NewPlugin[laniakea.NoDB]("ping") p := laniakea.NewPlugin[laniakea.NoData]("ping")
// 4. Add a command to the plugin. // 4. Add a command to the plugin.
// p.NewCommand(echo, "echo") creates a command that triggers the 'echo' function on the "/echo" command. // p.NewCommand(echo, "echo") creates a command that triggers the 'echo' function on the "/echo" command.
@@ -78,14 +85,15 @@ func main() {
// 5. Add another command using an anonymous function (closure). // 5. Add another command using an anonymous function (closure).
// This command simply replies "Pong" when the user sends "/ping". // This command simply replies "Pong" when the user sends "/ping".
p.AddCommand(p.NewCommand(func(ctx *laniakea.MsgContext, db *laniakea.NoDB) { p.AddCommand(p.NewCommand(func(ctx *laniakea.MsgContext, data laniakea.NoData) error {
ctx.Answer("Pong") ctx.Answer("Pong")
return nil
}, "ping")) }, "ping"))
// 6. Configure the bot with a custom error template and add the plugin. // 6. Configure the bot with a custom error template and add the plugin.
// ErrorTemplate sets a format string for errors (where %s will be replaced by the actual error). // SetErrorTemplate sets a format string for errors (where %s will be replaced by the actual error).
// AddPlugins(p) registers our "ping" plugin with the bot. // AddPlugins(p) registers our "ping" plugin with the bot.
bot = bot.ErrorTemplate("Error\n\n%s").AddPlugins(p) bot = bot.SetErrorTemplate("Error\n\n%s").AddPlugins(p)
// 7. Automatically generate commands like /start, /help, and a list of all registered commands. // 7. Automatically generate commands like /start, /help, and a list of all registered commands.
// This is optional but very useful for most bots. // This is optional but very useful for most bots.
@@ -94,26 +102,48 @@ func main() {
} }
// 8. Start the bot, listening for updates (long polling). // 8. Start the bot, listening for updates (long polling).
bot.Run() if err := bot.Run(); err != nil {
log.Fatal(err)
}
} }
``` ```
### How It Works ### How It Works
1. `BotOpts`: Holds configuration like the API token. 1. `BotOpts`: Holds configuration like the API token.
2. `NewBot[T]`: Creates a bot instance. The type parameter T allows you to pass a custom database context (e.g., *sql.DB) that will be available in all handlers. Use laniakea.NoDB if you don't need it. 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. 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)), the second is the command name (without the slash). 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 database context T. 5. **Handler Functions**: Receive *MsgContext (message details, methods like Answer) and your custom application data T, and return an error for centralized error handling.
6. `ErrorTemplate`: Sets a template for error messages. The %s placeholder is replaced by the actual error. 6. `SetErrorTemplate`: Sets a template for error messages. The %s placeholder is replaced by the actual error.
7. `AutoGenerateCommands`: Adds built-in commands (/start, /help) and a command that lists all available commands. 7. `AutoGenerateCommands`: Registers plugin-defined commands with Telegram across the supported scopes.
8. `Run()`: Starts the bot's update polling loop. 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.
## Webhook Runtime
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.
- You want webhook-delivered updates to reuse the same internal queue, worker pool, runners, and single-use lifecycle as polling.
- 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.
See the full guide in the wiki: [Webhook Runtime](https://git.scuroneko.dev/ScuroNeko/Laniakea/wiki/Webhook-Runtime)
## 📖 Core Concepts ## 📖 Core Concepts
### Plugins ### Plugins
Plugins are the main way to organize code. A plugin can have multiple commands and middlewares. Plugins are the main way to organize code. A plugin can have multiple commands and middlewares.
```go ```go
plugin := laniakea.NewPlugin[MyDB]("admin") plugin := laniakea.NewPlugin[*MyDB]("admin")
plugin.AddCommand(plugin.NewCommand(banUser, "ban")) plugin.AddCommand(plugin.NewCommand(banUser, "ban"))
bot.AddPlugins(plugin) bot.AddPlugins(plugin)
``` ```
@@ -122,9 +152,10 @@ bot.AddPlugins(plugin)
A command is a function that handles a specific bot command (e.g., /start). A command is a function that handles a specific bot command (e.g., /start).
```go ```go
func myHandler(ctx *laniakea.MsgContext, db *MyDB) { func myHandler(ctx *laniakea.MsgContext, db *MyDB) error {
// Access command arguments via ctx.Args ([]string) // Access command arguments via ctx.Args ([]string)
// Reply to the user: ctx.Answer("some text") // Reply to the user: ctx.Answer("some text")
return nil
} }
``` ```
@@ -133,8 +164,10 @@ func myHandler(ctx *laniakea.MsgContext, db *MyDB) {
Provides access to the incoming message and useful reply methods: Provides access to the incoming message and useful reply methods:
- `Answer(text string) *AnswerMessage`: Sends a message with parse_mode none. - `Answer(text string) *AnswerMessage`: Sends a message with parse_mode none.
- `AnswerLong(text string) []*AnswerMessage`: Splits long plain text into multiple messages.
- `AnswerMarkdown(text string) *AnswerMessage`: Sends a message formatted with MarkdownV2 (you handle escaping). - `AnswerMarkdown(text string) *AnswerMessage`: Sends a message formatted with MarkdownV2 (you handle escaping).
- `Keyboard(text string, keyboard *InlineKeyboard) *AnswerMessage`: Sends a message with parse_mode none and inline keyboard. - `Keyboard(text string, keyboard *InlineKeyboard) *AnswerMessage`: Sends a message with parse_mode none and inline keyboard.
- `KeyboardLong(text string, keyboard *InlineKeyboard) []*AnswerMessage`: Splits long plain text into multiple messages and attaches the keyboard to the final chunk.
- `KeyboardMarkdown(text string, keyboard *InlineKeyboard) *AnswerMessage`: Sends a message formatted with MarkdownV2 (you handle escaping) and inline keyboard. - `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. - `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). - `AnswerPhotoMarkdown(photoId, text string) *AnswerMessage`: Sends a photo with MarkdownV2 caption (you handle escaping).
@@ -153,16 +186,59 @@ Provides access to the incoming message and useful reply methods:
This split keeps method intent explicit: JSON-only calls go through `API`, file uploads go through `Uploader`. This split keeps method intent explicit: JSON-only calls go through `API`, file uploads go through `Uploader`.
### Database Context For advanced cases, `tgapi.NewRequest(...)` and `tgapi.NewUploaderRequest(...)` remain public as low-level escape hatches. They are intentionally less safe than method-specific helpers: callers must supply the correct Telegram method name and compatible request/response types themselves.
The `T` in `NewBot[T]` is a powerful feature. You can pass any type (like a database connection pool), and it will be available in every command and middleware handler. ### App Data
The `T` in `NewBot[T]` is a powerful feature. You can pass any type, but shared dependencies such as database pools, service containers, or API clients should usually use a pointer type.
```go ```go
type MyDB struct { /* ... */ } type MyDB struct { /* ... */ }
db := &MyDB{...} db := &MyDB{...}
bot := laniakea.NewBot[*MyDB](opts, db) // Pass db instance bot, err := laniakea.NewBot[*MyDB](opts)
if err != nil {
log.Fatal(err)
}
bot.SetAppData(db)
``` ```
### Scenes and Sessions
Scenes model multi-step conversations inside a plugin. Each active scene is stored in a session keyed by scope, so you can isolate flows per user, per chat, or per user-chat pair.
```go
plugin := laniakea.NewPlugin[MyDB]("signup")
plugin.NewScene("signup").
SetScope(laniakea.SceneScopeUserChat).
SetEntry("ask_name").
OnStep("ask_name", func(ctx *laniakea.SceneContext, db MyDB) (laniakea.SceneResult, error) {
if ctx.Text == "" {
ctx.Answer("What is your name?")
return ctx.Stay(), nil
}
if err := ctx.SaveData(struct {
Name string `json:"name"`
}{Name: ctx.Text}); err != nil {
return laniakea.SceneResult{}, err
}
ctx.Answer("Nice to meet you.")
return ctx.Next("done"), nil
}).
OnStep("done", func(ctx *laniakea.SceneContext, db MyDB) (laniakea.SceneResult, error) {
return ctx.Exit(), nil
})
```
- Use `ctx.EnterScene("signup")` to enter the configured entry step.
- Use `ctx.EnterSceneStep("signup", "done")` when you need an explicit starting step.
- Return `ctx.Stay()`, `ctx.Next(step)`, `ctx.Exit()`, or `ctx.Pass()` from scene handlers to control flow.
- `SceneActionPass` keeps the current session unchanged and continues normal bot routing.
- Use `SceneContext.SaveData(...)` and `SceneContext.BindData(...)` for JSON session state.
- Use `SceneScopeUser`, `SceneScopeChat`, or `SceneScopeUserChat` depending on how widely a conversation should be shared.
## 🧩 Middleware ## 🧩 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. 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.
@@ -177,11 +253,12 @@ func(ctx *MsgContext, db T) bool
- If it returns false, the execution chain stops immediately (the command will not run). - If it returns false, the execution chain stops immediately (the command will not run).
### Adding Middleware ### Adding Middleware
Use the Use method of a plugin to add one or more middleware functions. They are executed in the order they are added. Use `AddMiddleware` on a plugin to add one or more shared middleware functions. They are executed in the order they are added.
```go ```go
plugin := laniakea.NewPlugin[MyDB]("admin") plugin := laniakea.NewPlugin[*MyDB]("admin")
plugin.Use(loggingMiddleware, adminOnlyMiddleware) plugin.AddMiddleware(laniakea.NewMiddleware("logging", loggingMiddleware))
plugin.AddMiddleware(laniakea.NewMiddleware("admin-only", adminOnlyMiddleware))
plugin.AddCommand(plugin.NewCommand(banUser, "ban")) plugin.AddCommand(plugin.NewCommand(banUser, "ban"))
``` ```
@@ -210,16 +287,26 @@ func adminOnlyMiddleware(ctx *laniakea.MsgContext, db *MyDB) bool {
- Middleware can modify the MsgContext (e.g., add custom fields) before the command runs. - Middleware can modify the MsgContext (e.g., add custom fields) before the command runs.
## ⚙️ Advanced Configuration ## ⚙️ Advanced Configuration
- **Inline Keyboards**: Build keyboards using `laniakea.NewInlineKeyboardJson`, `laniakea.NewInlineKeyboardBase64`, or `laniakea.NewInlineKeyboard`. - **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. - **Rate Limiting**: Pass a configured utils.RateLimiter via BotOpts to handle Telegram's rate limits gracefully.
- **Custom HTTP Client**: Provide your own http.Client in BotOpts for fine-tuned control. - **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.
## Telegram Update Handling
- Commands and payloads are handled through plugins.
- Non-command updates can be routed with `plugin.AddUpdateHandler(updateType, handler)`.
- `message`, `channel_post`, and `callback_query` stay on the command/payload flow.
- `tgapi.Update` exposes a derived `Type` field after JSON unmarshalling so handlers can inspect the effective update kind directly.
## 📝 License ## 📝 License
This project is licensed under the GNU General Public License v3.0 — see the [LICENSE](LICENSE) file for details. This project is licensed under the GNU General Public License v3.0 — see the [LICENSE](LICENSE) file for details.
## 📚 Learn More ## 📚 Learn More
[GoDoc](https://pkg.go.dev/git.nix13.pw/scuroneko/laniakea) [GoDoc](https://pkg.go.dev/git.scuroneko.dev/scuroneko/laniakea)
[Wiki](https://git.scuroneko.dev/ScuroNeko/Laniakea/wiki)
[Telegram Bot API](https://core.telegram.org/bots/api) [Telegram Bot API](https://core.telegram.org/bots/api)
+130 -36
View File
@@ -4,12 +4,14 @@
[![Go Version](https://img.shields.io/badge/Go-1.24+-00ADD8?logo=go&style=flat-square)](https://go.dev/) [![Go Version](https://img.shields.io/badge/Go-1.24+-00ADD8?logo=go&style=flat-square)](https://go.dev/)
[![License: GPL-3.0](https://img.shields.io/badge/License-GPL%203.0-blue.svg?style=flat-square)](LICENSE) [![License: GPL-3.0](https://img.shields.io/badge/License-GPL%203.0-blue.svg?style=flat-square)](LICENSE)
![Gitea Release](https://img.shields.io/gitea/v/release/ScuroNeko/Laniakea?gitea_url=https%3A%2F%2Fgit.nix13.pw&sort=semver&display_name=release&style=flat-square&color=purple&link=https%3A%2F%2Fgit.nix13.pw%2FScuroNeko%2FLaniakea%2Freleases) ![Gitea Release](https://img.shields.io/gitea/v/release/ScuroNeko/Laniakea?gitea_url=https%3A%2F%2Fgit.scuroneko.dev&sort=semver&display_name=release&style=flat-square&color=purple&link=https%3A%2F%2Fgit.scuroneko.dev%2FScuroNeko%2FLaniakea%2Freleases)
Легковесная, простая в использовании и производительная обёртка для Telegram Bot API на Go. Она упрощает разработку ботов благодаря чистой системе плагинов, поддержке Middleware, автоматической генерации команд и встроенному рейтлимитеру. Легковесная, простая в использовании и производительная обёртка для Telegram Bot API на Go. Она упрощает разработку ботов благодаря чистой системе плагинов, поддержке Middleware, автоматической генерации команд и встроенному рейтлимитеру.
[English](README.md) [English](README.md)
[Wiki](https://git.scuroneko.dev/ScuroNeko/Laniakea/wiki)
--- ---
## ✨ Возможности ## ✨ Возможности
@@ -20,15 +22,16 @@
* **Поддержка промежуточных слоёв (Middleware):** Выполняйте код до или после команд (например, логирование, проверка доступа). * **Поддержка промежуточных слоёв (Middleware):** Выполняйте код до или после команд (например, логирование, проверка доступа).
* **Автоматическая генерация команд:** Генерируйте справку и списки команд автоматически. * **Автоматическая генерация команд:** Генерируйте справку и списки команд автоматически.
* **Встроенный ограничитель запросов (Rate Limiter):** Защитите бота от превышения лимитов Telegram API (с обработкой `retry_after`). * **Встроенный ограничитель запросов (Rate Limiter):** Защитите бота от превышения лимитов Telegram API (с обработкой `retry_after`).
* **Контекст данных:** Передавайте свой контекст базы данных или состояния в обработчики. * **Контекст данных:** Передавайте общие данные приложения или state в обработчики.
* **Текучий интерфейс (Fluent Interface):** Стройте цепочки методов для чистой конфигурации (например, `bot.ErrorTemplate(...).AddPlugins(...)`). * **Настраиваемый API:** Комбинируйте `Set...` и `Add...` helper-методы для понятной конфигурации, например `bot.SetErrorTemplate(...).AddPlugins(...)`.
* **Polling и Webhook Runtime:** Запускайте бота через long polling с `Run()` / `RunWithContext(...)` или через webhook server, которым владеет сам бот, с `RunWebHookWithContext(...)`.
--- ---
## 📦 Установка ## 📦 Установка
```bash ```bash
go get git.nix13.pw/scuroneko/laniakea go get git.scuroneko.dev/scuroneko/laniakea
``` ```
или или
@@ -46,17 +49,18 @@ package main
import ( import (
"log" "log"
"git.nix13.pw/scuroneko/laniakea" // Импортируем библиотеку Laniakea "git.scuroneko.dev/scuroneko/laniakea" // Импортируем библиотеку Laniakea
) )
// echo — это функция-обработчик команды. // echo — это функция-обработчик команды.
// Она получает два параметра: // Она получает два параметра:
// - ctx: контекст сообщения (содержит информацию о сообщении, отправителе, чате и т.д.) // - ctx: контекст сообщения (содержит информацию о сообщении, отправителе, чате и т.д.)
// - db: ваш пользовательский контекст базы данных (здесь мы используем NoDB — заглушку) // - data: ваши общие данные приложения (здесь мы используем NoData — заглушку без общих зависимостей)
func echo(ctx *laniakea.MsgContext, db *laniakea.NoDB) { func echo(ctx *laniakea.MsgContext, data laniakea.NoData) error {
// Отвечаем пользователю текстом, который он прислал, без префикса команды. // Отвечаем пользователю текстом, который он прислал, без префикса команды.
// ctx.Text содержит сообщение пользователя, из которого удалена часть с командой. // ctx.Text содержит сообщение пользователя, из которого удалена часть с командой.
ctx.Answer(ctx.Text) // Ввод пользователя БЕЗ команды ctx.Answer(ctx.Text) // Ввод пользователя БЕЗ команды
return nil
} }
func main() { func main() {
@@ -64,14 +68,17 @@ func main() {
opts := &laniakea.BotOpts{Token: "TOKEN"} opts := &laniakea.BotOpts{Token: "TOKEN"}
// 2. Инициализируем новый экземпляр бота. // 2. Инициализируем новый экземпляр бота.
// Используем laniakea.NoDB как тип контекста базы данных (база не нужна для примера). // Используем laniakea.NoData как тип данных приложения (общие зависимости не нужны для примера).
bot := laniakea.NewBot[laniakea.NoDB](opts) bot, err := laniakea.NewBot[laniakea.NoData](opts)
if err != nil {
log.Fatal(err)
}
// Гарантируем освобождение ресурсов бота при выходе. // Гарантируем освобождение ресурсов бота при выходе.
defer bot.Close() defer bot.Close()
// 3. Создаём новый плагин с именем "ping". // 3. Создаём новый плагин с именем "ping".
// Плагины помогают группировать связанные команды и промежуточные обработчики. // Плагины помогают группировать связанные команды и промежуточные обработчики.
p := laniakea.NewPlugin[laniakea.NoDB]("ping") p := laniakea.NewPlugin[laniakea.NoData]("ping")
// 4. Добавляем команду в плагин. // 4. Добавляем команду в плагин.
// p.NewCommand(echo, "echo") создаёт команду, которая вызывает функцию 'echo' по команде "/echo". // p.NewCommand(echo, "echo") создаёт команду, которая вызывает функцию 'echo' по команде "/echo".
@@ -79,14 +86,15 @@ func main() {
// 5. Добавляем ещё одну команду, используя анонимную функцию (замыкание). // 5. Добавляем ещё одну команду, используя анонимную функцию (замыкание).
// Эта команда просто отвечает "Pong", когда пользователь отправляет "/ping". // Эта команда просто отвечает "Pong", когда пользователь отправляет "/ping".
p.AddCommand(p.NewCommand(func(ctx *laniakea.MsgContext, db *laniakea.NoDB) { p.AddCommand(p.NewCommand(func(ctx *laniakea.MsgContext, data laniakea.NoData) error {
ctx.Answer("Pong") ctx.Answer("Pong")
return nil
}, "ping")) }, "ping"))
// 6. Настраиваем бота: задаём шаблон ошибки и добавляем плагин. // 6. Настраиваем бота: задаём шаблон ошибки и добавляем плагин.
// ErrorTemplate устанавливает формат для сообщений об ошибках (где %s будет заменён на текст ошибки). // SetErrorTemplate устанавливает формат для сообщений об ошибках (где %s будет заменён на текст ошибки).
// AddPlugins(p) регистрирует наш плагин "ping" в боте. // AddPlugins(p) регистрирует наш плагин "ping" в боте.
bot = bot.ErrorTemplate("Ошибка\n\n%s").AddPlugins(p) bot = bot.SetErrorTemplate("Ошибка\n\n%s").AddPlugins(p)
// 7. Автоматически генерируем команды, такие как /start, /help и список всех зарегистрированных команд. // 7. Автоматически генерируем команды, такие как /start, /help и список всех зарегистрированных команд.
// Это необязательно, но очень полезно для большинства ботов. // Это необязательно, но очень полезно для большинства ботов.
@@ -95,26 +103,48 @@ func main() {
} }
// 8. Запускаем бота, начиная прослушивание обновлений (long polling). // 8. Запускаем бота, начиная прослушивание обновлений (long polling).
bot.Run() if err := bot.Run(); err != nil {
log.Fatal(err)
}
} }
``` ```
### Как это работает ### Как это работает
1. `BotOpts`: Содержит конфигурацию, например, токен API. 1. `BotOpts`: Содержит конфигурацию, например, токен API.
2. `NewBot[T]`: Создаёт экземпляр бота. Параметр типа T позволяет передать пользовательский контекст базы данных (например, *sql.DB), который будет доступен во всех обработчиках. Используйте laniakea.NoDB, если он не нужен. 2. `NewBot[T]`: Создаёт экземпляр бота. Параметр типа T позволяет передать общие данные приложения (например, *sql.DB или контейнер сервисов), которые будут доступны во всех обработчиках. Используйте laniakea.NoData, если они не нужны.
3. `NewPlugin`: Создаёт логическую группу для команд и Middleware. 3. `NewPlugin`: Создаёт логическую группу для команд и Middleware.
4. `AddCommand`: Регистрирует команду. Первый аргумент — функция-обработчик (func(*MsgContext, T)), второй — имя команды (без слеша). 4. `AddCommand`: Регистрирует команду. Первый аргумент — функция-обработчик (`func(*MsgContext, T) error`), второй — имя команды (без слеша).
5. **Функции-обработчики**: Получают *MsgContext (детали сообщения, методы типа Answer) и ваш контекст базы данных T. 5. **Функции-обработчики**: Получают *MsgContext (детали сообщения, методы типа Answer) и ваши данные приложения типа T, а ошибку возвращают для централизованной обработки.
6. `ErrorTemplate`: Устанавливает шаблон для сообщений об ошибках. Плейсхолдер %s заменяется на текст ошибки. 6. `SetErrorTemplate`: Устанавливает шаблон для сообщений об ошибках. Плейсхолдер %s заменяется на текст ошибки.
7. `AutoGenerateCommands`: Добавляет встроенные команды (/start, /help) и команду, показывающую список всех доступных команд. 7. `AutoGenerateCommands`: Регистрирует команды из плагинов в Telegram для поддерживаемых scope.
8. `Run()`: Запускает цикл опроса обновлений бота. 8. `Run()`: Запускает цикл опроса обновлений бота и возвращает ошибку, если старт или polling завершился неуспешно.
9. `RunWebHookWithContext(...)`: Запускает bot-owned webhook runtime, когда Telegram должен доставлять update по HTTP вместо long polling.
10. Экземпляр `Bot` одноразовый. После завершения `Run()`, `RunWithContext()` или `RunWebHookWithContext()` для следующего запуска создавайте новый бот.
## Webhook Runtime
Laniakea также поддерживает bot-owned webhook runtime через `RunWebHookWithContext(...)` и `RunWebHook(...)`.
Используй его, когда:
- Telegram должен сам отправлять update на твой HTTP endpoint вместо polling.
- Ты хочешь, чтобы webhook-update проходили через ту же внутреннюю очередь, тот же worker pool, тех же runners и тот же single-use lifecycle, что и polling.
- Ты хочешь, чтобы 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()`.
Полное руководство есть в wiki: [Webhook Runtime](https://git.scuroneko.dev/ScuroNeko/Laniakea/wiki/Webhook-Runtime-RU)
## 📖 Основные концепции ## 📖 Основные концепции
### Плагины (Plugins) ### Плагины (Plugins)
Плагины — основной способ организации кода. Плагин может содержать несколько команд и Middleware. Плагины — основной способ организации кода. Плагин может содержать несколько команд и Middleware.
```go ```go
plugin := laniakea.NewPlugin[MyDB]("admin") plugin := laniakea.NewPlugin[*MyDB]("admin")
plugin.AddCommand(plugin.NewCommand(banUser, "ban")) plugin.AddCommand(plugin.NewCommand(banUser, "ban"))
bot.AddPlugins(plugin) bot.AddPlugins(plugin)
``` ```
@@ -123,9 +153,10 @@ bot.AddPlugins(plugin)
Команда — это функция, которая обрабатывает конкретную команду бота (например, /start). Команда — это функция, которая обрабатывает конкретную команду бота (например, /start).
```go ```go
func myHandler(ctx *laniakea.MsgContext, db *MyDB) { func myHandler(ctx *laniakea.MsgContext, db *MyDB) error {
// Доступ к аргументам команды через ctx.Args ([]string) // Доступ к аргументам команды через ctx.Args ([]string)
// Ответ пользователю: ctx.Answer("какой-то текст") // Ответ пользователю: ctx.Answer("какой-то текст")
return nil
} }
``` ```
@@ -133,26 +164,78 @@ func myHandler(ctx *laniakea.MsgContext, db *MyDB) {
Предоставляет доступ к входящему сообщению и полезные методы для ответа: Предоставляет доступ к входящему сообщению и полезные методы для ответа:
- `Answer(text string)`: Отправляет сообщение с parse_mode none. - `Answer(text string)`: Отправляет сообщение с parse_mode none.
- `AnswerLong(text string) []*AnswerMessage`: Разбивает длинный plain text на несколько сообщений.
- `AnswerMarkdown(text string)`: Отправляет сообщение, отформатированное MarkdownV2 (экранирование на вашей стороне). - `AnswerMarkdown(text string)`: Отправляет сообщение, отформатированное MarkdownV2 (экранирование на вашей стороне).
- `Keyboard(text string, keyboard *InlineKeyboard) *AnswerMessage`: Отправляет сообщение с parse_mode none и Inline клавиатурой. - `Keyboard(text string, keyboard *InlineKeyboard) *AnswerMessage`: Отправляет сообщение с parse_mode none и Inline клавиатурой.
- `KeyboardLong(text string, keyboard *InlineKeyboard) []*AnswerMessage`: Разбивает длинный plain text на несколько сообщений и вешает клавиатуру на последний chunk.
- `KeyboardMarkdown(text string, keyboard *InlineKeyboard) *AnswerMessage`: Отправляет сообщение, отформатированное MarkdownV2 (экранирование на вашей стороне), и Inline клавиатурой. - `KeyboardMarkdown(text string, keyboard *InlineKeyboard) *AnswerMessage`: Отправляет сообщение, отформатированное MarkdownV2 (экранирование на вашей стороне), и Inline клавиатурой.
- `AnswerPhoto(photoId, text string) *AnswerMessage`: Отправляет фотографию с подписью и parse_mode none. - `AnswerPhoto(photoId, text string) *AnswerMessage`: Отправляет фотографию с подписью и parse_mode none.
- `AnswerPhotoMarkdown(photoId, text string) *AnswerMessage`: Отправляет фотографию с подписью, отформатированной MarkdownV2 (экранирование на вашей стороне). - `AnswerPhotoMarkdown(photoId, text string) *AnswerMessage`: Отправляет фотографию с подписью, отформатированной MarkdownV2 (экранирование на вашей стороне).
- `EditCallback(text string)`: Редактирует сообщение, форматируя его в MarkdownV2 (экранирование на вашей стороне), после нажатия Inline кнопки. - `EditCallback(text string)`: Редактирует сообщение с `parse_mode` none после нажатия inline-кнопки.
- `EditCallbackMarkdown(text string)`: Редактирует сообщение с parse_mode none после нажатия Inline кнопки. - `EditCallbackMarkdown(text string)`: Редактирует сообщение в формате MarkdownV2 (экранирование на вашей стороне) после нажатия inline-кнопки.
- `SendChatAction(action string)`: Отправляет действие "печатает", "загружает фото" и т.д. - `SendAction(action tgapi.ChatActionType)`: Отправляет действие "печатает", "загружает фото" и т.д.
- Поля: `Text`, `Args`, `From`, `Chat`, `Msg` и другие. - Поля: `Text`, `Args`, `From`, `FromID`, `Msg`, `InlineMsgId`, `CallbackQueryId` и другие.
- И много других методов и полей! - И много других методов и полей!
### Контекст базы данных (Database Context) ### App Data
Параметр типа `T` в `NewBot[T]` — мощная функция. Вы можете передать любой тип (например, пул соединений с БД), и он будет доступен в каждом обработчике команды и中间件. Параметр типа `T` в `NewBot[T]` — мощная возможность. Вы можете передать любой тип, но для разделяемых зависимостей вроде пула соединений с БД, контейнера сервисов или API-клиента обычно стоит использовать pointer type.
```go ```go
type MyDB struct { /* ... */ } type MyDB struct { /* ... */ }
db := &MyDB{...} db := &MyDB{...}
bot := laniakea.NewBot[*MyDB](opts, db) // Передаём экземпляр db bot, err := laniakea.NewBot[*MyDB](opts)
if err != nil {
log.Fatal(err)
}
bot.SetAppData(db)
``` ```
### Сцены и сессии (Scenes and Sessions)
Сцены описывают многошаговые диалоги внутри плагина. Активная сцена хранится в session state, ключ которого зависит от scope, поэтому поток можно изолировать на пользователя, на чат или на пару пользователь-чат.
```go
plugin := laniakea.NewPlugin[MyDB]("signup")
plugin.NewScene("signup").
SetScope(laniakea.SceneScopeUserChat).
SetEntry("ask_name").
OnStep("ask_name", func(ctx *laniakea.SceneContext, db MyDB) (laniakea.SceneResult, error) {
if ctx.Text == "" {
ctx.Answer("Как тебя зовут?")
return ctx.Stay(), nil
}
if err := ctx.SaveData(struct {
Name string `json:"name"`
}{Name: ctx.Text}); err != nil {
return laniakea.SceneResult{}, err
}
ctx.Answer("Приятно познакомиться.")
return ctx.Next("done"), nil
}).
OnStep("done", func(ctx *laniakea.SceneContext, db MyDB) (laniakea.SceneResult, error) {
return ctx.Exit(), nil
})
```
- Используйте `ctx.EnterScene("signup")`, чтобы войти в entry step, настроенный у сцены.
- Используйте `ctx.EnterSceneStep("signup", "done")`, если нужен явный стартовый step.
- Из scene handler возвращайте `ctx.Stay()`, `ctx.Next(step)`, `ctx.Exit()` или `ctx.Pass()` для управления потоком.
- `SceneActionPass` не меняет текущую session state и продолжает обычный routing бота.
- Для JSON-состояния сцены используйте `SceneContext.SaveData(...)` и `SceneContext.BindData(...)`.
- Выбирайте `SceneScopeUser`, `SceneScopeChat` или `SceneScopeUserChat` в зависимости от того, насколько широко должен разделяться диалог.
### tgapi: API и Uploader
В `tgapi` есть два клиента:
- `API` для JSON-запросов (`SendMessage`, `EditMessageText`, методы с `file_id`/URL).
- `Uploader` для multipart-загрузок (`SendPhoto`, `SendDocument`, `SendVideo` с бинарными файлами).
Для продвинутых сценариев `tgapi.NewRequest(...)` и `tgapi.NewUploaderRequest(...)` остаются публичными low-level escape hatch API. Они менее безопасны, чем типизированные helper-методы: вызывающая сторона сама отвечает за корректное имя Telegram-метода и совместимые типы параметров/ответа.
## 🧩 Промежуточные слои (Middleware) ## 🧩 Промежуточные слои (Middleware)
Middleware — это функции, которые выполняются перед обработчиком команды. Они идеально подходят для сквозных задач, таких как логирование, контроль доступа, ограничение скорости запросов или модификация контекста. Middleware — это функции, которые выполняются перед обработчиком команды. Они идеально подходят для сквозных задач, таких как логирование, контроль доступа, ограничение скорости запросов или модификация контекста.
@@ -167,11 +250,12 @@ func(ctx *MsgContext, db T) bool
- Если возвращается false, цепочка выполнения немедленно прерывается (команда не запускается). - Если возвращается false, цепочка выполнения немедленно прерывается (команда не запускается).
### Добавление middleware ### Добавление middleware
Используйте метод Use плагина для добавления одной или нескольких функций middleware. Они выполняются в порядке добавления. Используйте метод `AddMiddleware` плагина для добавления одной или нескольких функций middleware. Они выполняются в порядке добавления.
```go ```go
plugin := laniakea.NewPlugin[MyDB]("admin") plugin := laniakea.NewPlugin[*MyDB]("admin")
plugin.Use(loggingMiddleware, adminOnlyMiddleware) plugin.AddMiddleware(laniakea.NewMiddleware("logging", loggingMiddleware))
plugin.AddMiddleware(laniakea.NewMiddleware("admin-only", adminOnlyMiddleware))
plugin.AddCommand(plugin.NewCommand(banUser, "ban")) plugin.AddCommand(plugin.NewCommand(banUser, "ban"))
``` ```
@@ -200,15 +284,25 @@ func adminOnlyMiddleware(ctx *laniakea.MsgContext, db *MyDB) bool {
- Middleware может изменять MsgContext (например, добавлять пользовательские поля) перед запуском команды. - Middleware может изменять MsgContext (например, добавлять пользовательские поля) перед запуском команды.
## ⚙️ Расширенная настройка ## ⚙️ Расширенная настройка
**Инлайн-клавиатуры**: Создавайте клавиатуры с помощью laniakea.NewKeyboard(). - **Инлайн-клавиатуры**: Создавайте клавиатуры с помощью `laniakea.NewInlineKeyboardJson`, `laniakea.NewInlineKeyboardBase64` или `laniakea.NewInlineKeyboard`. `Bot.SetPayloadType(...)` задаёт payload format по умолчанию, а `InlineKeyboard.SetPayloadType(...)` переопределяет его для конкретной клавиатуры.
**Ограничение запросов**: Передайте настроенный utils.RateLimiter через BotOpts для корректной обработки лимитов Telegram. - **Ограничение запросов**: Передайте настроенный `utils.RateLimiter` через `BotOpts` для корректной обработки лимитов Telegram.
**Пользовательский HTTP-клиент**: Предоставьте свой http.Client в BotOpts для точного контроля. - **Локализация**: `L10n` безопасен для конкурентного использования после подключения к боту.
- **Пользовательские update handlers**: Используйте `plugin.AddUpdateHandler(...)` для Telegram update types вне command/payload flow.
- **Жизненный цикл**: `RunWithContext(...)` и `RunWebHookWithContext(...)` не вызывают `Close()` автоматически. Завершайте бот явно и создавайте новый `Bot` для следующего запуска.
## Обработка Telegram Updates
- Команды и payload-ы обрабатываются через плагины.
- Для некомандных update-ов можно зарегистрировать обработчик через `plugin.AddUpdateHandler(updateType, handler)`.
- `message`, `channel_post` и `callback_query` остаются в command/payload flow.
- После JSON-декодирования `tgapi.Update` заполняет поле `Type`, чтобы обработчики могли явно видеть итоговый вид update.
## 📝 Лицензия ## 📝 Лицензия
Этот проект лицензирован под GNU General Public License v3.0 - подробности см. в файле [LICENSE](LICENSE). Этот проект лицензирован под GNU General Public License v3.0 - подробности см. в файле [LICENSE](LICENSE).
## 📚 Дополнительная информация ## 📚 Дополнительная информация
[GoDoc Laniakea](https://pkg.go.dev/git.nix13.pw/scuroneko/laniakea) [GoDoc Laniakea](https://pkg.go.dev/git.scuroneko.dev/scuroneko/laniakea)
[Wiki](https://git.scuroneko.dev/ScuroNeko/Laniakea/wiki)
[Telegram Bot API](https://core.telegram.org/bots/api) [Telegram Bot API](https://core.telegram.org/bots/api)
+45
View File
@@ -0,0 +1,45 @@
# Semantic Versioning Policy
This project follows Semantic Versioning with the rules below.
## Public API Surface
The public API consists of:
- exported identifiers in package `laniakea`
- exported identifiers in package `tgapi`
- documented behavior in `README.md`, `README_RU.md`, and package godoc
Anything unexported is internal and may change without notice.
## Breaking Changes
A release requires a major version bump when it changes any of the following:
- exported function, method, type, field, constant, or variable names
- function or method signatures
- JSON field names or request/response wire compatibility in `tgapi`
- documented behavioral guarantees relied on by callers
Examples:
- removing an exported alias
- changing callback payload encoding defaults
- changing handler dispatch semantics in a way that breaks existing bots
## Minor Changes
A release uses a minor version bump for backward-compatible additions:
- new exported types, methods, helpers, or update handlers
- support for new Telegram Bot API fields or methods
- optional configuration knobs that do not change existing defaults
## Patch Changes
A release uses a patch version bump for backward-compatible fixes:
- bug fixes
- test-only changes
- godoc and README clarifications
- internal refactors with no public behavior change
## Pre-Releases
`-rc.N` builds may still adjust API details before `v1.0.0`.
Once `v1.0.0` is released, breaking changes require a new major version.
+28
View File
@@ -0,0 +1,28 @@
# TODO
The framework backlog has moved to the wiki.
Primary page:
- https://git.scuroneko.dev/ScuroNeko/Laniakea/wiki/Framework-Backlog
Russian page:
- https://git.scuroneko.dev/ScuroNeko/Laniakea/wiki/Framework-Backlog-RU
Current priority split:
- `Partial`: none.
- `Ideas`: service layer and dependency graph model, plugin composition contract.
Completed former high-priority items:
- `[v1.0.0-rc.14] Webhook runtime model.`
- `[v1.0.0-rc.13] Observability model`: added first-class `Observer` events for update, command, payload, scene, policy, runner, polling, and centralized error flows, with safe event dispatch and regression coverage for the new runtime hooks.
- `[v1.0.0-rc.13] Authorization and policy model`: added first-class `Policy[T]`, middleware integration through `RequirePolicy(...)`, plugin and bot policy registration helpers, built-in Telegram-aware policies, and composable `AllPolicies(...)`, `AnyPolicy(...)`, and `NotPolicy(...)` helpers with regression coverage.
- `[v1.0.0-rc.13] Update schema contract`: documented and tested the normalized `MsgContext` update-routing contract, including routing categories and per-update field guarantees.
- `[v1.0.0-rc.13] User-facing vs internal error model`: added explicit user-visible vs internal-only error markers and updated centralized handler error routing accordingly.
- `[v1.0.0-rc.13] Configuration freeze model`: formalized bot configuration freeze after first run, documented lifecycle commit points, and added regression coverage for ignored late mutations.
- `[v1.0.0-rc.12] Conversation / Scene Model`.
- `[v1.0.0-rc.12] Typed Handler Input Model`.
- `[v1.0.0-rc.12] Request Context / Cancellation Model`.
+183 -342
View File
@@ -4,35 +4,45 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"sort"
"strings"
"sync" "sync"
"time"
"git.nix13.pw/scuroneko/extypes" "git.scuroneko.dev/scuroneko/extypes"
"git.nix13.pw/scuroneko/laniakea/tgapi" "git.scuroneko.dev/scuroneko/laniakea/tgapi"
"git.nix13.pw/scuroneko/laniakea/utils" "git.scuroneko.dev/scuroneko/laniakea/utils"
"git.nix13.pw/scuroneko/slog" "git.scuroneko.dev/scuroneko/slog"
"github.com/alitto/pond/v2"
) )
// DbContext is an interface representing the application's database context. // AppData is the generic shared application data type injected into bots,
// It is injected into plugins and middleware via Bot.DatabaseContext(). // plugins, and handlers.
//
// Use it for long-lived shared dependencies such as database handles, service
// containers, API clients, or immutable configuration snapshots.
// //
// Example: // Example:
// //
// type MyDB struct { ... } // type MyDB struct { ... }
// bot := NewBot[MyDB](opts).DatabaseContext(&myDB) // myDB := &MyDB{}
// bot, err := NewBot[*MyDB](opts)
// if err != nil {
// return err
// }
// bot.SetAppData(myDB)
// //
// Use NoDB if no database is needed. // Use NoData if no shared application data is needed.
type DbContext any type AppData any
// NoDB is a placeholder type for bots that do not use a database. // NoData is a placeholder type for bots that do not use shared application
// Use Bot[NoDB] to indicate no dependency injection is required. // data.
type NoDB struct{ DbContext } //
// Use Bot[NoData] to indicate no shared dependency injection is required.
type NoData struct{ AppData }
// DbLogger is a function type that returns a slog.LoggerWriter for database logging. // AppDataLogger builds a slog.LoggerWriter from injected application data.
// Used to inject database-specific log output (e.g., SQL queries, ORM events). //
type DbLogger[T DbContext] func(db *T) slog.LoggerWriter // Use it when shared application data exposes a log sink or adapter that should
// receive framework logs.
type AppDataLogger[T AppData] func(data T) slog.LoggerWriter
// BotPayloadType defines the serialization format for callback data payloads. // BotPayloadType defines the serialization format for callback data payloads.
type BotPayloadType string type BotPayloadType string
@@ -44,6 +54,20 @@ var (
BotPayloadJson BotPayloadType = "json" BotPayloadJson BotPayloadType = "json"
) )
var (
// ErrNoPrefixes reports that the bot was started without any command prefixes.
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 = errors.New("bot can only be run once")
// ErrTokenRequired reports that BotOpts.Token was empty.
ErrTokenRequired = errors.New("token required")
// ErrOptsIsNil reports that NewBot was called with a nil BotOpts pointer.
ErrOptsIsNil = errors.New("opts is nil")
)
// Bot is the core Telegram bot instance. // Bot is the core Telegram bot instance.
// //
// Manages: // Manages:
@@ -53,17 +77,22 @@ var (
// - Logging and rate limiting // - Logging and rate limiting
// - Localization and draft message support // - Localization and draft message support
// //
// All methods are safe for concurrent use. Direct field access is not recommended. // Runtime accessors are safe for concurrent use. Configure the bot before Run,
type Bot[T DbContext] struct { // RunWithContext, or RunWebHookWithContext.
token string // A Bot is single-use: after Run, RunWithContext, or RunWebHookWithContext returns,
debug bool // create a new Bot for the next session.
errorTemplate string type Bot[T AppData] struct {
username string token string
payloadType BotPayloadType debug bool
maxWorkers int errorTemplate string
username string
payloadType BotPayloadType
strictPayloadType bool
maxWorkers int
logger *slog.Logger // Main bot logger (JSON stdout + optional file) logger *slog.Logger // Main bot logger (JSON stdout + optional file)
RequestLogger *slog.Logger // Optional request-level API logging RequestLogger *slog.Logger // Optional request-level API logging
webHookLogger *slog.Logger // Webhook logger. Available only after Bot.RunWebHookWithContext.
extraLoggers extypes.Slice[*slog.Logger] // API, Uploader, and custom loggers extraLoggers extypes.Slice[*slog.Logger] // API, Uploader, and custom loggers
plugins []Plugin[T] // Command/event handlers plugins []Plugin[T] // Command/event handlers
@@ -73,9 +102,16 @@ type Bot[T DbContext] struct {
api *tgapi.API // Telegram API client api *tgapi.API // Telegram API client
uploader *tgapi.Uploader // File uploader uploader *tgapi.Uploader // File uploader
dbContext *T // Injected database context
l10n *L10n // Localization manager l10n *L10n // Localization manager
draftProvider *DraftProvider // Draft message builder draftProvider *DraftProvider // Draft message builder
observer Observer // Optional event observer for instrumentation
appData T // Injected application data
hasAppData bool
warnedValueData bool
sessionStore SessionStore // Session store for scene management
sceneScopePriority []SceneScope
updateOffsetMu sync.Mutex updateOffsetMu sync.Mutex
updateOffset int // Last processed update ID updateOffset int // Last processed update ID
@@ -83,6 +119,21 @@ type Bot[T DbContext] struct {
updateQueue chan *tgapi.Update // Internal queue for processing updates updateQueue chan *tgapi.Update // Internal queue for processing updates
runnerOnceWG sync.WaitGroup // Tracks one-time async runners runnerOnceWG sync.WaitGroup // Tracks one-time async runners
runnerBgWG sync.WaitGroup // Tracks background async runners runnerBgWG sync.WaitGroup // Tracks background async runners
runStateMu sync.Mutex
running bool
ran bool
}
func (bot *Bot[T]) configMutable(method string) bool {
bot.runStateMu.Lock()
defer bot.runStateMu.Unlock()
if !bot.ran {
return true
}
if bot.logger != nil {
bot.logger.Warnln(fmt.Sprintf("%s called after bot configuration was frozen; ignoring", method))
}
return false
} }
// NewBot creates and initializes a new Bot instance using the provided BotOpts. // NewBot creates and initializes a new Bot instance using the provided BotOpts.
@@ -93,13 +144,12 @@ type Bot[T DbContext] struct {
// - Fetches bot username via GetMe() // - Fetches bot username via GetMe()
// - Sets up DraftProvider with random IDs // - Sets up DraftProvider with random IDs
// - Adds API and Uploader loggers to extraLoggers // - Adds API and Uploader loggers to extraLoggers
// func NewBot[T any](opts *BotOpts) (*Bot[T], error) {
// Panics if: if opts == nil {
// - Token is empty return nil, ErrOptsIsNil
// - GetMe() fails (invalid token or network error) }
func NewBot[T any](opts *BotOpts) *Bot[T] {
if opts.Token == "" { if opts.Token == "" {
panic("laniakea: BotOpts.Token is required") return nil, ErrTokenRequired
} }
updateQueue := make(chan *tgapi.Update, 512) updateQueue := make(chan *tgapi.Update, 512)
@@ -130,22 +180,26 @@ func NewBot[T any](opts *BotOpts) *Bot[T] {
} }
bot := &Bot[T]{ bot := &Bot[T]{
updateOffset: 0, updateOffset: 0,
errorTemplate: "%s", errorTemplate: "%s",
payloadType: BotPayloadBase64, payloadType: BotPayloadBase64,
maxWorkers: workers, strictPayloadType: opts.StrictPayloadType,
updateQueue: updateQueue, maxWorkers: workers,
api: api, updateQueue: updateQueue,
uploader: uploader, api: api,
debug: opts.Debug, uploader: uploader,
prefixes: prefixes, debug: opts.Debug,
token: opts.Token, prefixes: prefixes,
plugins: make([]Plugin[T], 0), token: opts.Token,
updateTypes: append([]tgapi.UpdateType{}, opts.UpdateTypes...), plugins: make([]Plugin[T], 0),
runners: make([]Runner[T], 0), updateTypes: append([]tgapi.UpdateType{}, opts.UpdateTypes...),
extraLoggers: make([]*slog.Logger, 0), runners: make([]Runner[T], 0),
l10n: &L10n{}, extraLoggers: make([]*slog.Logger, 0),
draftProvider: NewRandomDraftProvider(api), l10n: &L10n{},
draftProvider: NewRandomDraftProvider(api),
sessionStore: NewMemorySessionStore(),
sceneScopePriority: []SceneScope{SceneScopeUserChat, SceneScopeChat, SceneScopeUser},
} }
// Add API and Uploader loggers to extraLoggers for unified output // Add API and Uploader loggers to extraLoggers for unified output
@@ -163,7 +217,7 @@ func NewBot[T any](opts *BotOpts) *Bot[T] {
u, err := api.GetMe() u, err := api.GetMe()
if err != nil { if err != nil {
_ = bot.Close() _ = bot.Close()
bot.logger.Fatal(err) return nil, err
} }
bot.username = Val(u.Username, "") bot.username = Val(u.Username, "")
if bot.username == "" { if bot.username == "" {
@@ -171,46 +225,66 @@ func NewBot[T any](opts *BotOpts) *Bot[T] {
} }
bot.logger.Infoln(fmt.Sprintf("Authorized as %s (@%s)", u.FirstName, Val(u.Username, "unknown"))) bot.logger.Infoln(fmt.Sprintf("Authorized as %s (@%s)", u.FirstName, Val(u.Username, "unknown")))
return bot return bot, nil
} }
// Close gracefully shuts down bot-owned resources. // Close gracefully shuts down bot-owned resources.
// //
// Close shuts down, in order: // Close shuts down, in order:
// - Registered plugins via Plugin.Close // - Registered plugins via Plugin.Close
// - Webhook logger (if initialized)
// - Uploader (waits for pending uploads) // - Uploader (waits for pending uploads)
// - API client internals // - API client internals
// - RequestLogger (if enabled) // - RequestLogger (if enabled)
// - Main logger // - Main logger
// //
// RunWithContext does not call Close automatically. The caller is responsible // RunWithContext and RunWebHookWithContext do not call Close automatically.
// for invoking Close after RunWithContext returns to release these resources. // The caller is responsible for invoking Close after runtime returns to release
// these resources.
// //
// Close returns a joined error containing all shutdown failures, if any. // Close returns a joined error containing all shutdown failures, if any.
func (bot *Bot[T]) Close() error { func (bot *Bot[T]) Close() error {
var e []error var e []error
logCloseErr := func(err error) {
if err == nil {
return
}
if bot.logger != nil {
bot.logger.Errorln(err)
}
e = append(e, err)
}
for _, p := range bot.plugins { for _, p := range bot.plugins {
if err := p.Close(); err != nil { if err := p.Close(); err != nil {
e = append(e, err) e = append(e, err)
} }
} }
if err := bot.uploader.Close(); err != nil { if bot.webHookLogger != nil {
bot.logger.Errorln(err) if err := bot.webHookLogger.Close(); err != nil {
e = append(e, err) logCloseErr(err)
}
bot.webHookLogger = nil
} }
if err := bot.api.Close(); err != nil { if bot.uploader != nil {
bot.logger.Errorln(err) if err := bot.uploader.Close(); err != nil {
e = append(e, err) logCloseErr(err)
}
}
if bot.api != nil {
if err := bot.api.Close(); err != nil {
logCloseErr(err)
}
} }
if bot.RequestLogger != nil { if bot.RequestLogger != nil {
if err := bot.RequestLogger.Close(); err != nil { if err := bot.RequestLogger.Close(); err != nil {
bot.logger.Errorln(err) logCloseErr(err)
e = append(e, err)
} }
} }
if err := bot.logger.Close(); err != nil { if bot.logger != nil {
e = append(e, err) if err := bot.logger.Close(); err != nil {
e = append(e, err)
}
} }
return errors.Join(e...) return errors.Join(e...)
} }
@@ -226,40 +300,6 @@ func (bot *Bot[T]) CloseRemote(ctx context.Context) error {
return nil return nil
} }
// initLoggers configures the main and optional request loggers.
//
// Uses DEBUG flag to set log level (DEBUG if true, FATAL otherwise).
// Writes to stdout in JSON format by default.
// If WriteToFile is true, writes to main.log and requests.log in LoggerBasePath.
func (bot *Bot[T]) initLoggers(opts *BotOpts) {
level := slog.FATAL
if opts.Debug {
level = slog.DEBUG
}
bot.logger = utils.CreateLogger("BOT", level)
if opts.WriteToFile {
path := fmt.Sprintf("%s/main.log", strings.TrimRight(opts.LoggerBasePath, "/"))
logger, err := utils.CreateFileLogger("BOT", level, path)
if err != nil {
bot.logger.Fatal(err)
}
bot.logger = logger
}
if opts.UseRequestLogger {
bot.RequestLogger = utils.CreateLogger("REQUESTS", level)
if opts.WriteToFile {
path := fmt.Sprintf("%s/requests.log", strings.TrimRight(opts.LoggerBasePath, "/"))
logger, err := utils.CreateFileLogger("REQUESTS", level, path)
if err != nil {
bot.logger.Fatal(err)
}
bot.RequestLogger = logger
}
}
}
// GetUpdateOffset returns the current update offset (thread-safe). // GetUpdateOffset returns the current update offset (thread-safe).
func (bot *Bot[T]) GetUpdateOffset() int { func (bot *Bot[T]) GetUpdateOffset() int {
bot.updateOffsetMu.Lock() bot.updateOffsetMu.Lock()
@@ -274,16 +314,9 @@ func (bot *Bot[T]) SetUpdateOffset(offset int) {
bot.updateOffset = offset bot.updateOffset = offset
} }
// GetUpdateTypes returns the list of update types the bot is configured to receive.
func (bot *Bot[T]) GetUpdateTypes() []tgapi.UpdateType { return bot.updateTypes }
// GetLogger returns the main bot logger. // GetLogger returns the main bot logger.
func (bot *Bot[T]) GetLogger() *slog.Logger { return bot.logger } func (bot *Bot[T]) GetLogger() *slog.Logger { return bot.logger }
// GetDBContext returns the injected database context.
// Returns nil if not set via DatabaseContext().
func (bot *Bot[T]) GetDBContext() *T { return bot.dbContext }
// GetLoggerLevel returns the effective log level derived from the bot's debug // GetLoggerLevel returns the effective log level derived from the bot's debug
// flag. // flag.
func (bot *Bot[T]) GetLoggerLevel() slog.LogLevel { func (bot *Bot[T]) GetLoggerLevel() slog.LogLevel {
@@ -300,216 +333,6 @@ func (bot *Bot[T]) L10n(lang, key string) string {
return bot.l10n.Translate(lang, key) return bot.l10n.Translate(lang, key)
} }
// SetDraftProvider replaces the default DraftProvider with a custom one.
// Useful for using LinearDraftIdGenerator to persist draft IDs across restarts.
func (bot *Bot[T]) SetDraftProvider(p *DraftProvider) *Bot[T] {
bot.draftProvider = p
return bot
}
// DatabaseContext injects a database context into the bot.
// This context is accessible to plugins and middleware via GetDBContext().
func (bot *Bot[T]) DatabaseContext(ctx *T) *Bot[T] {
bot.dbContext = ctx
return bot
}
// UpdateTypes sets the list of update types the bot will request from Telegram.
// Overwrites any previously set types.
func (bot *Bot[T]) UpdateTypes(t ...tgapi.UpdateType) *Bot[T] {
bot.updateTypes = make([]tgapi.UpdateType, 0)
bot.updateTypes = append(bot.updateTypes, t...)
return bot
}
// SetPayloadType sets the payload encoding type used for callback data.
// JSON stores payload as a string: `{"cmd":"command","args":[...]}`.
// Base64 stores the same JSON encoded as a Base64URL string.
func (bot *Bot[T]) SetPayloadType(t BotPayloadType) *Bot[T] {
bot.payloadType = t
return bot
}
// AddUpdateType adds one or more update types to the list.
// Does not overwrite existing types.
func (bot *Bot[T]) AddUpdateType(t ...tgapi.UpdateType) *Bot[T] {
bot.updateTypes = append(bot.updateTypes, t...)
return bot
}
// AddPrefixes adds one or more command prefixes (e.g., "/", "!").
// Must have at least one prefix before Run().
func (bot *Bot[T]) AddPrefixes(prefixes ...string) *Bot[T] {
bot.prefixes = append(bot.prefixes, prefixes...)
return bot
}
// ErrorTemplate sets the format string for error messages sent to users.
// Use "%s" to insert the error message.
// Example: "❌ Error: %s" → "❌ Error: Command not found".
func (bot *Bot[T]) ErrorTemplate(s string) *Bot[T] {
bot.errorTemplate = s
return bot
}
// Debug enables or disables debug logging.
func (bot *Bot[T]) Debug(debug bool) *Bot[T] {
bot.debug = debug
level := slog.FATAL
if debug {
level = slog.DEBUG
}
bot.logger.Level(level)
if bot.RequestLogger != nil {
bot.RequestLogger.Level(level)
}
for _, p := range bot.plugins {
if p.logger == nil {
continue
}
p.logger.Level(level)
}
return bot
}
// AddPlugins registers one or more plugins.
// Plugins are executed in registration order unless filtered by middleware.
//
// Registration is a commit point for plugin configuration. The Bot stores
// plugin metadata internally, so plugins must be fully configured before they
// are passed here. Post-registration mutation through the original *Plugin is
// not a supported API, even if some changes appear to work due to shared maps.
func (bot *Bot[T]) AddPlugins(plugin ...*Plugin[T]) *Bot[T] {
level := bot.GetLoggerLevel()
for _, p := range plugin {
if p.logger == nil {
logger := utils.CreateLogger(p.name, level)
p.SetLogger(logger)
}
bot.plugins = append(bot.plugins, *p)
bot.logger.Debugln(fmt.Sprintf("plugins with name \"%s\" registered", p.name))
}
return bot
}
// AddMiddleware registers one or more middleware handlers.
//
// Middleware are executed in order of increasing .order value before plugins.
// If two middleware have the same order, they are sorted lexicographically by name.
//
// Middleware can:
// - Modify or reject updates before they reach plugins
// - Inject context (e.g., user auth state, rate limit status)
// - Log, validate, or transform incoming data
//
// Example:
//
// bot.AddMiddleware(&authMiddleware, &rateLimitMiddleware)
//
// Panics if any middleware has a nil name.
func (bot *Bot[T]) AddMiddleware(middleware ...Middleware[T]) *Bot[T] {
for _, m := range middleware {
if m.name == "" {
panic("laniakea: middleware must have a non-empty name")
}
bot.middlewares = append(bot.middlewares, m)
bot.logger.Debugln(fmt.Sprintf("middleware with name \"%s\" registered", m.name))
}
// Stable sort by order (ascending), then by name (lexicographic)
sort.Slice(bot.middlewares, func(i, j int) bool {
first := bot.middlewares[i]
second := bot.middlewares[j]
if first.order != second.order {
return first.order < second.order
}
return first.name < second.name
})
return bot
}
// AddRunner registers a background runner to execute concurrently with the bot.
//
// Runners are goroutines that run independently of update processing.
// Common use cases:
// - Periodic cleanup (e.g., expiring drafts, clearing temp files)
// - Metrics collection or health checks
// - Scheduled tasks (e.g., daily announcements)
//
// Runners are started immediately after Bot.Run() is called.
//
// Example:
//
// bot.AddRunner(&cleanupRunner)
//
// Panics if runner has a nil name.
func (bot *Bot[T]) AddRunner(runner Runner[T]) *Bot[T] {
if runner.name == "" {
panic("laniakea: runner must have a non-empty name")
}
bot.runners = append(bot.runners, runner)
bot.logger.Debugln(fmt.Sprintf("runner with name \"%s\" registered", runner.name))
return bot
}
// AddL10n sets the localization (i18n) provider for the bot.
//
// The L10n instance must be pre-populated with translations.
// Translations are accessed via Bot.L10n(lang, key).
//
// Example:
//
// l10n := l10n.New()
// l10n.Add("en", "hello", "Hello!")
// l10n.Add("es", "hello", "¡Hola!")
// bot.AddL10n(l10n)
//
// Replaces any previously set L10n instance.
func (bot *Bot[T]) AddL10n(l *L10n) *Bot[T] {
if l == nil {
bot.logger.Warn("AddL10n called with nil L10n; localization will be disabled")
return bot
}
bot.l10n = l
return bot
}
// AddDatabaseLoggerWriter adds a database logger writer to all loggers.
//
// The writer will receive logs from:
// - Main bot logger
// - Request logger (if enabled)
// - API and Uploader loggers
// - Already registered plugin loggers
//
// Call this after AddPlugins if plugin loggers should also receive the writer.
// Plugins registered later do not automatically inherit previously added
// database writers; call AddDatabaseLoggerWriter again after adding them.
//
// Example:
//
// bot.AddDatabaseLoggerWriter(func(db *MyDB) slog.LoggerWriter {
// return db.QueryLogger()
// })
func (bot *Bot[T]) AddDatabaseLoggerWriter(writer DbLogger[T]) *Bot[T] {
w := writer(bot.dbContext)
bot.logger.AddWriter(w)
if bot.RequestLogger != nil {
bot.RequestLogger.AddWriter(w)
}
for _, l := range bot.extraLoggers {
l.AddWriter(w)
}
for _, p := range bot.plugins {
if p.logger != nil {
p.logger.AddWriter(w)
}
}
return bot
}
// RunWithContext starts the bot with a given context for graceful shutdown. // RunWithContext starts the bot with a given context for graceful shutdown.
// //
// This is the main entry point for bot execution. It: // This is the main entry point for bot execution. It:
@@ -523,31 +346,29 @@ func (bot *Bot[T]) AddDatabaseLoggerWriter(writer DbLogger[T]) *Bot[T] {
// - Finishes processing currently queued updates // - Finishes processing currently queued updates
// - Waits for registered runners to exit // - 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.
// Telegram keeps webhook delivery active until the webhook is removed.
//
// RunWithContext does not close API, uploader, or logger resources on return. // RunWithContext does not close API, uploader, or logger resources on return.
// The caller must invoke Close after RunWithContext finishes. // The caller must invoke Close after RunWithContext finishes.
// //
// Example: // A Bot is single-use. After RunWithContext returns, later calls return ErrBotAlreadyRun.
// func (bot *Bot[T]) RunWithContext(ctx context.Context) error {
// ctx, cancel := context.WithCancel(context.Background())
// go bot.RunWithContext(ctx)
// // ... later ...
// cancel() // triggers graceful shutdown
// _ = bot.Close(context.Background())
func (bot *Bot[T]) RunWithContext(ctx context.Context) {
if len(bot.prefixes) == 0 { if len(bot.prefixes) == 0 {
bot.logger.Fatalln("no prefixes defined") return ErrNoPrefixes
return
} }
if len(bot.plugins) == 0 { if len(bot.plugins) == 0 {
bot.logger.Fatalln("no plugins defined") return ErrNoPlugins
return
} }
if err := bot.beginRun(); err != nil {
return err
}
defer bot.finishRun()
bot.ExecRunners(ctx) bot.ExecRunners(ctx)
bot.logger.Infoln("Bot running. Press CTRL+C to exit.")
// Start update polling in a goroutine // Start update polling in a goroutine
go func() { go func() {
defer func() { defer func() {
@@ -556,6 +377,8 @@ func (bot *Bot[T]) RunWithContext(ctx context.Context) {
} }
close(bot.updateQueue) close(bot.updateQueue)
}() }()
retryDelay := time.Duration(0)
retryCount := 0
for { for {
select { select {
case <-ctx.Done(): case <-ctx.Done():
@@ -567,14 +390,36 @@ func (bot *Bot[T]) RunWithContext(ctx context.Context) {
return return
} }
bot.logger.Errorln("failed to fetch updates:", err) bot.logger.Errorln("failed to fetch updates:", err)
retryDelay = nextPollRetryDelay(retryDelay)
retryCount++
bot.safeEmitEvent(ctx, PollingRetryEvent{
Attempt: retryCount,
Delay: retryDelay,
Err: err,
})
bot.safeEmitEvent(ctx, ErrorEvent{
Plugin: "bot",
HandlerKind: HandlerPollingKind,
HandlerName: "getUpdates",
Err: err,
UserFacing: false,
})
timer := time.NewTimer(retryDelay)
select {
case <-ctx.Done():
if !timer.Stop() {
<-timer.C
}
return
case <-timer.C:
}
continue continue
} }
retryDelay = 0
retryCount = 0
for _, update := range updates { for _, update := range updates {
u := update // copy loop variable to avoid race condition if err := bot.enqueueUpdate(ctx, update); err != nil {
select {
case bot.updateQueue <- &u:
case <-ctx.Done():
return return
} }
} }
@@ -582,17 +427,13 @@ func (bot *Bot[T]) RunWithContext(ctx context.Context) {
} }
}() }()
bot.logger.Infoln("Bot running. Press CTRL+C to exit.")
// Start worker pool for concurrent update handling // Start worker pool for concurrent update handling
pool := pond.NewPool(bot.maxWorkers) bot.startUpdateWorkers(ctx)
for update := range bot.updateQueue {
u := update // capture loop variable
pool.Submit(func() {
bot.handle(u)
})
}
pool.Stop() // Wait for all tasks to complete and stop the pool
bot.runnerOnceWG.Wait() bot.runnerOnceWG.Wait()
bot.runnerBgWG.Wait() bot.runnerBgWG.Wait()
return nil
} }
// Run starts the bot using a background context. // Run starts the bot using a background context.
@@ -601,6 +442,6 @@ func (bot *Bot[T]) RunWithContext(ctx context.Context) {
// Use this for simple bots where graceful shutdown is not required. // Use this for simple bots where graceful shutdown is not required.
// //
// For production use, prefer RunWithContext to handle SIGINT/SIGTERM gracefully. // For production use, prefer RunWithContext to handle SIGINT/SIGTERM gracefully.
func (bot *Bot[T]) Run() { func (bot *Bot[T]) Run() error {
bot.RunWithContext(context.Background()) return bot.RunWithContext(context.Background())
} }
+224
View File
@@ -0,0 +1,224 @@
package laniakea
import (
"fmt"
"slices"
"git.scuroneko.dev/scuroneko/laniakea/tgapi"
"git.scuroneko.dev/scuroneko/slog"
)
// AddPrefixes adds one or more command prefixes (e.g., "/", "!").
// The bot must have at least one prefix before any runtime entry point starts.
func (bot *Bot[T]) AddPrefixes(prefixes ...string) *Bot[T] {
if !bot.configMutable("AddPrefixes") {
return bot
}
bot.prefixes = append(bot.prefixes, prefixes...)
return bot
}
// SetDraftProvider replaces the default DraftProvider with a custom one.
// Useful for using LinearDraftIdGenerator to persist draft IDs across restarts.
func (bot *Bot[T]) SetDraftProvider(p *DraftProvider) *Bot[T] {
if !bot.configMutable("SetDraftProvider") {
return bot
}
bot.draftProvider = p
return bot
}
// GetDraftProvider returns the draft provider currently used by the bot.
func (bot *Bot[T]) GetDraftProvider() *DraftProvider {
return bot.draftProvider
}
// SetObserver sets an event observer for instrumentation.
func (bot *Bot[T]) SetObserver(observer Observer) *Bot[T] {
if !bot.configMutable("SetObserver") {
return bot
}
if observer == nil {
if bot.logger != nil {
bot.logger.Warn("SetObserver called with nil observer; instrumentation will be disabled")
}
bot.observer = nil
return bot
}
bot.observer = observer
return bot
}
// GetObserver returns the bot's event observer, or nil if no observer is set.
func (bot *Bot[T]) GetObserver() Observer {
return bot.observer
}
// SetSessionStore replaces the session store used for scene management.
func (bot *Bot[T]) SetSessionStore(store SessionStore) *Bot[T] {
if !bot.configMutable("SetSessionStore") {
return bot
}
if store == nil {
bot.logger.Warn("SetSessionStore called with nil store; using default MemorySessionStore")
return bot
}
bot.sessionStore = store
return bot
}
// GetSessionStore returns the session store used for scene management.
func (bot *Bot[T]) GetSessionStore() SessionStore {
return bot.sessionStore
}
// SetSceneScopePriority sets the lookup order for resolving active scene sessions.
func (bot *Bot[T]) SetSceneScopePriority(priority []SceneScope) *Bot[T] {
if !bot.configMutable("SetSceneScopePriority") {
return bot
}
newPriority := make([]SceneScope, 0, 3)
for _, scope := range priority {
if scope != SceneScopeUser && scope != SceneScopeChat && scope != SceneScopeUserChat {
bot.logger.Warnln(fmt.Sprintf("invalid scene scope %v in priority list; ignoring", scope))
continue
}
if slices.Index(newPriority, scope) >= 0 {
bot.logger.Warnln(fmt.Sprintf("duplicate scope %v in scene scope priority; ignoring duplicates", scope))
continue
}
newPriority = append(newPriority, scope)
}
if len(newPriority) == 0 || len(newPriority) > 3 {
bot.logger.Warnln("scene scope priority must have 1 to 3 scopes; ignoring invalid input")
return bot
}
bot.sceneScopePriority = append([]SceneScope(nil), newPriority...)
return bot
}
// SetAppData injects shared application data into the bot.
//
// The data is accessible to commands, payload handlers, middleware, scenes,
// and runners through the generic type parameter T.
//
// For shared dependencies such as *sql.DB, prefer using a pointer type as T.
// Value-typed application data is supported, but the bot warns once because
// handlers receive T by value.
func (bot *Bot[T]) SetAppData(ctx T) *Bot[T] {
if !bot.configMutable("SetAppData") {
return bot
}
if !bot.warnedValueData && shouldWarnOnValueAppData[T]() && bot.logger != nil {
bot.logger.Warnln("app data uses a value type; shared dependencies should usually use a pointer type as T")
bot.warnedValueData = true
}
bot.appData = ctx
bot.hasAppData = true
return bot
}
// GetAppData returns the injected application data.
// If SetAppData was not called, it returns the zero value of T.
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") {
return bot
}
bot.updateTypes = make([]tgapi.UpdateType, 0)
bot.updateTypes = append(bot.updateTypes, t...)
return bot
}
// AddUpdateType adds one or more update types to the list.
// Does not overwrite existing types.
func (bot *Bot[T]) AddUpdateType(t ...tgapi.UpdateType) *Bot[T] {
if !bot.configMutable("AddUpdateType") {
return bot
}
bot.updateTypes = append(bot.updateTypes, t...)
return bot
}
// GetUpdateTypes returns the list of update types the bot is configured to receive.
func (bot *Bot[T]) GetUpdateTypes() []tgapi.UpdateType {
return append([]tgapi.UpdateType(nil), bot.updateTypes...)
}
// SetPayloadType sets the default payload encoding type used for callback data.
// JSON stores payload as a string: `{"cmd":"command","args":[...]}`.
// Base64 stores the same JSON encoded as a Base64URL string.
// InlineKeyboard.SetPayloadType may override this value for an individual keyboard.
func (bot *Bot[T]) SetPayloadType(t BotPayloadType) *Bot[T] {
if !bot.configMutable("SetPayloadType") {
return bot
}
bot.payloadType = t
return bot
}
// GetPayloadType returns the bot's default callback payload encoding type.
func (bot *Bot[T]) GetPayloadType() BotPayloadType { return bot.payloadType }
// SetStrictPayloadType enables or disables strict callback payload decoding.
// When enabled, callback payloads must match the bot's default payload type.
func (bot *Bot[T]) SetStrictPayloadType(strict bool) *Bot[T] {
if !bot.configMutable("SetStrictPayloadType") {
return bot
}
bot.strictPayloadType = strict
return bot
}
// SetErrorTemplate sets the format string for error messages sent to users.
// 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") {
return bot
}
bot.errorTemplate = s
return bot
}
// SetDebug enables or disables debug logging.
func (bot *Bot[T]) SetDebug(debug bool) *Bot[T] {
bot.debug = debug
level := slog.FATAL
if debug {
level = slog.DEBUG
}
bot.logger.Level(level)
if bot.RequestLogger != nil {
bot.RequestLogger.Level(level)
}
for _, p := range bot.plugins {
if p.logger == nil {
continue
}
p.logger.Level(level)
}
return bot
}
// SetL10n sets the localization (i18n) provider for the bot.
//
// The L10n instance must be pre-populated with translations.
// Translations are accessed via Bot.L10n(lang, key).
//
// Replaces any previously set L10n instance.
func (bot *Bot[T]) SetL10n(l *L10n) *Bot[T] {
if !bot.configMutable("SetL10n") {
return bot
}
if l == nil {
bot.logger.Warn("SetL10n called with nil L10n; localization will be disabled")
return bot
}
bot.l10n = l
return bot
}
+35 -10
View File
@@ -5,13 +5,13 @@ import (
"strconv" "strconv"
"strings" "strings"
"git.nix13.pw/scuroneko/laniakea/tgapi" "git.scuroneko.dev/scuroneko/laniakea/tgapi"
) )
// BotOpts holds configuration options for initializing a Bot. // BotOpts holds configuration options for initializing a Bot.
// //
// Values are loaded from environment variables via LoadOptsFromEnv(). // Values are loaded from environment variables via LoadOptsFromEnv().
// Use NewOpts() to create a zero-value struct and set fields manually. // Use &BotOpts{} to create a value and set fields manually.
type BotOpts struct { type BotOpts struct {
// Token is the Telegram bot token (required). // Token is the Telegram bot token (required).
Token string Token string
@@ -56,7 +56,11 @@ type BotOpts struct {
// Use this to prioritize responsiveness over reliability. // Use this to prioritize responsiveness over reliability.
DropRLOverflow bool DropRLOverflow bool
// MaxWorkers is the maximum number of concurrency running update handlers. // StrictPayloadType disables callback payload fallback decoding.
// When enabled, the bot accepts only the configured default payload type.
StrictPayloadType bool
// MaxWorkers is the maximum number of update handlers that may run concurrently.
MaxWorkers int MaxWorkers int
} }
@@ -75,20 +79,31 @@ type BotOpts struct {
// - API_URL: custom API endpoint // - API_URL: custom API endpoint
// - RATE_LIMIT: max requests per second (default: 30) // - RATE_LIMIT: max requests per second (default: 30)
// - DROP_RL_OVERFLOW: "true" to drop updates on rate limit overflow // - 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)
// //
// Returns a populated BotOpts. If TG_TOKEN is missing, behavior is undefined. // Returns a populated BotOpts.
// NewBot validates required fields and returns ErrTokenRequired when TG_TOKEN is missing.
func LoadOptsFromEnv() *BotOpts { func LoadOptsFromEnv() *BotOpts {
rateLimit := 30 rateLimit := 30
maxWorkers := 32
stringUpdateTypes := splitEnvList(os.Getenv("UPDATE_TYPES"))
updateTypes := make([]tgapi.UpdateType, 0, len(stringUpdateTypes))
for _, updateType := range stringUpdateTypes {
updateTypes = append(updateTypes, tgapi.UpdateType(updateType))
}
if rl := os.Getenv("RATE_LIMIT"); rl != "" { if rl := os.Getenv("RATE_LIMIT"); rl != "" {
if n, err := strconv.Atoi(rl); err == nil { if n, err := strconv.Atoi(rl); err == nil {
rateLimit = n rateLimit = n
} }
} }
stringUpdateTypes := splitEnvList(os.Getenv("UPDATE_TYPES")) if mw := os.Getenv("MAX_WORKERS"); mw != "" {
updateTypes := make([]tgapi.UpdateType, 0, len(stringUpdateTypes)) if n, err := strconv.Atoi(os.Getenv("MAX_WORKERS")); err == nil {
for _, updateType := range stringUpdateTypes { maxWorkers = n
updateTypes = append(updateTypes, tgapi.UpdateType(updateType)) }
} }
return &BotOpts{ return &BotOpts{
@@ -106,8 +121,11 @@ func LoadOptsFromEnv() *BotOpts {
UseTestServer: os.Getenv("USE_TEST_SERVER") == "true", UseTestServer: os.Getenv("USE_TEST_SERVER") == "true",
APIUrl: os.Getenv("API_URL"), APIUrl: os.Getenv("API_URL"),
RateLimit: rateLimit, RateLimit: rateLimit,
DropRLOverflow: os.Getenv("DROP_RL_OVERFLOW") == "true", DropRLOverflow: os.Getenv("DROP_RL_OVERFLOW") == "true",
StrictPayloadType: os.Getenv("STRICT_PAYLOAD_TYPE") == "true",
MaxWorkers: maxWorkers,
} }
} }
@@ -196,6 +214,13 @@ func (opts *BotOpts) SetDropRLOverflow(drop bool) *BotOpts {
return opts return opts
} }
// SetStrictPayloadType enables or disables strict callback payload decoding.
// When enabled, the bot accepts only the configured default payload type.
func (opts *BotOpts) SetStrictPayloadType(strict bool) *BotOpts {
opts.StrictPayloadType = strict
return opts
}
// SetMaxWorkers sets the maximum number of concurrent update handlers. // SetMaxWorkers sets the maximum number of concurrent update handlers.
// Must be called before NewBot, as the value is captured during bot creation. // Must be called before NewBot, as the value is captured during bot creation.
// //
+10 -1
View File
@@ -4,7 +4,7 @@ import (
"reflect" "reflect"
"testing" "testing"
"git.nix13.pw/scuroneko/laniakea/tgapi" "git.scuroneko.dev/scuroneko/laniakea/tgapi"
) )
func TestLoadOptsFromEnvIgnoresEmptyUpdateTypes(t *testing.T) { func TestLoadOptsFromEnvIgnoresEmptyUpdateTypes(t *testing.T) {
@@ -45,3 +45,12 @@ func TestLoadPrefixesFromEnvDropsEmptyValues(t *testing.T) {
t.Fatalf("unexpected prefixes: got %v want %v", got, want) t.Fatalf("unexpected prefixes: got %v want %v", got, want)
} }
} }
func TestLoadOptsFromEnvReadsStrictPayloadType(t *testing.T) {
t.Setenv("STRICT_PAYLOAD_TYPE", "true")
opts := LoadOptsFromEnv()
if !opts.StrictPayloadType {
t.Fatal("expected StrictPayloadType to be enabled")
}
}
+157
View File
@@ -0,0 +1,157 @@
package laniakea
import (
"fmt"
"sort"
"git.scuroneko.dev/scuroneko/laniakea/utils"
)
// AddPlugins registers one or more plugins.
// Plugins are executed in registration order unless filtered by middleware.
//
// Registration is a commit point for plugin configuration. The Bot stores
// plugin metadata internally, so plugins must be fully configured before they
// are passed here. Post-registration mutation through the original *Plugin is
// not a supported API, even if some changes appear to work due to shared maps.
func (bot *Bot[T]) AddPlugins(plugin ...*Plugin[T]) *Bot[T] {
if !bot.configMutable("AddPlugins") {
return bot
}
level := bot.GetLoggerLevel()
for _, p := range plugin {
if p == nil {
if bot.logger != nil {
bot.logger.Warn("nil plugin skipped")
}
continue
}
cloned := clonePlugin(p)
if cloned.logger == nil {
cloned.logger = utils.CreateLogger(cloned.name, level)
}
bot.plugins = append(bot.plugins, cloned)
if bot.logger != nil {
bot.logger.Debugln(fmt.Sprintf("plugins with name \"%s\" registered", cloned.name))
}
}
return bot
}
// AddMiddleware registers one or more middleware handlers.
//
// Middleware are executed in order of increasing .order value before plugins.
// If two middleware have the same order, they are sorted lexicographically by name.
//
// Middleware can:
// - Modify or reject updates before they reach plugins
// - Inject context (e.g., user auth state, rate limit status)
// - Log, validate, or transform incoming data
//
// Example:
//
// bot.AddMiddleware(authMiddleware, rateLimitMiddleware)
//
// Middleware with an empty name are skipped with a warning.
func (bot *Bot[T]) AddMiddleware(middleware ...Middleware[T]) *Bot[T] {
if !bot.configMutable("AddMiddleware") {
return bot
}
for _, m := range middleware {
if m.name == "" {
bot.logger.Warnln("middleware must have a non-empty name")
continue
}
bot.middlewares = append(bot.middlewares, m)
bot.logger.Debugln(fmt.Sprintf("middleware with name \"%s\" registered", m.name))
}
// Stable sort by order (ascending), then by name (lexicographic)
sort.Slice(bot.middlewares, func(i, j int) bool {
first := bot.middlewares[i]
second := bot.middlewares[j]
if first.order != second.order {
return first.order < second.order
}
return first.name < second.name
})
return bot
}
// UsePolicy registers a Policy as a bot-level middleware.
func (bot *Bot[T]) UsePolicy(name string, policy Policy[T]) *Bot[T] {
mw := RequirePolicy(name, policy)
return bot.AddMiddleware(mw)
}
// AddRunner registers a background runner to execute concurrently with the bot.
//
// Runners are goroutines that run independently of update processing.
// Common use cases:
// - Periodic cleanup (e.g., expiring drafts, clearing temp files)
// - Metrics collection or health checks
// - Scheduled tasks (e.g., daily announcements)
//
// Runners start from the bot runtime entry points, immediately after
// RunWithContext or RunWebHookWithContext begins.
//
// Example:
//
// bot.AddRunner(cleanupRunner)
//
// Runners with an empty name are skipped with a warning.
func (bot *Bot[T]) AddRunner(runner Runner[T]) *Bot[T] {
if !bot.configMutable("AddRunner") {
return bot
}
if runner.name == "" {
bot.logger.Warnln("runner must have a non-empty name")
return bot
}
bot.runners = append(bot.runners, runner)
bot.logger.Debugln(fmt.Sprintf("runner with name \"%s\" registered", runner.name))
return bot
}
// AddAppDataLoggerWriter adds an app-data-backed logger writer to all loggers.
//
// The writer will receive logs from:
// - Main bot logger
// - Request logger (if enabled)
// - API and Uploader loggers
// - Already registered plugin loggers
//
// Call this after AddPlugins if plugin loggers should also receive the writer.
// Plugins registered later do not automatically inherit previously added
// writers; call AddAppDataLoggerWriter again after adding them.
//
// Example:
//
// bot.AddAppDataLoggerWriter(func(data *MyAppData) slog.LoggerWriter {
// return data.QueryLogger()
// })
func (bot *Bot[T]) AddAppDataLoggerWriter(writer AppDataLogger[T]) *Bot[T] {
if !bot.hasAppData {
bot.logger.Warnln("app data is not set; skipping app-data logger writer")
return bot
}
if isNilValue(bot.appData) {
bot.logger.Warnln("app data is nil; skipping app-data logger writer")
return bot
}
w := writer(bot.appData)
bot.logger.AddWriter(w)
if bot.RequestLogger != nil {
bot.RequestLogger.AddWriter(w)
}
for _, l := range bot.extraLoggers {
l.AddWriter(w)
}
for _, p := range bot.plugins {
if p.logger != nil {
p.logger.AddWriter(w)
}
}
return bot
}
+59
View File
@@ -0,0 +1,59 @@
package laniakea
func (bot *Bot[T]) getSession(key string) (SceneSession, error) {
return bot.sessionStore.Get(key)
}
func (bot *Bot[T]) setSession(key string, session SceneSession) error {
return bot.sessionStore.Set(key, session)
}
func (bot *Bot[T]) deleteSession(key string) error {
return bot.sessionStore.Delete(key)
}
func (bot *Bot[T]) findScene(name string) (*sceneMeta, bool) {
for _, plugin := range bot.plugins {
scene, ok := plugin.scenes[name]
if !ok {
continue
}
steps := make(map[string]struct{}, len(scene.steps))
for step := range scene.steps {
steps[step] = struct{}{}
}
return &sceneMeta{
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) {
var zero SceneSession
for _, scope := range bot.sceneScopePriority {
key, ok := buildSceneKey(scope, ctx)
if !ok {
continue
}
session, err := bot.sessionStore.Get(key)
if err != nil {
return "", zero, err
}
if session.Scene != "" {
return key, session, nil
}
}
return "", zero, ErrCantFindSession
}
func (bot *Bot[T]) buildSceneKey(scope SceneScope, ctx *MsgContext) (string, bool) {
return buildSceneKey(scope, ctx)
}
+590
View File
@@ -0,0 +1,590 @@
package laniakea
import (
"context"
"errors"
"io"
"net/http"
"path/filepath"
"reflect"
"strings"
"testing"
"time"
"git.scuroneko.dev/scuroneko/laniakea/tgapi"
"git.scuroneko.dev/scuroneko/slog"
)
type pollingRoundTripFunc func(*http.Request) (*http.Response, error)
func (f pollingRoundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {
return f(req)
}
type pollingRetryObserver struct {
recordingObserver
cancel context.CancelFunc
}
func (o *pollingRetryObserver) OnPollingRetry(ctx context.Context, ev PollingRetryEvent) {
o.recordingObserver.OnPollingRetry(ctx, ev)
if o.cancel != nil {
o.cancel()
}
}
type testObserver struct{}
func (testObserver) OnReceiveUpdate(context.Context, UpdateReceivedEvent) {}
func (testObserver) OnHandledUpdate(context.Context, UpdateHandledEvent) {}
func (testObserver) OnHandlerStarted(context.Context, HandlerStartedEvent) {}
func (testObserver) OnHandlerFinished(context.Context, HandlerFinishedEvent) {
}
func (testObserver) OnSceneTransition(context.Context, SceneTransitionEvent) {}
func (testObserver) OnPolicyChecked(context.Context, PolicyCheckedEvent) {}
func (testObserver) OnRunnerFinished(context.Context, RunnerFinishedEvent) {}
func (testObserver) OnPollingRetry(context.Context, PollingRetryEvent) {}
func (testObserver) OnError(context.Context, ErrorEvent) {}
func TestGetUpdateTypesReturnsCopy(t *testing.T) {
bot := &Bot[NoData]{updateTypes: []tgapi.UpdateType{tgapi.UpdateTypeMessage}}
got := bot.GetUpdateTypes()
got[0] = tgapi.UpdateTypeCallbackQuery
if want := []tgapi.UpdateType{tgapi.UpdateTypeMessage}; !reflect.DeepEqual(bot.updateTypes, want) {
t.Fatalf("GetUpdateTypes exposed internal slice: got %v want %v", bot.updateTypes, want)
}
}
func TestAddPluginsSnapshotsConfiguration(t *testing.T) {
bot := &Bot[NoData]{logger: slog.CreateLogger()}
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 }))
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 }))
registered := bot.plugins[0]
if _, exists := registered.commands["late"]; exists {
t.Fatal("late command leaked into registered plugin snapshot")
}
if registered.commands["start"].description != "" {
t.Fatalf("registered command description unexpectedly mutated: %q", registered.commands["start"].description)
}
if len(registered.middlewares) != 1 {
t.Fatalf("registered middlewares unexpectedly mutated: got %d want 1", len(registered.middlewares))
}
}
func TestBotPayloadTypeConfiguration(t *testing.T) {
bot := &Bot[NoData]{payloadType: BotPayloadBase64}
if got := bot.GetPayloadType(); got != BotPayloadBase64 {
t.Fatalf("unexpected initial payload type: %q", got)
}
bot.SetPayloadType(BotPayloadJson)
if got := bot.GetPayloadType(); got != BotPayloadJson {
t.Fatalf("unexpected updated payload type: %q", got)
}
bot.SetStrictPayloadType(true)
if !bot.strictPayloadType {
t.Fatal("expected strict payload type to be enabled")
}
}
func TestAddPluginsSkipsNilPlugin(t *testing.T) {
bot := &Bot[NoData]{logger: slog.CreateLogger()}
plugin := NewPlugin[NoData]("demo")
bot.AddPlugins(nil, plugin)
if len(bot.plugins) != 1 {
t.Fatalf("expected exactly one registered plugin, got %d", len(bot.plugins))
}
if bot.plugins[0].name != "demo" {
t.Fatalf("unexpected plugin name: %q", bot.plugins[0].name)
}
}
func TestInitLoggersFallsBackToStdoutLoggerOnFileError(t *testing.T) {
bot := &Bot[NoData]{}
bot.initLoggers(&BotOpts{
Debug: true,
WriteToFile: true,
UseRequestLogger: true,
LoggerBasePath: filepath.Join(t.TempDir(), "missing", "nested"),
})
if bot.logger == nil {
t.Fatal("expected main logger fallback")
}
if bot.RequestLogger == nil {
t.Fatal("expected request logger fallback")
}
if err := bot.RequestLogger.Close(); err != nil {
t.Fatalf("failed to close request logger: %v", err)
}
if err := bot.logger.Close(); err != nil {
t.Fatalf("failed to close main logger: %v", err)
}
}
func TestNextPollRetryDelay(t *testing.T) {
tests := []struct {
name string
prev time.Duration
want time.Duration
}{
{name: "initial", prev: 0, want: time.Second},
{name: "double", prev: 2 * time.Second, want: 4 * time.Second},
{name: "cap", prev: 20 * time.Second, want: 30 * time.Second},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := nextPollRetryDelay(tt.prev); got != tt.want {
t.Fatalf("nextPollRetryDelay(%s) = %s, want %s", tt.prev, got, tt.want)
}
})
}
}
func TestAddDatabaseLoggerWriterSkipsWhenAppDataIsUnset(t *testing.T) {
bot := &Bot[NoData]{logger: slog.CreateLogger()}
called := false
bot.AddAppDataLoggerWriter(func(db NoData) slog.LoggerWriter {
called = true
return nil
})
if called {
t.Fatal("expected app-data logger writer to be skipped when app data is unset")
}
}
func TestAddDatabaseLoggerWriterSkipsWhenAppDataIsNil(t *testing.T) {
type testDB struct{}
bot := &Bot[*testDB]{logger: slog.CreateLogger()}
var db *testDB
bot.SetAppData(db)
called := false
bot.AddAppDataLoggerWriter(func(db *testDB) slog.LoggerWriter {
called = true
return nil
})
if called {
t.Fatal("expected app-data logger writer to be skipped when app data is nil")
}
}
func TestShouldWarnOnValueAppData(t *testing.T) {
type testDB struct{}
type dbIface interface{ Ping() error }
tests := []struct {
name string
got bool
want bool
}{
{name: "NoData", got: shouldWarnOnValueAppData[NoData](), want: false},
{name: "pointer", got: shouldWarnOnValueAppData[*testDB](), want: false},
{name: "interface", got: shouldWarnOnValueAppData[dbIface](), want: false},
{name: "map", got: shouldWarnOnValueAppData[map[string]int](), want: false},
{name: "struct", got: shouldWarnOnValueAppData[testDB](), want: true},
{name: "int", got: shouldWarnOnValueAppData[int](), want: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.got != tt.want {
t.Fatalf("shouldWarnOnValueAppData = %v, want %v", tt.got, tt.want)
}
})
}
}
func TestSetAppDataMarksValueWarningOnce(t *testing.T) {
type testDB struct{}
bot := &Bot[testDB]{logger: slog.CreateLogger()}
bot.SetAppData(testDB{})
if !bot.warnedValueData {
t.Fatal("expected value-typed app data to mark warning state")
}
ptrBot := &Bot[*testDB]{logger: slog.CreateLogger()}
ptrBot.SetAppData(&testDB{})
if ptrBot.warnedValueData {
t.Fatal("did not expect pointer-typed app data to mark warning state")
}
}
func TestSetObserverAndGetObserver(t *testing.T) {
bot := &Bot[NoData]{logger: slog.CreateLogger()}
observer := testObserver{}
if got := bot.GetObserver(); got != nil {
t.Fatalf("expected nil observer by default, got %#v", got)
}
bot.SetObserver(observer)
if got := bot.GetObserver(); got == nil {
t.Fatal("expected observer to be stored")
}
}
func TestSetObserverNilClearsObserver(t *testing.T) {
bot := &Bot[NoData]{logger: slog.CreateLogger()}
bot.SetObserver(testObserver{})
if bot.GetObserver() == nil {
t.Fatal("expected observer to be set")
}
bot.SetObserver(nil)
if got := bot.GetObserver(); got != nil {
t.Fatalf("expected nil observer after clearing, got %#v", got)
}
}
func TestRunWithContextRejectsSecondRun(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
cancel()
bot := &Bot[NoData]{
logger: slog.CreateLogger(),
prefixes: []string{"/"},
plugins: []Plugin[NoData]{{name: "demo"}},
updateQueue: make(chan *tgapi.Update, 1),
maxWorkers: 1,
}
if err := bot.RunWithContext(ctx); err != nil {
t.Fatalf("first RunWithContext returned error: %v", err)
}
if err := bot.RunWithContext(ctx); !errors.Is(err, ErrBotAlreadyRun) {
t.Fatalf("expected ErrBotAlreadyRun on second run, got %v", err)
}
}
func TestCloseDoesNotDeleteWebhook(t *testing.T) {
requests := 0
client := &http.Client{
Transport: pollingRoundTripFunc(func(req *http.Request) (*http.Response, error) {
requests++
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("http://example.invalid").
SetHTTPClient(client),
)
uploader := tgapi.NewUploader(api)
bot := &Bot[NoData]{
logger: slog.CreateLogger(),
webHookLogger: slog.CreateLogger(),
api: api,
uploader: uploader,
}
if err := bot.Close(); err != nil {
t.Fatalf("Close returned error: %v", err)
}
if requests != 0 {
t.Fatalf("Close performed unexpected remote requests: got %d want 0", requests)
}
}
func TestRunWithContextEmitsPollingRetryAndErrorEvents(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":500,"description":"boom"}`)),
}, nil
}),
}
api := tgapi.NewAPI(
tgapi.NewAPIOpts("token").
SetAPIUrl("http://example.invalid").
SetHTTPClient(client),
)
defer func() {
_ = api.Close()
}()
bot := &Bot[NoData]{
logger: slog.CreateLogger(),
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 <= 0 || got.Err == nil {
t.Fatalf("unexpected polling retry event: %#v", got)
}
if len(observer.errors) != 1 {
t.Fatalf("expected one polling error event, got %d", len(observer.errors))
}
if got := observer.errors[0]; got.HandlerKind != HandlerPollingKind || got.HandlerName != "getUpdates" || got.Plugin != "bot" || got.Err == nil || got.UserFacing {
t.Fatalf("unexpected polling error event: %#v", got)
}
}
func TestBotConfigurationFreezesAfterRunStarts(t *testing.T) {
type testDB struct{ Name string }
makeBot := func() *Bot[*testDB] {
return &Bot[*testDB]{
logger: slog.CreateLogger(),
prefixes: []string{"/"},
updateTypes: []tgapi.UpdateType{tgapi.UpdateTypeMessage},
payloadType: BotPayloadBase64,
strictPayloadType: false,
errorTemplate: "%s",
l10n: &L10n{},
draftProvider: &DraftProvider{},
sessionStore: NewMemorySessionStore(),
sceneScopePriority: []SceneScope{SceneScopeUserChat, SceneScopeChat, SceneScopeUser},
}
}
tests := []struct {
name string
check func(t *testing.T, bot *Bot[*testDB])
}{
{
name: "SetAppData",
check: func(t *testing.T, bot *Bot[*testDB]) {
original := &testDB{Name: "before"}
bot.SetAppData(original)
if err := bot.beginRun(); err != nil {
t.Fatalf("beginRun returned error: %v", err)
}
t.Cleanup(bot.finishRun)
later := &testDB{Name: "after"}
bot.SetAppData(later)
if bot.appData != original {
t.Fatal("SetAppData mutated after configuration freeze")
}
},
},
{
name: "UpdateTypes",
check: func(t *testing.T, bot *Bot[*testDB]) {
original := append([]tgapi.UpdateType(nil), bot.updateTypes...)
if err := bot.beginRun(); err != nil {
t.Fatalf("beginRun returned error: %v", err)
}
t.Cleanup(bot.finishRun)
bot.SetUpdateTypes(tgapi.UpdateTypePoll)
if !reflect.DeepEqual(bot.updateTypes, original) {
t.Fatalf("UpdateTypes mutated after configuration freeze: got %v want %v", bot.updateTypes, original)
}
},
},
{
name: "AddUpdateType",
check: func(t *testing.T, bot *Bot[*testDB]) {
original := append([]tgapi.UpdateType(nil), bot.updateTypes...)
if err := bot.beginRun(); err != nil {
t.Fatalf("beginRun returned error: %v", err)
}
t.Cleanup(bot.finishRun)
bot.AddUpdateType(tgapi.UpdateTypePoll)
if !reflect.DeepEqual(bot.updateTypes, original) {
t.Fatalf("AddUpdateType mutated after configuration freeze: got %v want %v", bot.updateTypes, original)
}
},
},
{
name: "SetPayloadType",
check: func(t *testing.T, bot *Bot[*testDB]) {
if err := bot.beginRun(); err != nil {
t.Fatalf("beginRun returned error: %v", err)
}
t.Cleanup(bot.finishRun)
bot.SetPayloadType(BotPayloadJson)
if bot.payloadType != BotPayloadBase64 {
t.Fatalf("payloadType mutated after configuration freeze: got %q want %q", bot.payloadType, BotPayloadBase64)
}
},
},
{
name: "SetStrictPayloadType",
check: func(t *testing.T, bot *Bot[*testDB]) {
if err := bot.beginRun(); err != nil {
t.Fatalf("beginRun returned error: %v", err)
}
t.Cleanup(bot.finishRun)
bot.SetStrictPayloadType(true)
if bot.strictPayloadType {
t.Fatal("strictPayloadType mutated after configuration freeze")
}
},
},
{
name: "AddPrefixes",
check: func(t *testing.T, bot *Bot[*testDB]) {
original := append([]string(nil), bot.prefixes...)
if err := bot.beginRun(); err != nil {
t.Fatalf("beginRun returned error: %v", err)
}
t.Cleanup(bot.finishRun)
bot.AddPrefixes("!")
if !reflect.DeepEqual(bot.prefixes, original) {
t.Fatalf("prefixes mutated after configuration freeze: got %v want %v", bot.prefixes, original)
}
},
},
{
name: "ErrorTemplate",
check: func(t *testing.T, bot *Bot[*testDB]) {
if err := bot.beginRun(); err != nil {
t.Fatalf("beginRun returned error: %v", err)
}
t.Cleanup(bot.finishRun)
bot.SetErrorTemplate("changed")
if bot.errorTemplate != "%s" {
t.Fatalf("errorTemplate mutated after configuration freeze: got %q want %q", bot.errorTemplate, "%s")
}
},
},
{
name: "SetDraftProvider",
check: func(t *testing.T, bot *Bot[*testDB]) {
original := bot.draftProvider
if err := bot.beginRun(); err != nil {
t.Fatalf("beginRun returned error: %v", err)
}
t.Cleanup(bot.finishRun)
bot.SetDraftProvider(&DraftProvider{})
if bot.draftProvider != original {
t.Fatal("draftProvider mutated after configuration freeze")
}
},
},
{
name: "SetSessionStore",
check: func(t *testing.T, bot *Bot[*testDB]) {
original := bot.sessionStore
if err := bot.beginRun(); err != nil {
t.Fatalf("beginRun returned error: %v", err)
}
t.Cleanup(bot.finishRun)
bot.SetSessionStore(NewMemorySessionStore())
if bot.sessionStore != original {
t.Fatal("sessionStore mutated after configuration freeze")
}
},
},
{
name: "SetSceneScopePriority",
check: func(t *testing.T, bot *Bot[*testDB]) {
original := append([]SceneScope(nil), bot.sceneScopePriority...)
if err := bot.beginRun(); err != nil {
t.Fatalf("beginRun returned error: %v", err)
}
t.Cleanup(bot.finishRun)
bot.SetSceneScopePriority([]SceneScope{SceneScopeUser})
if !reflect.DeepEqual(bot.sceneScopePriority, original) {
t.Fatalf("sceneScopePriority mutated after configuration freeze: got %v want %v", bot.sceneScopePriority, original)
}
},
},
{
name: "AddL10n",
check: func(t *testing.T, bot *Bot[*testDB]) {
original := bot.l10n
if err := bot.beginRun(); err != nil {
t.Fatalf("beginRun returned error: %v", err)
}
t.Cleanup(bot.finishRun)
bot.SetL10n(&L10n{})
if bot.l10n != original {
t.Fatal("l10n mutated after configuration freeze")
}
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.check(t, makeBot())
})
}
}
func TestAddPluginsAndRuntimeRegistrationsNoOpAfterRunStarts(t *testing.T) {
bot := &Bot[NoData]{
logger: slog.CreateLogger(),
prefixes: []string{"/"},
middlewares: []Middleware[NoData]{NewMiddleware("base", func(ctx *MsgContext, db NoData) bool { return true })},
runners: []Runner[NoData]{NewRunner("base", func(bot *Bot[NoData]) error { return nil })},
}
plugin := NewPlugin[NoData]("late")
if err := bot.beginRun(); err != nil {
t.Fatalf("beginRun returned error: %v", err)
}
defer bot.finishRun()
bot.AddPlugins(plugin)
bot.AddMiddleware(NewMiddleware("late", func(ctx *MsgContext, db NoData) bool { return true }))
bot.AddRunner(NewRunner("late", func(bot *Bot[NoData]) error { return nil }))
if len(bot.plugins) != 0 {
t.Fatalf("expected AddPlugins to be ignored after configuration freeze, got %d plugins", len(bot.plugins))
}
if len(bot.middlewares) != 1 {
t.Fatalf("expected AddMiddleware to be ignored after configuration freeze, got %d middlewares", len(bot.middlewares))
}
if len(bot.runners) != 1 {
t.Fatalf("expected AddRunner to be ignored after configuration freeze, got %d runners", len(bot.runners))
}
}
+178
View File
@@ -0,0 +1,178 @@
package laniakea
import (
"context"
"fmt"
"maps"
"reflect"
"strings"
"time"
"git.scuroneko.dev/scuroneko/extypes"
"git.scuroneko.dev/scuroneko/laniakea/tgapi"
"git.scuroneko.dev/scuroneko/laniakea/utils"
"git.scuroneko.dev/scuroneko/slog"
"github.com/alitto/pond/v2"
)
func (bot *Bot[T]) enqueueUpdate(ctx context.Context, update tgapi.Update) error {
select {
case <-ctx.Done():
return ctx.Err()
case bot.updateQueue <- new(update):
return nil
}
}
func (bot *Bot[T]) startUpdateWorkers(ctx context.Context) {
pool := pond.NewPool(bot.maxWorkers)
for update := range bot.updateQueue {
u := update // capture loop variable
pool.Submit(func() {
bot.handle(ctx, u)
})
}
pool.Stop() // Wait for all tasks to complete and stop the pool
}
func (bot *Bot[T]) initLoggers(opts *BotOpts) {
level := slog.FATAL
if opts.Debug {
level = slog.DEBUG
}
bot.logger = utils.CreateLogger("BOT", level)
if opts.WriteToFile {
path := fmt.Sprintf("%s/main.log", strings.TrimRight(opts.LoggerBasePath, "/"))
logger, err := utils.CreateFileLogger("BOT", level, path)
if err != nil {
bot.logger.Errorln(err)
} else {
bot.logger = logger
}
}
if opts.UseRequestLogger {
bot.RequestLogger = utils.CreateLogger("REQUESTS", level)
if opts.WriteToFile {
path := fmt.Sprintf("%s/requests.log", strings.TrimRight(opts.LoggerBasePath, "/"))
logger, err := utils.CreateFileLogger("REQUESTS", level, path)
if err != nil {
bot.logger.Errorln(err)
} else {
bot.RequestLogger = logger
}
}
}
}
func (bot *Bot[T]) beginRun() error {
bot.runStateMu.Lock()
defer bot.runStateMu.Unlock()
if bot.running || bot.ran {
return ErrBotAlreadyRun
}
bot.running = true
bot.ran = true
return nil
}
func (bot *Bot[T]) finishRun() {
bot.runStateMu.Lock()
bot.running = false
bot.runStateMu.Unlock()
}
func nextPollRetryDelay(prev time.Duration) time.Duration {
if prev <= 0 {
return time.Second
}
next := prev * 2
if next > 30*time.Second {
return 30 * time.Second
}
return next
}
func isNilValue[T any](v T) bool {
rv := reflect.ValueOf(v)
if !rv.IsValid() {
return true
}
switch rv.Kind() {
case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Pointer, reflect.Slice:
return rv.IsNil()
default:
return false
}
}
func shouldWarnOnValueAppData[T any]() bool {
t := reflect.TypeFor[T]()
if t == reflect.TypeFor[NoData]() {
return false
}
switch t.Kind() {
case reflect.Pointer, reflect.Interface, reflect.Map, reflect.Slice, reflect.Func, reflect.Chan:
return false
default:
return true
}
}
func clonePlugin[T AppData](p *Plugin[T]) Plugin[T] {
cloned := Plugin[T]{
name: p.name,
commands: make(map[string]*Command[T], len(p.commands)),
payloads: make(map[string]*Command[T], len(p.payloads)),
scenes: make(map[string]*Scene[T], len(p.scenes)),
middlewares: append(extypes.Slice[Middleware[T]](nil), p.middlewares...),
skipAutoCmd: p.skipAutoCmd,
logger: p.logger,
handlers: make(map[tgapi.UpdateType]CommandExecutor[T]),
onClose: p.onClose,
}
for name, command := range p.commands {
cloned.commands[name] = cloneCommand(command)
}
for name, command := range p.payloads {
cloned.payloads[name] = cloneCommand(command)
}
for name, scene := range p.scenes {
cloned.scenes[name] = cloneScene(scene)
}
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))
for name, handler := range scene.steps {
cloned.steps[name] = handler
}
for name, handler := range scene.commands {
cloned.commands[name] = handler
}
return &cloned
}
+461
View File
@@ -0,0 +1,461 @@
package laniakea
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"os"
"strings"
"time"
"git.scuroneko.dev/scuroneko/laniakea/tgapi"
"git.scuroneko.dev/scuroneko/laniakea/utils"
)
// BotWebHookOpts configures Telegram webhook registration and the local HTTP server.
type BotWebHookOpts struct {
Path string
LocalPort int
UseStatusPath bool
URL string
Certificate []byte
IPAddress string
MaxConnections int8
AllowedUpdates []tgapi.UpdateType
DropPendingUpdates bool
SecretToken string
}
// NewBotWebHookOpts returns webhook options with the default path, local port, and max connections.
func NewBotWebHookOpts() *BotWebHookOpts {
return &BotWebHookOpts{
Path: "/",
LocalPort: 8080,
MaxConnections: 40,
}
}
// SetPath sets the local HTTP path that receives Telegram webhook requests.
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 {
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 {
opts.UseStatusPath = use
return opts
}
// SetURL sets the public base URL Telegram should call for incoming updates.
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 {
opts.Certificate = certificate
return opts
}
// MustLoadCertificate loads a webhook certificate from disk and panics on failure.
func (opts *BotWebHookOpts) MustLoadCertificate(filename string) *BotWebHookOpts {
f, err := os.Open(filename)
if err != nil {
panic(err)
}
defer func() {
_ = f.Close()
}()
opts.Certificate, err = io.ReadAll(f)
if err != nil {
panic(err)
}
return opts
}
// SetIPAddress sets the fixed IP address Telegram should use for webhook delivery.
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 {
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 {
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 {
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 {
opts.SecretToken = secretToken
return opts
}
// 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
// configured through SetUpdateTypes/AddUpdateType are used. When UseStatusPath is enabled,
// SecretToken must be non-empty so the operational endpoint is not left public.
//
// 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 {
if opts == nil {
return errors.New("nil BotWebHookOpts")
}
if len(bot.prefixes) == 0 {
return ErrNoPrefixes
}
if len(bot.plugins) == 0 {
return ErrNoPlugins
}
if opts.URL == "" {
return errors.New("empty BotWebHookOpts.URL")
}
if opts.MaxConnections > 100 || opts.MaxConnections <= 0 {
return errors.New("BotWebHookOpts.MaxConnections must between 1 and 100")
}
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())
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 bot.runWebhookRuntime(ctx, func(runCtx context.Context) error {
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")
} else {
_, err = bot.api.DeleteWebhookWithContext(runCtx, tgapi.DeleteWebhook{})
if err != nil {
return err
}
bot.webHookLogger.Infof("Bot webhook deleted: %s", i.URL)
}
allowedUpdates := bot.webhookAllowedUpdates(opts)
var ok bool
if opts.Certificate != nil {
ok, err = bot.uploader.SetWebhookWithContext(runCtx, tgapi.UploadSetWebhook{
URL: fmt.Sprintf("%s%s", opts.URL, opts.Path),
IPAddress: opts.IPAddress,
MaxConnections: opts.MaxConnections,
AllowedUpdates: allowedUpdates,
DropPendingUpdates: opts.DropPendingUpdates,
SecretToken: opts.SecretToken,
}, tgapi.NewUploaderFile("certificate", opts.Certificate))
} else {
ok, err = bot.api.SetWebhookWithContext(runCtx, tgapi.SetWebhook{
URL: fmt.Sprintf("%s%s", opts.URL, opts.Path),
IPAddress: opts.IPAddress,
MaxConnections: opts.MaxConnections,
AllowedUpdates: allowedUpdates,
DropPendingUpdates: opts.DropPendingUpdates,
SecretToken: opts.SecretToken,
})
}
if err != nil {
return err
}
if !ok {
return errors.New("failed to set webhook")
}
if len(tlsFiles) == 2 {
return bot.runWebHookTLS(runCtx, opts, tlsFiles[0], tlsFiles[1])
}
return bot.runWebHook(runCtx, opts)
})
}
// 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...)
}
// 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 {
var e []error
if bot.api == nil {
e = append(e, errors.New("bot api nil"))
} else {
if _, err := bot.api.DeleteWebhook(tgapi.DeleteWebhook{}); err != nil {
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 {
e = append(e, err)
}
bot.webHookLogger = nil
}
return errors.Join(e...)
}
func (bot *Bot[T]) webhookAllowedUpdates(opts *BotWebHookOpts) []tgapi.UpdateType {
if len(opts.AllowedUpdates) > 0 {
return append([]tgapi.UpdateType(nil), opts.AllowedUpdates...)
}
return bot.GetUpdateTypes()
}
func (bot *Bot[T]) runWebhookRuntime(ctx context.Context, run func(context.Context) error) error {
if err := bot.beginRun(); err != nil {
return err
}
defer bot.finishRun()
runCtx, cancel := context.WithCancel(ctx)
defer cancel()
bot.ExecRunners(runCtx)
workersDone := make(chan struct{})
go func() {
bot.startUpdateWorkers(runCtx)
close(workersDone)
}()
runErr := run(runCtx)
cancel()
close(bot.updateQueue)
<-workersDone
bot.runnerOnceWG.Wait()
bot.runnerBgWG.Wait()
return runErr
}
func updateHandler[T any](ctx context.Context, bot *Bot[T], secret string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
defer func() {
_ = r.Body.Close()
}()
if r.Method != http.MethodPost {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
if secret != "" && r.Header.Get("X-Telegram-Bot-Api-Secret-Token") != secret {
w.WriteHeader(http.StatusForbidden)
return
}
const maxWebhookBodySize = 256 << 10 // 256 KiB
r.Body = http.MaxBytesReader(w, r.Body, maxWebhookBodySize)
data, err := io.ReadAll(r.Body)
if err != nil {
if _, ok := errors.AsType[*http.MaxBytesError](err); ok {
w.WriteHeader(http.StatusRequestEntityTooLarge)
return
}
w.WriteHeader(http.StatusBadRequest)
return
}
if len(data) == 0 {
w.WriteHeader(http.StatusBadRequest)
return
}
var up tgapi.Update
if err := json.Unmarshal(data, &up); err != nil {
w.WriteHeader(http.StatusBadRequest)
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)
if err := bot.enqueueUpdate(ctx, up); err != nil {
bot.webHookLogger.Errorln(err)
w.WriteHeader(http.StatusServiceUnavailable)
return
}
w.WriteHeader(http.StatusOK)
}
}
func statusHandler[T any](bot *Bot[T], opts *BotWebHookOpts) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
auth := ""
if r.Header.Get("Authorization") != "" {
auth = r.Header.Get("Authorization")
} 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 {
w.WriteHeader(http.StatusNotFound)
return
}
i, err := bot.api.GetWebhookInfoWithContext(r.Context())
if err != nil {
bot.webHookLogger.Errorln(err)
w.WriteHeader(http.StatusInternalServerError)
return
}
data, err := json.MarshalIndent(i, "", " ")
if err != nil {
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)
}
}
}
func (bot *Bot[T]) newWebHookMux(ctx context.Context, opts *BotWebHookOpts) *http.ServeMux {
r := http.NewServeMux()
if opts.UseStatusPath {
r.HandleFunc("/status", statusHandler[T](bot, opts))
}
r.HandleFunc(opts.Path, updateHandler[T](ctx, bot, opts.SecretToken))
return r
}
func (bot *Bot[T]) runWebHook(ctx context.Context, opts *BotWebHookOpts) error {
srv := &http.Server{
Addr: fmt.Sprintf(":%d", opts.LocalPort),
Handler: bot.newWebHookMux(ctx, opts),
}
errCh := make(chan error, 1)
go func() {
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() {
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")
}
if !strings.HasPrefix(path, "/") {
return errors.New("BotWebHookOpts.Path must start with '/'")
}
if strings.Contains(path, "?") || strings.Contains(path, "#") {
return errors.New("BotWebHookOpts.Path must not contain query or fragment")
}
if useStatusPath && path == "/status" {
return errors.New("BotWebHookOpts.Path must not be '/status' when status path is enabled")
}
return nil
}
func validateWebhookTLSFiles(tlsFiles []string) error {
switch len(tlsFiles) {
case 0, 2:
return nil
case 1:
return errors.New("you must specify both private and public keys")
default:
return errors.New("too many files; you must specify only private and public keys")
}
}
+293
View File
@@ -0,0 +1,293 @@
package laniakea
import (
"context"
"errors"
"io"
"net/http"
"net/http/httptest"
"strings"
"sync/atomic"
"testing"
"git.scuroneko.dev/scuroneko/laniakea/tgapi"
"git.scuroneko.dev/scuroneko/slog"
)
func TestEnqueueUpdateCopiesValue(t *testing.T) {
bot := &Bot[NoData]{
updateQueue: make(chan *tgapi.Update, 1),
}
update := tgapi.Update{UpdateID: 42}
if err := bot.enqueueUpdate(context.Background(), update); err != nil {
t.Fatalf("enqueueUpdate returned error: %v", err)
}
update.UpdateID = 99
got := <-bot.updateQueue
if got.UpdateID != 42 {
t.Fatalf("enqueueUpdate did not copy the update value: got %d want %d", got.UpdateID, 42)
}
}
func TestUpdateHandlerEnqueuesUpdate(t *testing.T) {
bot := &Bot[NoData]{
updateQueue: make(chan *tgapi.Update, 1),
webHookLogger: slog.CreateLogger(),
}
t.Cleanup(func() {
_ = 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)
if rec.Result().StatusCode != http.StatusOK {
t.Fatalf("unexpected status: got %d want %d", rec.Result().StatusCode, http.StatusOK)
}
select {
case got := <-bot.updateQueue:
if got.UpdateID != 7 {
t.Fatalf("unexpected update id in queue: got %d want %d", got.UpdateID, 7)
}
if got.Type != tgapi.UpdateTypeMessage {
t.Fatalf("unexpected update type in queue: got %q want %q", got.Type, tgapi.UpdateTypeMessage)
}
default:
t.Fatal("expected webhook handler to enqueue an update")
}
}
func TestRunWebhookRuntimeRejectsSecondRun(t *testing.T) {
bot := &Bot[NoData]{
logger: slog.CreateLogger(),
updateQueue: make(chan *tgapi.Update, 1),
maxWorkers: 1,
}
t.Cleanup(func() {
_ = bot.logger.Close()
})
if err := bot.runWebhookRuntime(context.Background(), func(context.Context) error { return nil }); err != nil {
t.Fatalf("first runWebhookRuntime returned error: %v", err)
}
if err := bot.runWebhookRuntime(context.Background(), func(context.Context) error { return nil }); !errors.Is(err, ErrBotAlreadyRun) {
t.Fatalf("expected ErrBotAlreadyRun on second run, got %v", err)
}
}
func TestRunWebhookRuntimeExecutesRunners(t *testing.T) {
var calls atomic.Int32
bot := &Bot[NoData]{
logger: slog.CreateLogger(),
updateQueue: make(chan *tgapi.Update, 1),
maxWorkers: 1,
runners: []Runner[NoData]{
NewRunner("runner", func(bot *Bot[NoData]) error {
calls.Add(1)
return nil
}).Onetime(true).Async(false),
},
}
t.Cleanup(func() {
_ = bot.logger.Close()
})
if err := bot.runWebhookRuntime(context.Background(), func(context.Context) error { return nil }); err != nil {
t.Fatalf("runWebhookRuntime returned error: %v", err)
}
if got := calls.Load(); got != 1 {
t.Fatalf("expected runner to execute once, got %d", got)
}
}
func TestWebhookAllowedUpdatesUsesBotUpdateTypesByDefault(t *testing.T) {
bot := &Bot[NoData]{
updateTypes: []tgapi.UpdateType{tgapi.UpdateTypeMessage, tgapi.UpdateTypeCallbackQuery},
}
opts := NewBotWebHookOpts()
got := bot.webhookAllowedUpdates(opts)
if len(got) != 2 {
t.Fatalf("unexpected allowed updates length: got %d want %d", len(got), 2)
}
if got[0] != tgapi.UpdateTypeMessage || got[1] != tgapi.UpdateTypeCallbackQuery {
t.Fatalf("unexpected allowed updates: %v", got)
}
got[0] = tgapi.UpdateTypePoll
if bot.updateTypes[0] != tgapi.UpdateTypeMessage {
t.Fatalf("webhookAllowedUpdates exposed internal slice: got %v", bot.updateTypes)
}
}
func TestValidateWebhookPath(t *testing.T) {
tests := []struct {
name string
path string
useStatusPath bool
wantErr bool
}{
{name: "root", path: "/", wantErr: false},
{name: "custom path", path: "/telegram", wantErr: false},
{name: "empty", path: "", wantErr: true},
{name: "missing slash", path: "telegram", wantErr: true},
{name: "query", path: "/telegram?x=1", wantErr: true},
{name: "fragment", path: "/telegram#main", wantErr: true},
{name: "status collision", path: "/status", useStatusPath: true, wantErr: true},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
err := validateWebhookPath(tc.path, tc.useStatusPath)
if tc.wantErr && err == nil {
t.Fatal("expected error, got nil")
}
if !tc.wantErr && err != nil {
t.Fatalf("unexpected error: %v", err)
}
})
}
}
func TestValidateWebhookTLSFiles(t *testing.T) {
tests := []struct {
name string
files []string
wantErr bool
}{
{name: "no tls", files: nil, wantErr: false},
{name: "two files", files: []string{"key.pem", "cert.pem"}, wantErr: false},
{name: "one file", files: []string{"cert.pem"}, wantErr: true},
{name: "three files", files: []string{"a", "b", "c"}, wantErr: true},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
err := validateWebhookTLSFiles(tc.files)
if tc.wantErr && err == nil {
t.Fatal("expected error, got nil")
}
if !tc.wantErr && err != nil {
t.Fatalf("unexpected error: %v", err)
}
})
}
}
func TestUpdateHandlerRejectsOversizedBody(t *testing.T) {
bot := &Bot[NoData]{
updateQueue: make(chan *tgapi.Update, 1),
webHookLogger: slog.CreateLogger(),
}
t.Cleanup(func() {
_ = 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)
if rec.Result().StatusCode != http.StatusRequestEntityTooLarge {
t.Fatalf("unexpected status: got %d want %d", rec.Result().StatusCode, http.StatusRequestEntityTooLarge)
}
}
func TestStatusHandlerRequiresMatchingSecret(t *testing.T) {
client := &http.Client{
Transport: pollingRoundTripFunc(func(req *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":true,"result":{"url":"https://bot.example.com/telegram"}}`)),
}, nil
}),
}
api := tgapi.NewAPI(
tgapi.NewAPIOpts("token").
SetAPIUrl("http://example.invalid").
SetHTTPClient(client),
)
defer func() {
_ = api.Close()
}()
bot := &Bot[NoData]{
api: api,
webHookLogger: slog.CreateLogger(),
}
t.Cleanup(func() {
_ = bot.webHookLogger.Close()
})
handler := statusHandler(bot, &BotWebHookOpts{SecretToken: "secret"})
tests := []struct {
name string
headerName string
headerVal string
wantStatus int
}{
{name: "missing auth", wantStatus: http.StatusNotFound},
{name: "wrong auth", headerName: "Authorization", headerVal: "wrong", wantStatus: http.StatusNotFound},
{name: "matching telegram header", headerName: "X-Telegram-Bot-Api-Secret-Token", headerVal: "secret", wantStatus: http.StatusOK},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/status", nil)
if tc.headerName != "" {
req.Header.Set(tc.headerName, tc.headerVal)
}
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Result().StatusCode != tc.wantStatus {
t.Fatalf("unexpected status: got %d want %d", rec.Result().StatusCode, tc.wantStatus)
}
})
}
}
func TestRunWebHookWithContextRejectsInvalidTLSFilesBeforeRemoteSetup(t *testing.T) {
bot := &Bot[NoData]{
prefixes: []string{"/"},
plugins: []Plugin[NoData]{{name: "demo"}},
}
opts := NewBotWebHookOpts().SetURL("https://bot.example.com")
err := bot.RunWebHookWithContext(context.Background(), opts, "cert.pem")
if err == nil {
t.Fatal("expected tls validation error, got nil")
}
if !strings.Contains(err.Error(), "both private and public keys") {
t.Fatalf("unexpected error: %v", err)
}
}
func TestRunWebHookWithContextRequiresSecretWhenStatusPathEnabled(t *testing.T) {
bot := &Bot[NoData]{
prefixes: []string{"/"},
plugins: []Plugin[NoData]{{name: "demo"}},
}
opts := NewBotWebHookOpts().
SetURL("https://bot.example.com").
SetUseStatusPath(true)
err := bot.RunWebHookWithContext(context.Background(), opts)
if err == nil {
t.Fatal("expected status-path secret validation error, got nil")
}
if !strings.Contains(err.Error(), "SecretToken required") {
t.Fatalf("unexpected error: %v", err)
}
}
+19 -13
View File
@@ -4,13 +4,14 @@ import (
"errors" "errors"
"fmt" "fmt"
"regexp" "regexp"
"sort"
"strings" "strings"
"git.nix13.pw/scuroneko/laniakea/tgapi" "git.scuroneko.dev/scuroneko/laniakea/tgapi"
) )
// CmdRegexp matches command names allowed for Telegram command registration. // CmdRegexp matches command names allowed for Telegram command registration.
var CmdRegexp = regexp.MustCompile("^[a-zA-Z0-9]+$") var CmdRegexp = regexp.MustCompile("^[_a-z0-9]{1,32}$")
// ErrTooManyCommands is returned when the total number of registered commands // ErrTooManyCommands is returned when the total number of registered commands
// exceeds Telegram's limit of 100 bot commands per bot. // exceeds Telegram's limit of 100 bot commands per bot.
@@ -20,7 +21,7 @@ var CmdRegexp = regexp.MustCompile("^[a-zA-Z0-9]+$")
// bot initialization. // bot initialization.
var ErrTooManyCommands = errors.New("too many commands. max 100") var ErrTooManyCommands = errors.New("too many commands. max 100")
// generateBotCommand builds a BotCommand description with generated usage text. // Internal helper to build a BotCommand description with generated usage text.
func generateBotCommand[T any](cmd *Command[T]) tgapi.BotCommand { func generateBotCommand[T any](cmd *Command[T]) tgapi.BotCommand {
desc := "" desc := ""
if len(cmd.description) > 0 { if len(cmd.description) > 0 {
@@ -44,13 +45,20 @@ func generateBotCommand[T any](cmd *Command[T]) tgapi.BotCommand {
return tgapi.BotCommand{Command: cmd.command, Description: usage} return tgapi.BotCommand{Command: cmd.command, Description: usage}
} }
// checkCmdRegex reports whether cmd matches CmdRegexp. // 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) }
// gatherCommandsForPlugin collects non-skipped, valid commands from one plugin. // Internal helper to collect non-skipped, valid commands from one plugin.
func gatherCommandsForPlugin[T any](pl Plugin[T]) []tgapi.BotCommand { func gatherCommandsForPlugin[T any](pl Plugin[T]) []tgapi.BotCommand {
commands := make([]tgapi.BotCommand, 0) commands := make([]tgapi.BotCommand, 0)
for _, cmd := range pl.commands { names := make([]string, 0, len(pl.commands))
for name := range pl.commands {
names = append(names, name)
}
sort.Strings(names)
for _, name := range names {
cmd := pl.commands[name]
if cmd.skipAutoCmd { if cmd.skipAutoCmd {
continue continue
} }
@@ -62,9 +70,7 @@ func gatherCommandsForPlugin[T any](pl Plugin[T]) []tgapi.BotCommand {
return commands return commands
} }
// gatherCommands collects all commands from all plugins // Internal helper to collect all auto-generated commands from registered plugins.
// and converts them into tgapi.BotCommand objects.
// See gatherCommandsForPlugin.
func gatherCommands[T any](bot *Bot[T]) []tgapi.BotCommand { func gatherCommands[T any](bot *Bot[T]) []tgapi.BotCommand {
commands := make([]tgapi.BotCommand, 0) commands := make([]tgapi.BotCommand, 0)
for _, pl := range bot.plugins { for _, pl := range bot.plugins {
@@ -106,7 +112,7 @@ func (bot *Bot[T]) AutoGenerateCommands() error {
} }
// Clear existing commands to avoid duplication or stale entries // Clear existing commands to avoid duplication or stale entries
_, err := bot.api.DeleteMyCommands(tgapi.DeleteMyCommandsP{}) _, err := bot.api.DeleteMyCommands(tgapi.DeleteMyCommands{})
if err != nil { if err != nil {
return fmt.Errorf("failed to delete existing commands: %w", err) return fmt.Errorf("failed to delete existing commands: %w", err)
} }
@@ -119,7 +125,7 @@ func (bot *Bot[T]) AutoGenerateCommands() error {
} }
for _, scope := range scopes { for _, scope := range scopes {
_, err = bot.api.SetMyCommands(tgapi.SetMyCommandsP{ _, err = bot.api.SetMyCommands(tgapi.SetMyCommands{
Commands: commands, Commands: commands,
Scope: scope, Scope: scope,
}) })
@@ -153,12 +159,12 @@ func (bot *Bot[T]) AutoGenerateCommandsForScope(scope *tgapi.BotCommandScope) er
return ErrTooManyCommands return ErrTooManyCommands
} }
_, err := bot.api.DeleteMyCommands(tgapi.DeleteMyCommandsP{Scope: scope}) _, err := bot.api.DeleteMyCommands(tgapi.DeleteMyCommands{Scope: scope})
if err != nil { if err != nil {
return fmt.Errorf("failed to delete existing commands: %w", err) return fmt.Errorf("failed to delete existing commands: %w", err)
} }
_, err = bot.api.SetMyCommands(tgapi.SetMyCommandsP{ _, err = bot.api.SetMyCommands(tgapi.SetMyCommands{
Commands: commands, Commands: commands,
Scope: scope, Scope: scope,
}) })
+27 -6
View File
@@ -4,13 +4,14 @@ import (
"errors" "errors"
"io" "io"
"net/http" "net/http"
"reflect"
"strconv" "strconv"
"strings" "strings"
"sync/atomic" "sync/atomic"
"testing" "testing"
"git.nix13.pw/scuroneko/laniakea/tgapi" "git.scuroneko.dev/scuroneko/laniakea/tgapi"
"git.nix13.pw/scuroneko/slog" "git.scuroneko.dev/scuroneko/slog"
) )
type roundTripFunc func(*http.Request) (*http.Response, error) type roundTripFunc func(*http.Request) (*http.Response, error)
@@ -42,16 +43,16 @@ func TestAutoGenerateCommandsChecksLimitBeforeDelete(t *testing.T) {
} }
}() }()
plugin := NewPlugin[NoDB]("overflow") plugin := NewPlugin[NoData]("overflow")
exec := func(ctx *MsgContext, db *NoDB) {} exec := func(ctx *MsgContext, db NoData) error { return nil }
for i := 0; i < 101; i++ { for i := 0; i < 101; i++ {
plugin.AddCommand(NewCommand(exec, "cmd"+strconv.Itoa(i))) plugin.AddCommand(NewCommand(exec, "cmd"+strconv.Itoa(i)))
} }
bot := &Bot[NoDB]{ bot := &Bot[NoData]{
api: api, api: api,
logger: slog.CreateLogger(), logger: slog.CreateLogger(),
plugins: []Plugin[NoDB]{*plugin}, plugins: []Plugin[NoData]{*plugin},
} }
err := bot.AutoGenerateCommands() err := bot.AutoGenerateCommands()
@@ -62,3 +63,23 @@ func TestAutoGenerateCommandsChecksLimitBeforeDelete(t *testing.T) {
t.Fatalf("expected no HTTP calls before limit validation, got %d", calls.Load()) t.Fatalf("expected no HTTP calls before limit validation, got %d", calls.Load())
} }
} }
func TestGatherCommandsForPluginReturnsSortedCommands(t *testing.T) {
plugin := NewPlugin[NoData]("sorted")
exec := func(ctx *MsgContext, db NoData) error { return nil }
plugin.AddCommand(NewCommand(exec, "zeta"))
plugin.AddCommand(NewCommand(exec, "alpha"))
plugin.AddCommand(NewCommand(exec, "mid"))
commands := gatherCommandsForPlugin(*plugin)
got := make([]string, 0, len(commands))
for _, cmd := range commands {
got = append(got, cmd.Command)
}
want := []string{"alpha", "mid", "zeta"}
if !reflect.DeepEqual(got, want) {
t.Fatalf("unexpected command order: got %v want %v", got, want)
}
}
+19 -41
View File
@@ -1,55 +1,33 @@
/* /*
Package laniakea provides a modular, extensible framework for building scalable Telegram bots. Package laniakea provides a modular, extensible framework for building scalable Telegram bots.
It offers a fluent API for configuration and separates concerns through several core concepts: Core concepts:
- Bot: The central instance managing API communication, update processing, logging, - Bot manages Telegram API access, update processing, logging, rate limiting, and dependency injection.
rate limiting, and dependency injection. Created via NewBot[T]. - Plugins group commands, payloads, and non-command update handlers behind shared middleware.
- MsgContext provides access to the current update and reply/edit/delete helpers.
- Plugins: Organize commands and payloads into reusable units. - InlineKeyboard builds callback-driven keyboards and structured payloads.
A plugin can have multiple commands and shared middlewares. - DraftProvider accumulates multi-step replies before sending them.
- L10n stores key-based translations with fallback behavior.
- Commands: Named bot commands with descriptions, argument validation, and - Runners execute startup or background tasks alongside the polling loop.
execution logic. Automatically registrable across different chat scopes.
- Middleware: Functions that intercept and modify updates before they reach plugins.
Useful for authentication, logging, validation, etc. Return false to stop processing.
- MsgContext: Provides access to the incoming update and convenient methods for
responding, editing, deleting, and translating messages. Includes built-in rate limiting
and error handling. ⚠️ MarkdownV2 methods require manual escaping via EscapeMarkdownV2().
- InlineKeyboard: A fluent builder for constructing inline keyboards with styled buttons,
icons, URLs, and structured callback data (JSON or Base64).
- DraftProvider: Manages ephemeral, multi-step message drafts with automatic ID generation
(random or linear). Drafts can be built incrementally and flushed atomically.
- L10n: Simple key-based localization system with fallback language support.
- Runners: Background goroutines for periodic tasks or oneoff initialization,
with configurable timeouts and async execution.
- RateLimiting & Logging: Builtin rate limiter (respects Telegram's retry_after)
and structured logging (JSON stdout + optional file output) with requestlevel tracing.
- Dependency Injection: Pass any custom database context (e.g., *sql.DB) to all handlers
via the type parameter T in Bot[T].
Example usage: Example usage:
bot := laniakea.NewBot[mydb.DBContext](laniakea.LoadOptsFromEnv()). bot, err := laniakea.NewBot[*mydb.AppData](laniakea.LoadOptsFromEnv())
DatabaseContext(&myDB). if err != nil {
return err
}
bot.SetAppData(myDB).
AddUpdateType(tgapi.UpdateTypeMessage). AddUpdateType(tgapi.UpdateTypeMessage).
AddPrefixes("/", "!"). AddPrefixes("/", "!").
AddPlugins(&startPlugin, &helpPlugin). AddPlugins(&startPlugin, &helpPlugin).
AddMiddleware(&authMiddleware, &logMiddleware). AddMiddleware(authMiddleware, logMiddleware).
AddRunner(&cleanupRunner). AddRunner(cleanupRunner).
AddL10n(l10n.New()) SetL10n(l10n.New())
bot.Run() return bot.Run()
All public methods are safe for concurrent use unless stated otherwise. Configure bots, plugins, and localization before starting Run, RunWithContext, or RunWebHookWithContext.
Direct field access is not recommended; use provided accessors (e.g., GetDBContext, SetUpdateOffset). Runtime accessors are safe for concurrent use unless stated otherwise.
*/ */
package laniakea package laniakea
+17 -18
View File
@@ -1,18 +1,14 @@
package laniakea package laniakea
import ( import (
"errors"
"math/rand/v2" "math/rand/v2"
"sync" "sync"
"sync/atomic" "sync/atomic"
"git.nix13.pw/scuroneko/laniakea/tgapi" "git.scuroneko.dev/scuroneko/laniakea/tgapi"
) )
// ErrDraftChatIDZero is returned when a draft is used without setting a chat ID. // Interface for generating unique draft IDs.
var ErrDraftChatIDZero = errors.New("zero draft chat ID")
// draftIdGenerator defines an interface for generating unique draft IDs.
type draftIdGenerator interface { type draftIdGenerator interface {
// Next returns the next unique draft ID. // Next returns the next unique draft ID.
Next() uint64 Next() uint64
@@ -38,12 +34,9 @@ func (g *LinearDraftIdGenerator) Next() uint64 {
return g.lastId.Add(1) return g.lastId.Add(1)
} }
// DraftProvider manages a collection of Drafts and provides methods to create and // DraftProvider manages a collection of Drafts and a shared draft ID generator.
// configure them. It holds shared configuration (chat, parse mode, entities) and
// a draft ID generator.
// //
// DraftProvider is NOT thread-safe. Concurrent access from multiple goroutines // DraftProvider is safe for concurrent use.
// requires external synchronization.
type DraftProvider struct { type DraftProvider struct {
mu sync.RWMutex mu sync.RWMutex
api *tgapi.API api *tgapi.API
@@ -133,10 +126,7 @@ type Draft struct {
// NewDraft creates a new draft with the provided parse mode. // NewDraft creates a new draft with the provided parse mode.
// //
// The draft inherits the provider's chatID, messageThreadID, and entities. // The caller must set a chat with SetChat before Push or Flush.
// If parseMode is zero, the provider's default parseMode is used.
//
// Panics if chatID is zero — call SetChat() on the provider first.
func (p *DraftProvider) NewDraft(parseMode tgapi.ParseMode) *Draft { func (p *DraftProvider) NewDraft(parseMode tgapi.ParseMode) *Draft {
id := p.generator.Next() id := p.generator.Next()
draft := &Draft{ draft := &Draft{
@@ -224,8 +214,14 @@ func (d *Draft) Flush() error {
if d.Message == "" { if d.Message == "" {
return nil return nil
} }
if d.chatID == 0 {
return ErrDraftChatIDZero
}
if err := validateMessageText(d.Message); err != nil {
return err
}
params := tgapi.SendMessageP{ params := tgapi.SendMessage{
ChatID: d.chatID, ChatID: d.chatID,
ParseMode: d.parseMode, ParseMode: d.parseMode,
Entities: d.entities, Entities: d.entities,
@@ -242,13 +238,16 @@ func (d *Draft) Flush() error {
return err return err
} }
// push is the internal helper for Push(). It updates the server draft via SendMessageDraft. // Internal helper for Push that updates the server-side draft.
func (d *Draft) push(text string) error { func (d *Draft) push(text string) error {
if d.chatID == 0 { if d.chatID == 0 {
return ErrDraftChatIDZero return ErrDraftChatIDZero
} }
d.Message += text d.Message += text
params := tgapi.SendMessageDraftP{ if err := validateMessageText(d.Message); err != nil {
return err
}
params := tgapi.SendMessageDraft{
ChatID: d.chatID, ChatID: d.chatID,
DraftID: d.ID, DraftID: d.ID,
Text: d.Message, Text: d.Message,
+55
View File
@@ -0,0 +1,55 @@
package laniakea
import (
"errors"
"strings"
"testing"
"git.scuroneko.dev/scuroneko/laniakea/tgapi"
"git.scuroneko.dev/scuroneko/slog"
)
func TestDraftFlushRequiresChatID(t *testing.T) {
draft := NewRandomDraftProvider(&tgapi.API{}).NewDraft(tgapi.ParseNone)
draft.Message = "hello"
if err := draft.Flush(); err != ErrDraftChatIDZero {
t.Fatalf("expected ErrDraftChatIDZero, got %v", err)
}
}
func TestMsgContextNewDraftWorksWithoutLimiter(t *testing.T) {
ctx := &MsgContext{
Api: &tgapi.API{},
Msg: &tgapi.Message{
Chat: &tgapi.Chat{ID: 42, Type: tgapi.ChatTypePrivate},
},
Logger: slog.CreateLogger(),
draftProvider: NewRandomDraftProvider(&tgapi.API{}),
}
draft := ctx.NewDraft()
if draft == nil {
t.Fatal("expected draft")
}
if draft.chatID != 42 {
t.Fatalf("unexpected chat id: %d", draft.chatID)
}
}
func TestDraftFlushRejectsLongMessage(t *testing.T) {
draft := NewRandomDraftProvider(&tgapi.API{}).NewDraft(tgapi.ParseNone).SetChat(42, 0)
draft.Message = strings.Repeat("a", maxMessageTextLen+1)
if err := draft.Flush(); !errors.Is(err, ErrMessageTooLong) {
t.Fatalf("expected ErrMessageTooLong, got %v", err)
}
}
func TestDraftPushRejectsLongMessage(t *testing.T) {
draft := NewRandomDraftProvider(&tgapi.API{}).NewDraft(tgapi.ParseNone).SetChat(42, 0)
if err := draft.Push(strings.Repeat("a", maxMessageTextLen+1)); !errors.Is(err, ErrMessageTooLong) {
t.Fatalf("expected ErrMessageTooLong, got %v", err)
}
}
+59
View File
@@ -0,0 +1,59 @@
package laniakea
import "errors"
type classifiedError struct {
err error
userVisible bool
internalOnly bool
}
func (e *classifiedError) Error() string {
if e == nil || e.err == nil {
return ""
}
return e.err.Error()
}
func (e *classifiedError) Unwrap() error {
if e == nil {
return nil
}
return e.err
}
// AsUserError marks err as safe to show to the user through the centralized
// handler error flow.
func AsUserError(err error) error {
if err == nil {
return nil
}
return &classifiedError{err: err, userVisible: true}
}
// AsInternalError marks err as internal-only so it will be logged but not sent
// to the user through the centralized handler error flow.
func AsInternalError(err error) error {
if err == nil {
return nil
}
return &classifiedError{err: err, internalOnly: true}
}
// IsUserError reports whether err was explicitly marked as user-visible.
func IsUserError(err error) bool {
var classified *classifiedError
if !errors.As(err, &classified) {
return false
}
return classified.userVisible
}
// IsInternalError reports whether err was explicitly marked as internal-only.
func IsInternalError(err error) bool {
var classified *classifiedError
if !errors.As(err, &classified) {
return false
}
return classified.internalOnly
}
+81
View File
@@ -0,0 +1,81 @@
package laniakea
import (
"errors"
"fmt"
"unicode/utf8"
)
const (
maxMessageTextLen = 4096
maxMessageCaptionLen = 1024
)
var (
// ErrEmptyMessage reports that a required message text is empty.
ErrEmptyMessage = errors.New("empty message")
// ErrMessageTooLong reports that a message exceeds Telegram's text limit.
ErrMessageTooLong = errors.New("message too long")
// ErrCaptionTooLong reports that a caption exceeds Telegram's caption limit.
ErrCaptionTooLong = errors.New("caption too long")
// ErrMessageSplitImpossible reports that automatic message splitting cannot preserve semantics.
ErrMessageSplitImpossible = errors.New("message split is impossible")
// ErrPayloadTypeMismatch reports that callback payload encoding does not match bot policy.
ErrPayloadTypeMismatch = errors.New("payload type mismatch")
// ErrDraftChatIDZero reports that a draft has no target chat ID.
ErrDraftChatIDZero = errors.New("zero draft chat ID")
// ErrMessageNil reports that a required message value is nil.
ErrMessageNil = errors.New("message is nil")
// ErrMessageContextNil reports that an operation requires ctx.Msg but none is set.
ErrMessageContextNil = errors.New("message context is nil")
// ErrEditTargetMissing reports that an edit operation has no message target.
ErrEditTargetMissing = errors.New("edit target is missing")
// ErrCallbackMessageMissing reports that a callback operation has no callback message target.
ErrCallbackMessageMissing = errors.New("callback message is missing")
// ErrDraftProviderNil reports that draft creation was requested without a draft provider.
ErrDraftProviderNil = errors.New("draft provider is nil")
// ErrAPIIsNil reports that an operation requires an API client but none is set.
ErrAPIIsNil = errors.New("api is nil")
// ErrMessageIDZero reports that an operation requires a non-zero message ID.
ErrMessageIDZero = errors.New("message ID is zero")
// 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.
ErrBindArgsTargetNotStruct = errors.New("bind args: dst must point to a struct")
// ErrBindArgsUnsupportedFieldType reports that BindArgs encountered an unsupported field kind.
ErrBindArgsUnsupportedFieldType = errors.New("bind args: unsupported field type")
// ErrBindArgsConversion reports that BindArgs could not convert a string argument into a field type.
ErrBindArgsConversion = errors.New("bind args: conversion failed")
// ErrCantFindSession reports that no scene session matches the current context.
ErrCantFindSession = errors.New("can't find session for this context")
// ErrSceneNotFound reports that the requested scene is not registered.
ErrSceneNotFound = errors.New("scene not found")
// ErrSceneStepNotFound reports that the requested scene step is not registered.
ErrSceneStepNotFound = errors.New("scene step not found")
// ErrNotInScene reports that the current context has no active scene session.
ErrNotInScene = errors.New("not in scene")
// ErrSceneEntryNotSet reports that a scene has no configured entry step.
ErrSceneEntryNotSet = errors.New("scene entry step not set")
// ErrSceneRuntimeNil reports that scene APIs were used without an attached runtime.
ErrSceneRuntimeNil = errors.New("scene runtime is nil")
)
func validateMessageText(text string) error {
length := utf8.RuneCountInString(text)
switch {
case length == 0:
return ErrEmptyMessage
case length > maxMessageTextLen:
return fmt.Errorf("%w: got %d, limit %d", ErrMessageTooLong, length, maxMessageTextLen)
default:
return nil
}
}
func validateCaptionText(text string) error {
length := utf8.RuneCountInString(text)
if length > maxMessageCaptionLen {
return fmt.Errorf("%w: got %d, limit %d", ErrCaptionTooLong, length, maxMessageCaptionLen)
}
return nil
}
+3 -3
View File
@@ -1,12 +1,12 @@
module git.nix13.pw/scuroneko/laniakea module git.scuroneko.dev/scuroneko/laniakea
go 1.26 go 1.26
retract v1.0.0-rc.5 retract v1.0.0-rc.5
require ( require (
git.nix13.pw/scuroneko/extypes v1.2.2 git.scuroneko.dev/scuroneko/extypes v1.2.3
git.nix13.pw/scuroneko/slog v1.1.2 git.scuroneko.dev/scuroneko/slog v1.1.3
github.com/alitto/pond/v2 v2.7.0 github.com/alitto/pond/v2 v2.7.0
golang.org/x/time v0.15.0 golang.org/x/time v0.15.0
) )
+4 -4
View File
@@ -1,7 +1,7 @@
git.nix13.pw/scuroneko/extypes v1.2.2 h1:N54c1ejrPs1yfIkvYuwqI7B1+8S9mDv2GqQA6sct4dk= git.scuroneko.dev/scuroneko/extypes v1.2.3 h1:n7QsfTZEn9fJNZLXGH/LkNq4cADaRk+LTu6LNMv9y6s=
git.nix13.pw/scuroneko/extypes v1.2.2/go.mod h1:b4XYk1OW1dVSiE2MT/OMuX/K/UItf1swytX6eroVYnk= git.scuroneko.dev/scuroneko/extypes v1.2.3/go.mod h1:MhYpXC6sloLOpoM2guf64eSOrz+ET/QJZ8toobc3Ors=
git.nix13.pw/scuroneko/slog v1.1.2 h1:pl7tV5FN25Yso7sLYoOgBXi9+jLo5BDJHWmHlNPjpY0= git.scuroneko.dev/scuroneko/slog v1.1.3 h1:vI4GZykn8gDb6OJ2xq+KLcEk38M7O4e/z1kzpeRHEHw=
git.nix13.pw/scuroneko/slog v1.1.2/go.mod h1:UcfRIHDqpVQHahBGM93awLDK8//AsAvOqBwwbWqMkjM= git.scuroneko.dev/scuroneko/slog v1.1.3/go.mod h1:gnDap54sfZv3EuSyZd7fjOH46aLbDFpvtN2wgFcWkgE=
github.com/alitto/pond/v2 v2.7.0 h1:c76L+yN916m/DRXjGCeUBHHu92uWnh/g1bwVk4zyyXg= github.com/alitto/pond/v2 v2.7.0 h1:c76L+yN916m/DRXjGCeUBHHu92uWnh/g1bwVk4zyyXg=
github.com/alitto/pond/v2 v2.7.0/go.mod h1:xkjYEgQ05RSpWdfSd1nM3OVv7TBhLdy7rMp3+2Nq+yE= github.com/alitto/pond/v2 v2.7.0/go.mod h1:xkjYEgQ05RSpWdfSd1nM3OVv7TBhLdy7rMp3+2Nq+yE=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
+111 -135
View File
@@ -1,160 +1,116 @@
package laniakea package laniakea
import ( import (
"context"
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"strings" "time"
"git.nix13.pw/scuroneko/laniakea/tgapi" "git.scuroneko.dev/scuroneko/laniakea/tgapi"
) )
// ErrInvalidPayloadType is returned when callback payload encoding type is unknown. // ErrInvalidPayloadType is returned when callback payload encoding type is unknown.
var ErrInvalidPayloadType = errors.New("invalid payload type") var ErrInvalidPayloadType = errors.New("invalid payload type")
func (bot *Bot[T]) handle(u *tgapi.Update) { func (bot *Bot[T]) handle(parentCtx context.Context, u *tgapi.Update) {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
bot.logger.Errorln(fmt.Sprintf("panic in handle: %v", r)) bot.logger.Errorln(fmt.Sprintf("panic in handle: %v", r))
} }
}() }()
startTime := time.Now()
ctx := &MsgContext{ ctx, cancel := context.WithCancel(parentCtx)
defer cancel()
msgCtx := &MsgContext{
Update: *u, Api: bot.api, Update: *u, Api: bot.api,
botLogger: bot.logger, Logger: bot.logger,
errorTemplate: bot.errorTemplate, errorTemplate: bot.errorTemplate,
l10n: bot.l10n, l10n: bot.l10n,
draftProvider: bot.draftProvider, draftProvider: bot.draftProvider,
sceneRuntime: bot,
observer: bot.observer,
payloadType: bot.payloadType, payloadType: bot.payloadType,
ctx: ctx,
} }
bot.prepareUpdateCtx(u, msgCtx)
bot.safeEmitEvent(ctx, UpdateReceivedEvent{
UpdateID: u.UpdateID,
UpdateType: u.Type,
FromID: msgCtx.FromID,
ChatID: msgCtx.ChatID,
})
for _, middleware := range bot.middlewares { for _, middleware := range bot.middlewares {
if !middleware.Execute(ctx, bot.dbContext) { if !middleware.Execute(msgCtx, bot.appData) {
return return
} }
} }
if u.CallbackQuery != nil { sceneHandled, err := bot.tryHandleScene(msgCtx)
bot.handleCallback(u, ctx)
} else {
bot.handleMessage(u, ctx)
}
}
func (bot *Bot[T]) handleMessage(update *tgapi.Update, ctx *MsgContext) {
if update.Message == nil {
return
}
if update.Message.From == nil {
return
}
var text string
if len(update.Message.Text) > 0 {
text = update.Message.Text
} else {
text = update.Message.Caption
}
text = strings.TrimSpace(text)
prefix, hasPrefix := bot.checkPrefixes(text)
if !hasPrefix {
return
}
ctx.Prefix = prefix
ctx.FromID = update.Message.From.ID
ctx.From = update.Message.From
ctx.Msg = update.Message
// Убираем префикс
text = strings.TrimSpace(text[len(prefix):])
// Извлекаем команду как первое слово
spaceIndex := strings.Index(text, " ")
var cmd string
var args string
if spaceIndex == -1 {
cmd = text
args = ""
} else {
cmd = text[:spaceIndex]
args = strings.TrimSpace(text[spaceIndex:])
}
if strings.Contains(cmd, "@") {
botUsername := bot.username
if botUsername != "" && strings.HasSuffix(cmd, "@"+botUsername) {
cmd = cmd[:len(cmd)-len("@"+botUsername)] // убираем @botname
}
}
// Ищем команду по точному совпадению
for _, plugin := range bot.plugins {
if _, exists := plugin.commands[cmd]; exists {
ctx.Text = args
ctx.Args = strings.Fields(args) // Убирает лишние пробелы
if !plugin.executeMiddlewares(ctx, bot.dbContext) {
return
}
ctx.Logger = plugin.logger
if ctx.Logger == nil {
ctx.Logger = ctx.botLogger
}
plugin.executeCmd(cmd, ctx, bot.dbContext)
return
}
}
}
func (bot *Bot[T]) handleCallback(update *tgapi.Update, ctx *MsgContext) {
data, err := bot.decodePayload(update.CallbackQuery.Data)
if err != nil { if err != nil {
bot.logger.Errorln(err) bot.logger.Errorln(err)
bot.safeEmitEvent(ctx, UpdateHandledEvent{
UpdateID: u.UpdateID,
UpdateType: u.Type,
FromID: msgCtx.FromID,
ChatID: msgCtx.ChatID,
Duration: time.Since(startTime),
Handled: false,
})
bot.safeEmitEvent(ctx, ErrorEvent{
UpdateID: u.UpdateID,
UpdateType: u.Type,
Plugin: "bot",
HandlerKind: HandlerSceneKind,
HandlerName: "tryHandleScene",
FromID: msgCtx.FromID,
ChatID: msgCtx.ChatID,
Err: err,
UserFacing: false,
})
return
}
if sceneHandled {
bot.safeEmitEvent(ctx, UpdateHandledEvent{
UpdateID: u.UpdateID,
UpdateType: u.Type,
FromID: msgCtx.FromID,
ChatID: msgCtx.ChatID,
Duration: time.Since(startTime),
Handled: true,
})
return return
} }
ctx.FromID = update.CallbackQuery.From.ID handled := false
ctx.From = &update.CallbackQuery.From switch u.Type {
if update.CallbackQuery.Message != nil { case tgapi.UpdateTypeMessage, tgapi.UpdateTypeChannelPost:
ctx.Msg = update.CallbackQuery.Message handled = bot.handleMessage(u, msgCtx)
ctx.CallbackMsgId = update.CallbackQuery.Message.MessageID case tgapi.UpdateTypeCallbackQuery:
} handled = bot.handleCallback(u, msgCtx)
if update.CallbackQuery.InlineMessageID != nil { default:
ctx.InlineMsgId = *update.CallbackQuery.InlineMessageID handled = bot.handleUpdate(u, msgCtx)
}
ctx.CallbackQueryId = update.CallbackQuery.ID
ctx.Args = data.Args
for _, plugin := range bot.plugins {
_, ok := plugin.payloads[data.Command]
if !ok {
continue
}
if !plugin.executeMiddlewares(ctx, bot.dbContext) {
return
}
ctx.Logger = plugin.logger
if ctx.Logger == nil {
ctx.Logger = ctx.botLogger
}
plugin.executePayload(data.Command, ctx, bot.dbContext)
return
} }
bot.safeEmitEvent(ctx, UpdateHandledEvent{
UpdateID: u.UpdateID,
UpdateType: u.Type,
FromID: msgCtx.FromID,
ChatID: msgCtx.ChatID,
Duration: time.Since(startTime),
Handled: handled,
})
} }
func (bot *Bot[T]) checkPrefixes(text string) (string, bool) { func cloneMsgContext(src *MsgContext) *MsgContext {
for _, prefix := range bot.prefixes { cloned := *src
if prefix == "" { if src.Args != nil {
continue cloned.Args = append([]string(nil), src.Args...)
}
if strings.HasPrefix(text, prefix) {
return prefix, true
}
} }
return "", false return &cloned
} }
func encodeJsonPayload(d CallbackData) (string, error) { func encodeJsonPayload(d CallbackData) (string, error) {
@@ -164,11 +120,13 @@ func encodeJsonPayload(d CallbackData) (string, error) {
} }
return string(b), nil return string(b), nil
} }
func decodeJsonPayload(s string) (CallbackData, error) { func decodeJsonPayload(s string) (CallbackData, error) {
var data CallbackData var data CallbackData
err := json.Unmarshal([]byte(s), &data) err := json.Unmarshal([]byte(s), &data)
return data, err return data, err
} }
func encodeBase64Payload(d CallbackData) (string, error) { func encodeBase64Payload(d CallbackData) (string, error) {
data, err := encodeJsonPayload(d) data, err := encodeJsonPayload(d)
if err != nil { if err != nil {
@@ -179,15 +137,6 @@ func encodeBase64Payload(d CallbackData) (string, error) {
return string(dst), nil return string(dst), nil
} }
// func encodePayload(payloadType BotPayloadType, d CallbackData) (string, error) {
// switch payloadType {
// case BotPayloadBase64:
// return encodeBase64Payload(d)
// case BotPayloadJson:
// return encodeJsonPayload(d)
// }
// return "", ErrInvalidPayloadType
// }
func decodeBase64Payload(s string) (CallbackData, error) { func decodeBase64Payload(s string) (CallbackData, error) {
b, err := base64.RawURLEncoding.DecodeString(s) b, err := base64.RawURLEncoding.DecodeString(s)
if err != nil { if err != nil {
@@ -195,19 +144,46 @@ func decodeBase64Payload(s string) (CallbackData, error) {
} }
return decodeJsonPayload(string(b)) return decodeJsonPayload(string(b))
} }
func decodePayload(payloadType BotPayloadType, s string) (CallbackData, error) {
func decodePayload(payloadType BotPayloadType, s string, strict bool) (CallbackData, BotPayloadType, error) {
switch payloadType { switch payloadType {
case BotPayloadBase64: case BotPayloadBase64:
return decodeBase64Payload(s) 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
case BotPayloadJson: case BotPayloadJson:
return decodeJsonPayload(s) 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 CallbackData{}, ErrInvalidPayloadType return CallbackData{}, "", ErrInvalidPayloadType
} }
// func (bot *Bot[T]) encodePayload(d CallbackData) (string, error) {
// return encodePayload(bot.payloadType, d)
// }
func (bot *Bot[T]) decodePayload(s string) (CallbackData, error) { func (bot *Bot[T]) decodePayload(s string) (CallbackData, error) {
return decodePayload(bot.payloadType, s) data, decodedType, err := decodePayload(bot.payloadType, s, bot.strictPayloadType)
if err != nil {
return CallbackData{}, err
}
if decodedType == BotPayloadBase64 && bot.debug && bot.logger != nil {
bot.logger.Debugf("decoded callback payload base64->json: raw=%q json=%s", s, data.ToJson())
}
return data, nil
} }
+1055 -2
View File
File diff suppressed because it is too large Load Diff
+27 -20
View File
@@ -3,17 +3,16 @@ package laniakea
import ( import (
"fmt" "fmt"
"git.nix13.pw/scuroneko/extypes" "git.scuroneko.dev/scuroneko/extypes"
"git.nix13.pw/scuroneko/laniakea/tgapi" "git.scuroneko.dev/scuroneko/laniakea/tgapi"
) )
// ButtonStyleDanger, ButtonStyleSuccess, ButtonStylePrimary are predefined
// Telegram keyboard button styles for visual feedback.
//
// These values map directly to Telegram Bot API's InlineKeyboardButton style field.
const ( const (
ButtonStyleDanger tgapi.KeyboardButtonStyle = "danger" // ButtonStyleDanger marks a destructive inline keyboard action.
ButtonStyleDanger tgapi.KeyboardButtonStyle = "danger"
// ButtonStyleSuccess marks a confirmatory inline keyboard action.
ButtonStyleSuccess tgapi.KeyboardButtonStyle = "success" ButtonStyleSuccess tgapi.KeyboardButtonStyle = "success"
// ButtonStylePrimary marks a primary inline keyboard action.
ButtonStylePrimary tgapi.KeyboardButtonStyle = "primary" ButtonStylePrimary tgapi.KeyboardButtonStyle = "primary"
) )
@@ -83,8 +82,7 @@ func (b InlineKbButtonBuilder) SetCallbackDataBase64(cmd string, args ...any) In
return b return b
} }
// build converts the builder state into a tgapi.InlineKeyboardButton. // Internal helper that converts the builder state into a Telegram button.
// This method is typically called internally by InlineKeyboard.AddButton().
func (b InlineKbButtonBuilder) build() tgapi.InlineKeyboardButton { func (b InlineKbButtonBuilder) build() tgapi.InlineKeyboardButton {
return tgapi.InlineKeyboardButton{ return tgapi.InlineKeyboardButton{
Text: b.text, Text: b.text,
@@ -138,16 +136,25 @@ func NewInlineKeyboard(payloadType BotPayloadType, maxRow int) *InlineKeyboard {
} }
} }
// SetPayloadType sets the serialization format for callback data added via // SetPayloadType sets the keyboard-local serialization format for callback data added via
// AddCallbackButton and AddCallbackButtonStyle methods. // AddCallbackButton and AddCallbackButtonStyle methods.
// It should be one of BotPayloadJson or BotPayloadBase64. // It overrides the bot's default payload type for this keyboard only.
func (in *InlineKeyboard) SetPayloadType(t BotPayloadType) *InlineKeyboard { func (in *InlineKeyboard) SetPayloadType(t BotPayloadType) *InlineKeyboard {
in.payloadType = t in.payloadType = t
return in return in
} }
// append adds a button to the current line. If the line is full, it auto-flushes. // GetPayloadType returns the keyboard-local callback payload encoding type.
// This is an internal helper used by other builder methods. func (in *InlineKeyboard) GetPayloadType() BotPayloadType { return in.payloadType }
// SetMaxRow sets the maximum number of buttons appended to a row before the
// keyboard automatically starts a new line.
func (in *InlineKeyboard) SetMaxRow(maxRow int) *InlineKeyboard {
in.maxRow = maxRow
return in
}
// Internal helper that appends a button and auto-flushes a full row.
func (in *InlineKeyboard) append(button tgapi.InlineKeyboardButton) *InlineKeyboard { func (in *InlineKeyboard) append(button tgapi.InlineKeyboardButton) *InlineKeyboard {
if in.CurrentLine.Len() == in.maxRow { if in.CurrentLine.Len() == in.maxRow {
in.AddLine() in.AddLine()
@@ -235,12 +242,12 @@ type CallbackData struct {
// (int, string, bool, float64) but may not serialize complex structs meaningfully. // (int, string, bool, float64) but may not serialize complex structs meaningfully.
// //
// Use this to build callback payloads for bot command routing. // Use this to build callback payloads for bot command routing.
func NewCallbackData(command string, args ...any) *CallbackData { func NewCallbackData(command string, args ...any) CallbackData {
stringArgs := make([]string, len(args)) stringArgs := make([]string, len(args))
for i, arg := range args { for i, arg := range args {
stringArgs[i] = fmt.Sprint(arg) stringArgs[i] = fmt.Sprint(arg)
} }
return &CallbackData{ return CallbackData{
Command: command, Command: command,
Args: stringArgs, Args: stringArgs,
} }
@@ -253,8 +260,8 @@ func NewCallbackData(command string, args ...any) *CallbackData {
// //
// This fallback ensures the bot receives a valid JSON payload even if internal // This fallback ensures the bot receives a valid JSON payload even if internal
// errors occur — avoiding "invalid callback_data" errors from Telegram. // errors occur — avoiding "invalid callback_data" errors from Telegram.
func (d *CallbackData) ToJson() string { func (d CallbackData) ToJson() string {
data, err := encodeJsonPayload(*d) data, err := encodeJsonPayload(d)
if err != nil { if err != nil {
// Fallback: return minimal valid JSON to avoid Telegram API rejection // Fallback: return minimal valid JSON to avoid Telegram API rejection
return `{"cmd":""}` return `{"cmd":""}`
@@ -264,8 +271,8 @@ func (d *CallbackData) ToJson() string {
// ToBase64 serializes the CallbackData to a JSON string and then encodes it as Base64. // ToBase64 serializes the CallbackData to a JSON string and then encodes it as Base64.
// Returns an empty string if serialization or encoding fails. // Returns an empty string if serialization or encoding fails.
func (d *CallbackData) ToBase64() string { func (d CallbackData) ToBase64() string {
s, err := encodeBase64Payload(*d) s, err := encodeBase64Payload(d)
if err != nil { if err != nil {
return `` return ``
} }
@@ -275,7 +282,7 @@ func (d *CallbackData) ToBase64() string {
// Encode serializes the CallbackData according to the specified payload type. // Encode serializes the CallbackData according to the specified payload type.
// Supported types: BotPayloadJson and BotPayloadBase64. // Supported types: BotPayloadJson and BotPayloadBase64.
// For unknown types, returns an empty string. // For unknown types, returns an empty string.
func (d *CallbackData) Encode(t BotPayloadType) string { func (d CallbackData) Encode(t BotPayloadType) string {
switch t { switch t {
case BotPayloadBase64: case BotPayloadBase64:
return d.ToBase64() return d.ToBase64()
+97
View File
@@ -0,0 +1,97 @@
package laniakea
import (
"errors"
"reflect"
"strings"
"testing"
)
func TestInlineKeyboardWrapsRowsAndEncodesJSONPayloads(t *testing.T) {
kb := NewInlineKeyboardJson(2).
AddCallbackButton("A", "cmd", 1).
AddCallbackButton("B", "cmd", 2).
AddCallbackButton("C", "cmd", 3)
markup := kb.Get()
if got := len(markup.InlineKeyboard); got != 2 {
t.Fatalf("unexpected row count: %d", got)
}
if got := len(markup.InlineKeyboard[0]); got != 2 {
t.Fatalf("unexpected first row size: %d", got)
}
if got := len(markup.InlineKeyboard[1]); got != 1 {
t.Fatalf("unexpected second row size: %d", got)
}
if !strings.Contains(markup.InlineKeyboard[0][0].CallbackData, `"cmd":"cmd"`) {
t.Fatalf("expected JSON callback payload, got %q", markup.InlineKeyboard[0][0].CallbackData)
}
}
func TestInlineKeyboardBuilderPreservesConfiguredButtonFields(t *testing.T) {
kb := NewInlineKeyboardBase64(3).
AddButton(
NewInlineKbButton("Docs").
SetStyle(ButtonStylePrimary).
SetUrl("https://example.test"),
)
button := kb.Get().InlineKeyboard[0][0]
if button.Style != ButtonStylePrimary {
t.Fatalf("unexpected style: %q", button.Style)
}
if button.URL != "https://example.test" {
t.Fatalf("unexpected url: %q", button.URL)
}
}
func TestInlineKeyboardGetPayloadTypeReturnsLocalOverride(t *testing.T) {
kb := NewInlineKeyboardJson(2)
if got := kb.GetPayloadType(); got != BotPayloadJson {
t.Fatalf("unexpected initial payload type: %q", got)
}
kb.SetPayloadType(BotPayloadBase64)
if got := kb.GetPayloadType(); got != BotPayloadBase64 {
t.Fatalf("unexpected updated payload type: %q", got)
}
}
func TestDecodePayloadAcceptsBase64KeyboardPayloadWhenBotPrefersJSON(t *testing.T) {
kb := NewInlineKeyboardBase64(1).
AddCallbackButton("A", "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 TestDecodePayloadAcceptsJSONKeyboardPayloadWhenBotPrefersBase64(t *testing.T) {
kb := NewInlineKeyboardJson(1).
AddCallbackButton("A", "cmd", 1, "two")
got, _, err := decodePayload(BotPayloadBase64, 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 TestDecodePayloadStrictRejectsMismatchedType(t *testing.T) {
kb := NewInlineKeyboardBase64(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)
}
}
+33 -38
View File
@@ -1,21 +1,18 @@
package laniakea package laniakea
// DictEntry represents a single localized entry with language-to-text mappings. import "sync"
// Example: {"ru": "Привет", "en": "Hello"}.
// DictEntry maps language codes to translated strings.
type DictEntry map[string]string type DictEntry map[string]string
// L10n is a localization manager that maps keys to language-specific strings. // L10n stores translations with a configurable fallback language and is safe for concurrent use.
type L10n struct { type L10n struct {
entries map[string]DictEntry // Map of translation keys to language dictionaries mu sync.RWMutex
fallbackLang string // Language code to use when requested language is missing entries map[string]DictEntry
fallbackLang string
} }
// NewL10n creates a new L10n instance with the specified fallback language. // NewL10n creates a localization store with the given fallback language.
// The fallback language is used when a requested language is not available
// for a given key.
//
// Example: NewL10n("en") will return "Hello" for key "greeting" if "ru" is requested
// but no "ru" entry exists.
func NewL10n(fallbackLanguage string) *L10n { func NewL10n(fallbackLanguage string) *L10n {
return &L10n{ return &L10n{
entries: make(map[string]DictEntry), entries: make(map[string]DictEntry),
@@ -23,54 +20,52 @@ func NewL10n(fallbackLanguage string) *L10n {
} }
} }
// AddDictEntry adds a new translation entry for the given key. // AddDictEntry stores translations for key.
// The value must be a DictEntry mapping language codes (e.g., "en", "ru") to their translated strings.
//
// If a key already exists, it is overwritten.
//
// Returns the L10n instance for method chaining.
func (l *L10n) AddDictEntry(key string, value DictEntry) *L10n { func (l *L10n) AddDictEntry(key string, value DictEntry) *L10n {
l.entries[key] = value l.mu.Lock()
defer l.mu.Unlock()
if l.entries == nil {
l.entries = make(map[string]DictEntry)
}
l.entries[key] = cloneDictEntry(value)
return l return l
} }
// GetFallbackLanguage returns the currently configured fallback language code. // GetFallbackLanguage returns the currently configured fallback language code.
func (l *L10n) GetFallbackLanguage() string { func (l *L10n) GetFallbackLanguage() string {
l.mu.RLock()
defer l.mu.RUnlock()
return l.fallbackLang return l.fallbackLang
} }
// Translate retrieves the translation for the given key and language. // Translate returns the translation for key in lang, falling back to the configured language or the key itself.
//
// Behavior:
// - If the key exists and the language has a translation → returns the translation
// - If the key exists but the language is missing → returns the fallback language's value
// - If the key does not exist → returns the key string itself (as fallback)
//
// Example:
//
// l.AddDictEntry("greeting", DictEntry{"en": "Hello", "ru": "Привет"})
// l.Translate("en", "greeting") → "Hello"
// l.Translate("es", "greeting") → "Hello" (fallback to "en")
// l.Translate("en", "unknown") → "unknown" (key not found)
//
// This behavior ensures that missing translations do not break UI or logs —
// instead, the original key is displayed, making it easy to identify gaps.
func (l *L10n) Translate(lang, key string) string { func (l *L10n) Translate(lang, key string) string {
l.mu.RLock()
defer l.mu.RUnlock()
entries, exists := l.entries[key] entries, exists := l.entries[key]
if !exists { if !exists {
return key // Return key as fallback when translation is missing return key
} }
// Try requested language
if translation, ok := entries[lang]; ok { if translation, ok := entries[lang]; ok {
return translation return translation
} }
// Fall back to configured fallback language
if fallback, ok := entries[l.fallbackLang]; ok { if fallback, ok := entries[l.fallbackLang]; ok {
return fallback return fallback
} }
// If fallback language is also missing, return the key
return key return key
} }
func cloneDictEntry(src DictEntry) DictEntry {
if src == nil {
return nil
}
cloned := make(DictEntry, len(src))
for lang, text := range src {
cloned[lang] = text
}
return cloned
}
+77
View File
@@ -0,0 +1,77 @@
package laniakea
import (
"fmt"
"sync"
"testing"
)
func TestL10nTranslateUsesFallbackAndKey(t *testing.T) {
l10n := NewL10n("en").
AddDictEntry("greeting", DictEntry{"en": "Hello", "ru": "Privet"}).
AddDictEntry("partial", DictEntry{"ru": "Tolko ru"})
tests := []struct {
name string
lang string
key string
want string
}{
{name: "exact match", lang: "ru", key: "greeting", want: "Privet"},
{name: "fallback language", lang: "es", key: "greeting", want: "Hello"},
{name: "missing fallback returns key", lang: "en", key: "partial", want: "partial"},
{name: "unknown key returns key", lang: "en", key: "unknown", want: "unknown"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := l10n.Translate(tt.lang, tt.key); got != tt.want {
t.Fatalf("unexpected translation: got %q want %q", got, tt.want)
}
})
}
}
func TestL10nAddDictEntryCopiesInput(t *testing.T) {
l10n := NewL10n("en")
entry := DictEntry{"en": "Hello"}
l10n.AddDictEntry("greeting", entry)
entry["en"] = "Mutated"
if got := l10n.Translate("en", "greeting"); got != "Hello" {
t.Fatalf("unexpected translation after external mutation: got %q", got)
}
}
func TestL10nZeroValueIsUsable(t *testing.T) {
var l10n L10n
l10n.AddDictEntry("greeting", DictEntry{"en": "Hello"})
if got := l10n.Translate("en", "greeting"); got != "Hello" {
t.Fatalf("unexpected translation from zero-value l10n: got %q", got)
}
}
func TestL10nConcurrentAccess(t *testing.T) {
l10n := NewL10n("en")
l10n.AddDictEntry("base", DictEntry{"en": "Hello"})
var wg sync.WaitGroup
for i := 0; i < 8; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
for j := 0; j < 100; j++ {
l10n.AddDictEntry(fmt.Sprintf("key-%d-%d", i, j), DictEntry{"en": "value"})
_ = l10n.Translate("en", "base")
}
}(i)
}
wg.Wait()
if got := l10n.Translate("en", "base"); got != "Hello" {
t.Fatalf("unexpected translation after concurrent access: got %q", got)
}
}
+3 -3
View File
@@ -4,7 +4,7 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"git.nix13.pw/scuroneko/laniakea/tgapi" "git.scuroneko.dev/scuroneko/laniakea/tgapi"
) )
// Updates fetches new updates from Telegram API using long polling. // Updates fetches new updates from Telegram API using long polling.
@@ -43,8 +43,8 @@ import (
func (bot *Bot[T]) Updates(ctx context.Context) ([]tgapi.Update, error) { func (bot *Bot[T]) Updates(ctx context.Context) ([]tgapi.Update, error) {
offset := bot.GetUpdateOffset() offset := bot.GetUpdateOffset()
params := tgapi.UpdateParams{ params := tgapi.UpdateParams{
Offset: Ptr(offset), Offset: new(offset),
Timeout: Ptr(30), Timeout: new(30),
AllowedUpdates: bot.GetUpdateTypes(), AllowedUpdates: bot.GetUpdateTypes(),
} }
+393 -66
View File
@@ -2,40 +2,87 @@ package laniakea
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"log"
"reflect"
"strconv"
"strings"
"time" "time"
"git.nix13.pw/scuroneko/laniakea/tgapi" "git.scuroneko.dev/scuroneko/laniakea/tgapi"
"git.nix13.pw/scuroneko/slog" "git.scuroneko.dev/scuroneko/slog"
) )
// MsgContext holds the context for handling a Telegram message or callback query. // MsgContext holds the normalized per-update context passed to command, payload,
// It provides methods to respond, edit, delete, and translate messages, as well as // scene, middleware, and generic update handlers.
// manage inline keyboards and message drafts. //
// MsgContext 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.
// - From and FromID are populated only when the update exposes a user identity.
// - Chat and ChatID are populated only when the update exposes a chat identity.
// - Text, Args, and Prefix are populated only by command or scene command routing.
// - 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,
// reply helpers need Msg, while inline callback edit helpers can work through
// InlineMsgId when there is no chat message.
type MsgContext struct { type MsgContext struct {
Api *tgapi.API Api *tgapi.API
Update tgapi.Update Update tgapi.Update
Msg *tgapi.Message // Msg is the normalized Telegram message for message-backed update kinds.
// It is nil for updates that do not include a message object.
Msg *tgapi.Message
// From is the normalized Telegram user for update kinds that expose one.
// It stays nil for sender-chat-only updates and update kinds without a user.
From *tgapi.User From *tgapi.User
// Chat is the normalized Telegram chat for update kinds that expose one.
// It is nil for updates that do not include a chat identity.
Chat *tgapi.Chat
// Logger is the logger assigned by the matched plugin for the current handler call. // Logger is the logger assigned by the matched plugin for the current handler call.
// It may fall back to the bot logger when the plugin has no dedicated logger. // It may fall back to the bot logger when the plugin has no dedicated logger.
Logger *slog.Logger Logger *slog.Logger
InlineMsgId string // InlineMsgId is the inline message identifier for callback queries that target
CallbackMsgId int // an inline message instead of a chat message.
InlineMsgId string
// CallbackMsgId is the message ID targeted by the current callback query when
// the callback comes from a chat message.
CallbackMsgId int
// CallbackQueryId is the Telegram callback query ID for payload handlers and
// callback-backed scene handlers.
CallbackQueryId string CallbackQueryId string
FromID int64 // FromID is the normalized sender ID when the current update exposes a user.
Prefix string // It is zero when the update has no user identity.
Text string FromID int64
Args []string // ChatID is the normalized chat ID when the current update exposes a chat.
// It is zero when the update has no chat identity.
ChatID int64
// Prefix is the matched command prefix for command routing and scene-local
// command routing. It is empty outside those flows.
Prefix string
// Text is the parsed command tail for command routing, the parsed scene-command
// tail for scene-local command routing, or the trimmed message text seen by a
// scene step/message handler. It is empty when the current routing path does
// not derive text input.
Text string
// Args contains parsed command or payload arguments for the current routing
// path. It is nil or empty when no argument vector is derived.
Args []string
errorTemplate string errorTemplate string
botLogger *slog.Logger
l10n *L10n l10n *L10n
draftProvider *DraftProvider draftProvider *DraftProvider
payloadType BotPayloadType payloadType BotPayloadType
sceneRuntime sceneRuntime
observer Observer
ctx context.Context
} }
// AnswerMessage represents a message sent or edited via MsgContext. // AnswerMessage represents a message sent or edited via MsgContext.
@@ -47,10 +94,13 @@ type AnswerMessage struct {
ctx *MsgContext // internal back-reference ctx *MsgContext // internal back-reference
} }
// edit is an internal helper to edit a message's text with optional keyboard and parse mode. // Internal helper for text edits with optional keyboard and parse mode.
// Used by Edit, EditMarkdown, EditCallback, etc.
func (ctx *MsgContext) edit(messageId int, text string, keyboard *InlineKeyboard, parseMode tgapi.ParseMode) *AnswerMessage { func (ctx *MsgContext) edit(messageId int, text string, keyboard *InlineKeyboard, parseMode tgapi.ParseMode) *AnswerMessage {
params := tgapi.EditMessageTextP{ if err := validateMessageText(text); err != nil {
ctx.Logger.Errorln(err)
return nil
}
params := tgapi.EditMessageText{
Text: text, Text: text,
ParseMode: parseMode, ParseMode: parseMode,
} }
@@ -61,15 +111,15 @@ func (ctx *MsgContext) edit(messageId int, text string, keyboard *InlineKeyboard
case ctx.InlineMsgId != "": case ctx.InlineMsgId != "":
params.InlineMessageID = ctx.InlineMsgId params.InlineMessageID = ctx.InlineMsgId
default: default:
ctx.botLogger.Errorln("Can't edit message: no valid message target") ctx.Logger.Errorln(ErrEditTargetMissing)
return nil return nil
} }
if keyboard != nil { if keyboard != nil {
params.ReplyMarkup = keyboard.Get() params.ReplyMarkup = keyboard.Get()
} }
msg, _, err := ctx.Api.EditMessageText(params) msg, _, err := ctx.Api.EditMessageTextWithContext(ctx.Context(), params)
if err != nil { if err != nil {
ctx.botLogger.Errorln(err) ctx.Logger.Errorln(err)
return nil return nil
} }
resultMessageID := messageId resultMessageID := messageId
@@ -95,11 +145,10 @@ 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.ParseMDV2)
} }
// editCallback is an internal helper to edit the message associated with a callback query. // Internal helper for editing callback-linked messages.
// Supports both regular callback messages and inline callback messages.
func (ctx *MsgContext) editCallback(text string, keyboard *InlineKeyboard, parseMode tgapi.ParseMode) *AnswerMessage { func (ctx *MsgContext) editCallback(text string, keyboard *InlineKeyboard, parseMode tgapi.ParseMode) *AnswerMessage {
if ctx.CallbackMsgId == 0 && ctx.InlineMsgId == "" { if ctx.CallbackMsgId == 0 && ctx.InlineMsgId == "" {
ctx.botLogger.Errorln("Can't edit non-callback update message") ctx.Logger.Errorln(ErrCallbackMessageMissing)
return nil return nil
} }
return ctx.edit(ctx.CallbackMsgId, text, keyboard, parseMode) return ctx.edit(ctx.CallbackMsgId, text, keyboard, parseMode)
@@ -129,10 +178,13 @@ func (ctx *MsgContext) EditCallbackfMarkdown(format string, keyboard *InlineKeyb
return ctx.editCallback(fmt.Sprintf(format, args...), keyboard, tgapi.ParseMDV2) return ctx.editCallback(fmt.Sprintf(format, args...), keyboard, tgapi.ParseMDV2)
} }
// editPhotoText edits the caption of a photo/video message. // Internal helper for media-caption edits.
// Returns nil when no valid edit target is available for the current context.
func (ctx *MsgContext) editPhotoText(messageId int, text string, kb *InlineKeyboard, parseMode tgapi.ParseMode) *AnswerMessage { func (ctx *MsgContext) editPhotoText(messageId int, text string, kb *InlineKeyboard, parseMode tgapi.ParseMode) *AnswerMessage {
params := tgapi.EditMessageCaptionP{ if err := validateCaptionText(text); err != nil {
ctx.Logger.Errorln(err)
return nil
}
params := tgapi.EditMessageCaption{
Caption: text, Caption: text,
ParseMode: parseMode, ParseMode: parseMode,
} }
@@ -143,16 +195,16 @@ func (ctx *MsgContext) editPhotoText(messageId int, text string, kb *InlineKeybo
case ctx.InlineMsgId != "": case ctx.InlineMsgId != "":
params.InlineMessageID = ctx.InlineMsgId params.InlineMessageID = ctx.InlineMsgId
default: default:
ctx.botLogger.Errorln("Can't edit caption: no valid message target") ctx.Logger.Errorln(ErrEditTargetMissing)
return nil return nil
} }
if kb != nil { if kb != nil {
params.ReplyMarkup = kb.Get() params.ReplyMarkup = kb.Get()
} }
msg, _, err := ctx.Api.EditMessageCaption(params) msg, _, err := ctx.Api.EditMessageCaptionWithContext(ctx.Context(), params)
if err != nil { if err != nil {
ctx.botLogger.Errorln(err) ctx.Logger.Errorln(err)
return nil return nil
} }
resultMessageID := messageId resultMessageID := messageId
@@ -188,14 +240,17 @@ func (m *AnswerMessage) EditCaptionKeyboardMarkdown(text string, kb *InlineKeybo
return m.ctx.editPhotoText(m.MessageID, text, kb, tgapi.ParseMDV2) return m.ctx.editPhotoText(m.MessageID, text, kb, tgapi.ParseMDV2)
} }
// answer sends a new message with optional keyboard and parse mode. // Internal helper for message replies with optional keyboard and parse mode.
// Uses API limiter to respect Telegram rate limits per chat.
func (ctx *MsgContext) answer(text string, keyboard *InlineKeyboard, parseMode tgapi.ParseMode) *AnswerMessage { func (ctx *MsgContext) answer(text string, keyboard *InlineKeyboard, parseMode tgapi.ParseMode) *AnswerMessage {
if ctx.Msg == nil { if ctx.Msg == nil {
ctx.botLogger.Errorln("Can't answer message without a message") ctx.Logger.Errorln(ErrMessageContextNil)
return nil return nil
} }
params := tgapi.SendMessageP{ if err := validateMessageText(text); err != nil {
ctx.Logger.Errorln(err)
return nil
}
params := tgapi.SendMessage{
ChatID: ctx.Msg.Chat.ID, ChatID: ctx.Msg.Chat.ID,
Text: text, Text: text,
ParseMode: parseMode, ParseMode: parseMode,
@@ -210,9 +265,9 @@ func (ctx *MsgContext) answer(text string, keyboard *InlineKeyboard, parseMode t
params.DirectMessagesTopicID = ctx.Msg.DirectMessageTopic.TopicID params.DirectMessagesTopicID = ctx.Msg.DirectMessageTopic.TopicID
} }
msg, err := ctx.Api.SendMessage(params) msg, err := ctx.Api.SendMessageWithContext(ctx.Context(), params)
if err != nil { if err != nil {
ctx.botLogger.Errorln(err) ctx.Logger.Errorln(err)
return nil return nil
} }
return &AnswerMessage{ return &AnswerMessage{
@@ -225,6 +280,14 @@ func (ctx *MsgContext) Answer(text string) *AnswerMessage {
return ctx.answer(text, nil, tgapi.ParseNone) return ctx.answer(text, nil, tgapi.ParseNone)
} }
// AnswerLong sends one or more plain-text messages if text exceeds Telegram's limit.
//
// 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 {
return ctx.answerLong(text, nil, tgapi.ParseNone)
}
// AnswerMarkdown sends a message using MarkdownV2 formatting. // AnswerMarkdown sends a message using MarkdownV2 formatting.
// //
// ⚠️ WARNING: User input must be escaped with laniakea.EscapeMarkdownV2() before passing here. // ⚠️ WARNING: User input must be escaped with laniakea.EscapeMarkdownV2() before passing here.
@@ -237,6 +300,11 @@ func (ctx *MsgContext) Answerf(template string, args ...any) *AnswerMessage {
return ctx.answer(fmt.Sprintf(template, args...), nil, tgapi.ParseNone) 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 {
return ctx.answerLong(fmt.Sprintf(template, args...), nil, tgapi.ParseNone)
}
// AnswerfMarkdown formats a string using fmt.Sprintf and sends it using MarkdownV2. // AnswerfMarkdown formats a string using fmt.Sprintf and sends it using MarkdownV2.
// //
// ⚠️ WARNING: User input must be escaped with laniakea.EscapeMarkdownV2() before passing here. // ⚠️ WARNING: User input must be escaped with laniakea.EscapeMarkdownV2() before passing here.
@@ -249,6 +317,13 @@ func (ctx *MsgContext) Keyboard(text string, kb *InlineKeyboard) *AnswerMessage
return ctx.answer(text, kb, tgapi.ParseNone) 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 {
return ctx.answerLong(text, kb, tgapi.ParseNone)
}
// KeyboardMarkdown sends a message with an inline keyboard using MarkdownV2. // KeyboardMarkdown sends a message with an inline keyboard using MarkdownV2.
// //
// ⚠️ WARNING: User input must be escaped with laniakea.EscapeMarkdownV2() before passing here. // ⚠️ WARNING: User input must be escaped with laniakea.EscapeMarkdownV2() before passing here.
@@ -256,13 +331,56 @@ func (ctx *MsgContext) KeyboardMarkdown(text string, keyboard *InlineKeyboard) *
return ctx.answer(text, keyboard, tgapi.ParseMDV2) return ctx.answer(text, keyboard, tgapi.ParseMDV2)
} }
// answerPhoto sends a photo with optional caption and keyboard. func (ctx *MsgContext) answerLong(text string, keyboard *InlineKeyboard, parseMode tgapi.ParseMode) []*AnswerMessage {
func (ctx *MsgContext) answerPhoto(photoId, text string, kb *InlineKeyboard, parseMode tgapi.ParseMode) *AnswerMessage { if parseMode != tgapi.ParseNone {
if ctx.Msg == nil { ctx.Logger.Errorln(ErrMessageSplitImpossible)
ctx.botLogger.Errorln("Can't answer message without a message")
return nil return nil
} }
params := tgapi.SendPhotoP{ if ctx.Msg == nil {
ctx.Logger.Errorln(ErrMessageContextNil)
return nil
}
if err := validateMessageText(text); err == nil {
msg := ctx.answer(text, keyboard, parseMode)
if msg == nil {
return nil
}
return []*AnswerMessage{msg}
} else if !errors.Is(err, ErrMessageTooLong) {
ctx.Logger.Errorln(err)
return nil
}
parts := SplitMessageText(text)
messages := make([]*AnswerMessage, 0, len(parts))
for i, part := range parts {
partKeyboard := (*InlineKeyboard)(nil)
if i == len(parts)-1 {
partKeyboard = keyboard
}
msg := ctx.answer(part, partKeyboard, parseMode)
if msg == nil {
break
}
messages = append(messages, msg)
}
if len(messages) == 0 {
return nil
}
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 {
if ctx.Msg == nil {
ctx.Logger.Errorln(ErrMessageContextNil)
return nil
}
if err := validateCaptionText(text); err != nil {
ctx.Logger.Errorln(err)
return nil
}
params := tgapi.SendPhoto{
ChatID: ctx.Msg.Chat.ID, ChatID: ctx.Msg.Chat.ID,
Caption: text, Caption: text,
ParseMode: parseMode, ParseMode: parseMode,
@@ -278,9 +396,9 @@ func (ctx *MsgContext) answerPhoto(photoId, text string, kb *InlineKeyboard, par
params.DirectMessagesTopicID = int(ctx.Msg.DirectMessageTopic.TopicID) params.DirectMessagesTopicID = int(ctx.Msg.DirectMessageTopic.TopicID)
} }
msg, err := ctx.Api.SendPhoto(params) msg, err := ctx.Api.SendPhotoWithContext(ctx.Context(), params)
if err != nil { if err != nil {
ctx.botLogger.Errorln(err) ctx.Logger.Errorln(err)
return nil return nil
} }
return &AnswerMessage{ return &AnswerMessage{
@@ -324,22 +442,22 @@ func (ctx *MsgContext) AnswerPhotofMarkdown(photoId, template string, args ...an
return ctx.answerPhoto(photoId, fmt.Sprintf(template, args...), nil, tgapi.ParseMDV2) return ctx.answerPhoto(photoId, fmt.Sprintf(template, args...), nil, tgapi.ParseMDV2)
} }
// delete removes a message by ID. // Internal helper that deletes a message by ID.
func (ctx *MsgContext) delete(messageId int) { func (ctx *MsgContext) delete(messageId int) {
if messageId == 0 { if messageId == 0 {
ctx.botLogger.Errorln("Can't delete message: message ID zero") ctx.Logger.Errorln(ErrMessageIDZero)
return return
} }
if ctx.Msg == nil { if ctx.Msg == nil {
ctx.botLogger.Errorln("Can't delete message: no chat message context") ctx.Logger.Errorln(ErrMessageContextNil)
return return
} }
_, err := ctx.Api.DeleteMessage(tgapi.DeleteMessageP{ _, err := ctx.Api.DeleteMessageWithContext(ctx.Context(), tgapi.DeleteMessage{
ChatID: ctx.Msg.Chat.ID, ChatID: ctx.Msg.Chat.ID,
MessageID: messageId, MessageID: messageId,
}) })
if err != nil { if err != nil {
ctx.botLogger.Errorln(err) ctx.Logger.Errorln(err)
} }
} }
@@ -349,24 +467,23 @@ func (m *AnswerMessage) Delete() { m.ctx.delete(m.MessageID) }
// CallbackDelete deletes the message that triggered the callback query. // CallbackDelete deletes the message that triggered the callback query.
func (ctx *MsgContext) CallbackDelete() { func (ctx *MsgContext) CallbackDelete() {
if ctx.CallbackMsgId == 0 { if ctx.CallbackMsgId == 0 {
ctx.botLogger.Errorln("Can't delete callback message: no callback message ID") ctx.Logger.Errorln(ErrCallbackMessageMissing)
return return
} }
ctx.delete(ctx.CallbackMsgId) ctx.delete(ctx.CallbackMsgId)
} }
// answerCallbackQuery sends a response to a callback query (optional text/alert/url). // Internal helper that answers a callback query with optional text, alert, or URL.
// Does nothing if CallbackQueryId is empty.
func (ctx *MsgContext) answerCallbackQuery(url, text string, showAlert bool) { func (ctx *MsgContext) answerCallbackQuery(url, text string, showAlert bool) {
if len(ctx.CallbackQueryId) == 0 { if len(ctx.CallbackQueryId) == 0 {
return return
} }
_, err := ctx.Api.AnswerCallbackQuery(tgapi.AnswerCallbackQueryP{ _, err := ctx.Api.AnswerCallbackQueryWithContext(ctx.Context(), tgapi.AnswerCallbackQuery{
CallbackQueryID: ctx.CallbackQueryId, CallbackQueryID: ctx.CallbackQueryId,
Text: text, ShowAlert: showAlert, URL: url, Text: text, ShowAlert: showAlert, URL: url,
}) })
if err != nil { if err != nil {
ctx.botLogger.Errorln(err) ctx.Logger.Errorln(err)
} }
} }
@@ -385,26 +502,30 @@ func (ctx *MsgContext) AnswerCbQueryUrl(u string) { ctx.answerCallbackQuery(u, "
// SendAction sends a chat action (typing, uploading_photo, etc.) to indicate bot activity. // SendAction sends a chat action (typing, uploading_photo, etc.) to indicate bot activity.
func (ctx *MsgContext) SendAction(action tgapi.ChatActionType) { func (ctx *MsgContext) SendAction(action tgapi.ChatActionType) {
if ctx.Msg == nil { if ctx.Msg == nil {
ctx.botLogger.Errorln("Can't send action without chat message context") ctx.Logger.Errorln("Can't send action without chat message context")
return return
} }
params := tgapi.SendChatActionP{ params := tgapi.SendChatAction{
ChatID: ctx.Msg.Chat.ID, Action: action, ChatID: ctx.Msg.Chat.ID, Action: action,
} }
if ctx.Msg.MessageThreadID > 0 { if ctx.Msg.MessageThreadID > 0 {
params.MessageThreadID = ctx.Msg.MessageThreadID params.MessageThreadID = ctx.Msg.MessageThreadID
} }
_, err := ctx.Api.SendChatAction(params) _, err := ctx.Api.SendChatActionWithContext(ctx.Context(), params)
if err != nil { if err != nil {
ctx.botLogger.Errorln(err) ctx.Logger.Errorln(err)
} }
} }
// error sends an error message to the user and logs it. // Internal helper that formats, sends, and logs an error.
// Uses errorTemplate to format the message.
// For callbacks: sends as callback answer (no alert).
// For regular messages: sends as plain text.
func (ctx *MsgContext) error(err error) { func (ctx *MsgContext) error(err error) {
if err == nil {
return
}
ctx.Logger.Errorln(err)
if IsInternalError(err) {
return
}
text := fmt.Sprintf(ctx.errorTemplate, err.Error()) text := fmt.Sprintf(ctx.errorTemplate, err.Error())
if ctx.CallbackQueryId != "" { if ctx.CallbackQueryId != "" {
@@ -412,7 +533,6 @@ func (ctx *MsgContext) error(err error) {
} else { } else {
ctx.answer(text, nil, tgapi.ParseNone) ctx.answer(text, nil, tgapi.ParseNone)
} }
ctx.botLogger.Errorln(err)
} }
// Error is an alias for error(). // Error is an alias for error().
@@ -420,15 +540,25 @@ func (ctx *MsgContext) Error(err error) { ctx.error(err) }
func (ctx *MsgContext) newDraft(parseMode tgapi.ParseMode) *Draft { func (ctx *MsgContext) newDraft(parseMode tgapi.ParseMode) *Draft {
if ctx.Msg == nil { if ctx.Msg == nil {
ctx.botLogger.Errorln("can't create draft: ctx.Msg is nil") ctx.Logger.Errorln(ErrMessageContextNil)
return nil
}
if ctx.Api == nil {
ctx.Logger.Errorln(ErrAPIIsNil)
return nil
}
if ctx.draftProvider == nil {
ctx.Logger.Errorln(ErrDraftProviderNil)
return nil return nil
} }
c, cancel := context.WithTimeout(context.Background(), 5*time.Second) if ctx.Api.Limiter != nil {
defer cancel() c, cancel := context.WithTimeout(ctx.Context(), 5*time.Second)
if err := ctx.Api.Limiter.Wait(c, ctx.Msg.Chat.ID); err != nil { defer cancel()
ctx.botLogger.Errorln(err) if err := ctx.Api.Limiter.Wait(c, ctx.Msg.Chat.ID); err != nil {
return nil ctx.Logger.Errorln(err)
return nil
}
} }
draft := ctx.draftProvider.NewDraft(parseMode).SetChat(ctx.Msg.Chat.ID, ctx.Msg.MessageThreadID) draft := ctx.draftProvider.NewDraft(parseMode).SetChat(ctx.Msg.Chat.ID, ctx.Msg.MessageThreadID)
@@ -463,3 +593,200 @@ func (ctx *MsgContext) Translate(key string) string {
func (ctx *MsgContext) NewInlineKeyboard(maxRow int) *InlineKeyboard { func (ctx *MsgContext) NewInlineKeyboard(maxRow int) *InlineKeyboard {
return NewInlineKeyboard(ctx.payloadType, maxRow) return NewInlineKeyboard(ctx.payloadType, maxRow)
} }
func bindPositional(args []string, dst any) error {
v := reflect.ValueOf(dst)
if v.Kind() != reflect.Pointer || v.IsNil() {
return ErrBindArgsTargetNotPointer
}
v = v.Elem()
if v.Kind() != reflect.Struct {
return ErrBindArgsTargetNotStruct
}
t := v.Type()
fields := make([]int, 0, v.NumField())
for i := 0; i < v.NumField(); i++ {
field := v.Field(i)
if !field.CanSet() {
continue
}
fields = append(fields, i)
}
argIndex := 0
for fieldPos, fieldIndex := range fields {
field := v.Field(fieldIndex)
fieldType := t.Field(fieldIndex)
if argIndex >= len(args) {
// Leave trailing fields at their zero values when arguments run out.
break
}
isLastBindableField := fieldPos == len(fields)-1
raw := args[argIndex]
if isLastBindableField && field.Kind() == reflect.String {
raw = strings.Join(args[argIndex:], " ")
}
switch field.Kind() {
case reflect.String:
field.SetString(raw)
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
n, err := strconv.ParseInt(raw, 10, 64)
if err != nil {
return fmt.Errorf("%w: field %s: %v", ErrBindArgsConversion, fieldType.Name, err)
}
field.SetInt(n)
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
n, err := strconv.ParseUint(raw, 10, 64)
if err != nil {
return fmt.Errorf("%w: field %s: %v", ErrBindArgsConversion, fieldType.Name, err)
}
field.SetUint(n)
case reflect.Float32, reflect.Float64:
f, err := strconv.ParseFloat(raw, 64)
if err != nil {
return fmt.Errorf("%w: field %s: %v", ErrBindArgsConversion, fieldType.Name, err)
}
field.SetFloat(f)
case reflect.Bool:
b, err := strconv.ParseBool(raw)
if err != nil {
return fmt.Errorf("%w: field %s: %v", ErrBindArgsConversion, fieldType.Name, err)
}
field.SetBool(b)
default:
return fmt.Errorf("%w: field %s: %s", ErrBindArgsUnsupportedFieldType, fieldType.Name, field.Kind())
}
if isLastBindableField && field.Kind() == reflect.String {
break
}
argIndex++
}
return nil
}
// BindArgs binds positional command arguments from ctx.Args into dst.
//
// Exported struct fields are filled in declaration order. When fewer arguments
// 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 {
return bindPositional(ctx.Args, dst)
}
// Context returns the request-scoped context associated with the current update.
func (ctx *MsgContext) Context() context.Context {
if ctx.ctx == nil {
return context.Background()
}
return ctx.ctx
}
func (ctx *MsgContext) emitPolicyChecked(event PolicyCheckedEvent) {
if ctx == nil || ctx.observer == nil {
return
}
defer func() {
if r := recover(); r != nil {
if ctx.Logger != nil {
ctx.Logger.Errorln(fmt.Sprintf("panic in observer policy event: %v", r))
return
}
log.Printf("panic in observer policy event: %v", r)
}
}()
ctx.observer.OnPolicyChecked(ctx.Context(), event)
}
// EnterScene enters the named scene at its configured entry step.
func (ctx *MsgContext) EnterScene(name string) error {
if ctx.sceneRuntime == nil {
return ErrSceneRuntimeNil
}
scene, ok := ctx.sceneRuntime.findScene(name)
if !ok {
return ErrSceneNotFound
}
key, ok := ctx.sceneRuntime.buildSceneKey(scene.Scope, ctx)
if !ok {
return ErrCantFindSession
}
if scene.Entry == "" {
return ErrSceneEntryNotSet
}
if _, ok := scene.Steps[scene.Entry]; !ok {
return ErrSceneStepNotFound
}
session := SceneSession{
Scene: scene.Name,
Step: scene.Entry,
}
return ctx.sceneRuntime.setSession(key, session)
}
// EnterSceneStep enters the named scene at a specific step.
func (ctx *MsgContext) EnterSceneStep(name, step string) error {
if ctx.sceneRuntime == nil {
return ErrSceneRuntimeNil
}
scene, ok := ctx.sceneRuntime.findScene(name)
if !ok {
return ErrSceneNotFound
}
if _, ok := scene.Steps[step]; !ok {
return ErrSceneStepNotFound
}
key, ok := ctx.sceneRuntime.buildSceneKey(scene.Scope, ctx)
if !ok {
return ErrCantFindSession
}
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 {
if ctx.sceneRuntime == nil {
return ErrSceneRuntimeNil
}
_, session, err := ctx.sceneRuntime.findSceneSession(ctx)
if err != nil {
return err
}
if session.Scene == "" {
return ErrNotInScene
}
scene, ok := ctx.sceneRuntime.findScene(session.Scene)
if !ok {
return ErrSceneNotFound
}
key, ok := ctx.sceneRuntime.buildSceneKey(scene.Scope, ctx)
if !ok {
return ErrCantFindSession
}
return ctx.sceneRuntime.deleteSession(key)
}
+408 -4
View File
@@ -2,13 +2,15 @@ package laniakea
import ( import (
"encoding/json" "encoding/json"
"errors"
"io" "io"
"net/http" "net/http"
"reflect"
"strings" "strings"
"testing" "testing"
"git.nix13.pw/scuroneko/laniakea/tgapi" "git.scuroneko.dev/scuroneko/laniakea/tgapi"
"git.nix13.pw/scuroneko/slog" "git.scuroneko.dev/scuroneko/slog"
) )
func TestAnswerPhotoIncludesDirectMessagesTopicID(t *testing.T) { func TestAnswerPhotoIncludesDirectMessagesTopicID(t *testing.T) {
@@ -45,10 +47,10 @@ func TestAnswerPhotoIncludesDirectMessagesTopicID(t *testing.T) {
ctx := &MsgContext{ ctx := &MsgContext{
Api: api, Api: api,
Msg: &tgapi.Message{ Msg: &tgapi.Message{
Chat: &tgapi.Chat{ID: 42, Type: string(tgapi.ChatTypePrivate)}, Chat: &tgapi.Chat{ID: 42, Type: tgapi.ChatTypePrivate},
DirectMessageTopic: &tgapi.DirectMessageTopic{TopicID: 77}, DirectMessageTopic: &tgapi.DirectMessageTopic{TopicID: 77},
}, },
botLogger: slog.CreateLogger(), Logger: slog.CreateLogger(),
} }
answer := ctx.AnswerPhoto("photo-id", "caption") answer := ctx.AnswerPhoto("photo-id", "caption")
@@ -62,3 +64,405 @@ func TestAnswerPhotoIncludesDirectMessagesTopicID(t *testing.T) {
t.Fatalf("unexpected direct_messages_topic_id: %v", got) t.Fatalf("unexpected direct_messages_topic_id: %v", got)
} }
} }
func TestBindArgsBindsScalarFields(t *testing.T) {
type input struct {
ID int
Active bool
Score float64
Name string
}
ctx := &MsgContext{Args: []string{"42", "true", "3.5", "Ada", "Lovelace"}}
var got input
if err := ctx.BindArgs(&got); err != nil {
t.Fatalf("BindArgs returned error: %v", err)
}
want := input{
ID: 42,
Active: true,
Score: 3.5,
Name: "Ada Lovelace",
}
if !reflect.DeepEqual(got, want) {
t.Fatalf("unexpected bound value: got %#v want %#v", got, want)
}
}
func TestBindArgsLeavesTrailingFieldsZeroWhenArgsRunOut(t *testing.T) {
type input struct {
ID int
Reason string
Admin bool
}
ctx := &MsgContext{Args: []string{"7"}}
var got input
if err := ctx.BindArgs(&got); err != nil {
t.Fatalf("BindArgs returned error: %v", err)
}
if got.ID != 7 {
t.Fatalf("unexpected ID: got %d want 7", got.ID)
}
if got.Reason != "" {
t.Fatalf("expected zero-value Reason, got %q", got.Reason)
}
if got.Admin {
t.Fatal("expected zero-value Admin")
}
}
func TestBindArgsRejectsInvalidTargets(t *testing.T) {
ctx := &MsgContext{Args: []string{"1"}}
if err := ctx.BindArgs(nil); !errors.Is(err, ErrBindArgsTargetNotPointer) {
t.Fatalf("expected ErrBindArgsTargetNotPointer for nil target, got %v", err)
}
var notStruct int
if err := ctx.BindArgs(&notStruct); !errors.Is(err, ErrBindArgsTargetNotStruct) {
t.Fatalf("expected ErrBindArgsTargetNotStruct for non-struct target, got %v", err)
}
}
func TestBindArgsReportsConversionFailures(t *testing.T) {
type input struct {
ID int
}
ctx := &MsgContext{Args: []string{"oops"}}
var got input
err := ctx.BindArgs(&got)
if err == nil {
t.Fatal("expected BindArgs to fail")
}
if !errors.Is(err, ErrBindArgsConversion) {
t.Fatalf("expected ErrBindArgsConversion, got %v", err)
}
if !strings.Contains(err.Error(), "field ID") {
t.Fatalf("expected field name in error, got %v", err)
}
}
func TestBindArgsRejectsUnsupportedFieldTypes(t *testing.T) {
type input struct {
Tags []string
}
ctx := &MsgContext{Args: []string{"tag"}}
var got input
err := ctx.BindArgs(&got)
if err == nil {
t.Fatal("expected BindArgs to fail")
}
if !errors.Is(err, ErrBindArgsUnsupportedFieldType) {
t.Fatalf("expected ErrBindArgsUnsupportedFieldType, got %v", err)
}
}
func TestErrorDefaultRemainsUserVisibleForMessageFlow(t *testing.T) {
var requests int
var gotBody map[string]any
client := &http.Client{
Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
requests++
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":{"message_id":9,"date":1}}`)),
}, 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 := &MsgContext{
Api: api,
Msg: &tgapi.Message{Chat: &tgapi.Chat{ID: 42, Type: tgapi.ChatTypePrivate}},
Logger: slog.CreateLogger(),
errorTemplate: "Error: %s",
}
ctx.error(errors.New("boom"))
if requests != 1 {
t.Fatalf("expected one user-facing error reply, got %d requests", requests)
}
if got := gotBody["text"]; got != "Error: boom" {
t.Fatalf("unexpected error reply text: %v", got)
}
}
func TestErrorInternalSkipsUserReplyForMessageFlow(t *testing.T) {
client := &http.Client{
Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
t.Fatal("unexpected HTTP request for internal-only 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 := &MsgContext{
Api: api,
Msg: &tgapi.Message{Chat: &tgapi.Chat{ID: 42, Type: tgapi.ChatTypePrivate}},
Logger: slog.CreateLogger(),
errorTemplate: "Error: %s",
}
ctx.error(AsInternalError(errors.New("boom")))
}
func TestErrorInternalSkipsCallbackAnswer(t *testing.T) {
client := &http.Client{
Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
t.Fatal("unexpected callback answer request for internal-only 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 := &MsgContext{
Api: api,
Logger: slog.CreateLogger(),
errorTemplate: "%s",
CallbackQueryId: "cb-1",
}
ctx.error(AsInternalError(errors.New("boom")))
}
func TestErrorUserVisibleAnswersCallback(t *testing.T) {
var requests int
var gotBody map[string]any
client := &http.Client{
Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
requests++
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 := &MsgContext{
Api: api,
Logger: slog.CreateLogger(),
errorTemplate: "Oops: %s",
CallbackQueryId: "cb-1",
}
ctx.error(AsUserError(errors.New("boom")))
if requests != 1 {
t.Fatalf("expected one callback error answer, got %d requests", requests)
}
if got := gotBody["text"]; got != "Oops: boom" {
t.Fatalf("unexpected callback error text: %v", got)
}
}
func TestAnswerRejectsEmptyMessage(t *testing.T) {
ctx := &MsgContext{
Msg: &tgapi.Message{Chat: &tgapi.Chat{ID: 42, Type: tgapi.ChatTypePrivate}},
Logger: slog.CreateLogger(),
}
if answer := ctx.Answer(""); answer != nil {
t.Fatal("expected nil answer for empty message")
}
}
func TestAnswerRejectsLongMessageWithoutSendingRequest(t *testing.T) {
client := &http.Client{
Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
t.Fatal("unexpected HTTP request")
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 := &MsgContext{
Api: api,
Msg: &tgapi.Message{Chat: &tgapi.Chat{ID: 42, Type: tgapi.ChatTypePrivate}},
Logger: slog.CreateLogger(),
}
if answer := ctx.Answer(strings.Repeat("a", maxMessageTextLen+1)); answer != nil {
t.Fatal("expected nil answer for long message")
}
}
func TestValidateMessageText(t *testing.T) {
if err := validateMessageText(""); !errors.Is(err, ErrEmptyMessage) {
t.Fatalf("expected ErrEmptyMessage, got %v", err)
}
if err := validateMessageText(strings.Repeat("a", maxMessageTextLen+1)); !errors.Is(err, ErrMessageTooLong) {
t.Fatalf("expected ErrMessageTooLong, got %v", err)
}
if err := validateMessageText("ok"); err != nil {
t.Fatalf("expected nil error, got %v", err)
}
}
func TestValidateCaptionText(t *testing.T) {
if err := validateCaptionText(strings.Repeat("a", maxMessageCaptionLen+1)); !errors.Is(err, ErrCaptionTooLong) {
t.Fatalf("expected ErrCaptionTooLong, got %v", err)
}
if err := validateCaptionText(""); err != nil {
t.Fatalf("expected nil error, got %v", err)
}
}
func TestSplitMessageTextPreservesContent(t *testing.T) {
text := "alpha beta\n" + strings.Repeat("x", maxMessageTextLen) + " omega"
parts := SplitMessageText(text)
if len(parts) < 2 {
t.Fatalf("expected multiple parts, got %d", len(parts))
}
for i, part := range parts {
if got := len([]rune(part)); got > maxMessageTextLen {
t.Fatalf("part %d exceeded limit: %d", i, got)
}
}
if got := strings.Join(parts, ""); got != text {
t.Fatalf("split/join mismatch: got %q want %q", got, text)
}
}
func TestAnswerLongSplitsRequestsAndAttachesKeyboardToLastChunk(t *testing.T) {
var requests []map[string]any
client := &http.Client{
Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
body, err := io.ReadAll(req.Body)
if err != nil {
t.Fatalf("failed to read request body: %v", err)
}
var got map[string]any
if err := json.Unmarshal(body, &got); err != nil {
t.Fatalf("failed to decode request body: %v", err)
}
requests = append(requests, got)
return &http.Response{
StatusCode: http.StatusOK,
Header: http.Header{"Content-Type": []string{"application/json"}},
Body: io.NopCloser(strings.NewReader(`{"ok":true,"result":{"message_id":9,"date":1}}`)),
}, 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 := &MsgContext{
Api: api,
Msg: &tgapi.Message{Chat: &tgapi.Chat{ID: 42, Type: tgapi.ChatTypePrivate}},
Logger: slog.CreateLogger(),
}
kb := NewInlineKeyboardJson(1).AddCallbackButton("A", "cmd")
text := strings.Repeat("a", maxMessageTextLen) + " " + strings.Repeat("b", 32)
messages := ctx.KeyboardLong(text, kb)
if got := len(messages); got != 2 {
t.Fatalf("expected 2 sent messages, got %d", got)
}
if got := len(requests); got != 2 {
t.Fatalf("expected 2 requests, got %d", got)
}
if _, ok := requests[0]["reply_markup"]; ok {
t.Fatal("did not expect keyboard on first chunk")
}
if _, ok := requests[1]["reply_markup"]; !ok {
t.Fatal("expected keyboard on final chunk")
}
gotTexts := []string{requests[0]["text"].(string), requests[1]["text"].(string)}
wantTexts := SplitMessageText(text)
if !reflect.DeepEqual(gotTexts, wantTexts) {
t.Fatalf("unexpected chunk texts: got %q want %q", gotTexts, wantTexts)
}
}
+219
View File
@@ -0,0 +1,219 @@
package laniakea
import (
"strings"
"time"
"git.scuroneko.dev/scuroneko/laniakea/tgapi"
)
func (bot *Bot[T]) handleMessage(update *tgapi.Update, ctx *MsgContext) bool {
var msg *tgapi.Message
if update.Message != nil {
msg = update.Message
} else if update.ChannelPost != nil {
msg = update.ChannelPost
} else {
return false
}
var text string
if len(msg.Text) > 0 {
text = msg.Text
} else if len(msg.Caption) > 0 {
text = msg.Caption
} else {
return false
}
prefix, cmd, args := bot.parseCommand(text)
if cmd == "" {
return false
}
ctx.Prefix = prefix
if strings.Contains(cmd, "@") {
botUsername := bot.username
if botUsername != "" && strings.HasSuffix(cmd, "@"+botUsername) {
cmd = cmd[:len(cmd)-len("@"+botUsername)] // убираем @botname
}
}
// Ищем команду по точному совпадению
for _, plugin := range bot.plugins {
if _, exists := plugin.commands[cmd]; exists {
ctx.Text = args
ctx.Args = strings.Fields(args) // Убирает лишние пробелы
if plugin.logger != nil {
ctx.Logger = plugin.logger
}
if !plugin.executeMiddlewares(ctx, bot.appData) {
return false
}
startTime := time.Now()
bot.safeEmitEvent(ctx.Context(), HandlerStartedEvent{
UpdateID: update.UpdateID,
UpdateType: update.Type,
Plugin: plugin.name,
HandlerKind: HandlerCommandKind,
HandlerName: cmd,
FromID: ctx.FromID,
ChatID: ctx.ChatID,
})
err := plugin.executeCmd(cmd, ctx, bot.appData)
handlerEndEvent := HandlerFinishedEvent{
UpdateID: update.UpdateID,
UpdateType: update.Type,
Plugin: plugin.name,
HandlerKind: HandlerCommandKind,
HandlerName: cmd,
FromID: ctx.FromID,
ChatID: ctx.ChatID,
Duration: time.Since(startTime),
}
var errorEvent *ErrorEvent = nil
if err != nil {
ctx.error(err)
handlerEndEvent.Err = err
handlerEndEvent.UserFacing = IsUserError(err)
errorEvent = &ErrorEvent{
UpdateID: update.UpdateID,
UpdateType: update.Type,
Plugin: plugin.name,
HandlerKind: HandlerCommandKind,
HandlerName: cmd,
FromID: ctx.FromID,
ChatID: ctx.ChatID,
Err: err,
UserFacing: handlerEndEvent.UserFacing,
}
}
bot.safeEmitEvent(ctx.Context(), handlerEndEvent)
if errorEvent != nil {
bot.safeEmitEvent(ctx.Context(), *errorEvent)
}
return true
}
}
return false
}
func (bot *Bot[T]) handleCallback(update *tgapi.Update, ctx *MsgContext) bool {
data, err := bot.decodePayload(update.CallbackQuery.Data)
if err != nil {
bot.logger.Errorln(err)
bot.safeEmitEvent(ctx.Context(), ErrorEvent{
UpdateID: update.UpdateID,
UpdateType: update.Type,
Plugin: "bot",
HandlerKind: HandlerPayloadKind,
HandlerName: "decodePayload",
FromID: ctx.FromID,
ChatID: ctx.ChatID,
Err: err,
UserFacing: false,
})
return false
}
ctx.Args = data.Args
for _, plugin := range bot.plugins {
_, ok := plugin.payloads[data.Command]
if !ok {
continue
}
ctx.Logger = plugin.logger
if ctx.Logger == nil {
ctx.Logger = bot.logger
}
if !plugin.executeMiddlewares(ctx, bot.appData) {
return false
}
startTime := time.Now()
bot.safeEmitEvent(ctx.Context(), HandlerStartedEvent{
UpdateID: update.UpdateID,
UpdateType: update.Type,
Plugin: plugin.name,
HandlerKind: HandlerPayloadKind,
HandlerName: data.Command,
FromID: ctx.FromID,
ChatID: ctx.ChatID,
})
err := plugin.executePayload(data.Command, ctx, bot.appData)
endEvent := HandlerFinishedEvent{
UpdateID: update.UpdateID,
UpdateType: update.Type,
Plugin: plugin.name,
HandlerKind: HandlerPayloadKind,
HandlerName: data.Command,
FromID: ctx.FromID,
ChatID: ctx.ChatID,
Duration: time.Since(startTime),
}
var errorEvent *ErrorEvent = nil
if err != nil {
ctx.error(err)
errorEvent = &ErrorEvent{
UpdateID: update.UpdateID,
UpdateType: update.Type,
Plugin: plugin.name,
HandlerKind: HandlerPayloadKind,
HandlerName: data.Command,
FromID: ctx.FromID,
ChatID: ctx.ChatID,
Err: err,
UserFacing: IsUserError(err),
}
endEvent.Err = err
endEvent.UserFacing = errorEvent.UserFacing
}
bot.safeEmitEvent(ctx.Context(), endEvent)
if errorEvent != nil {
bot.safeEmitEvent(ctx.Context(), *errorEvent)
}
return true
}
return false
}
func (bot *Bot[T]) checkPrefixes(text string) (string, bool) {
for _, prefix := range bot.prefixes {
if prefix == "" {
if bot.logger != nil {
bot.logger.Warnln("empty prefix is not allowed")
}
continue
}
if strings.HasPrefix(text, prefix) {
return prefix, true
}
}
return "", false
}
func (bot *Bot[T]) parseCommand(text string) (prefix, cmd, args string) {
if prefix, hasPrefix := bot.checkPrefixes(text); hasPrefix {
text = strings.TrimSpace(text[len(prefix):])
spaceIndex := strings.Index(text, " ")
var cmd string
var args string
if spaceIndex == -1 {
cmd = text
args = ""
} else {
cmd = text[:spaceIndex]
args = strings.TrimSpace(text[spaceIndex:])
}
return prefix, cmd, args
}
return "", "", ""
}
+184
View File
@@ -0,0 +1,184 @@
package laniakea
import (
"context"
"fmt"
"time"
"git.scuroneko.dev/scuroneko/laniakea/tgapi"
)
// HandlerEventKind identifies the kind of handler observed by runtime events.
type HandlerEventKind string
const (
// HandlerCommandKind identifies a command handler.
HandlerCommandKind HandlerEventKind = "command"
// HandlerPayloadKind identifies a callback payload handler.
HandlerPayloadKind HandlerEventKind = "payload"
// HandlerUpdateKind identifies a generic update handler.
HandlerUpdateKind HandlerEventKind = "update"
// HandlerRunnerKind identifies a background runner execution.
HandlerRunnerKind HandlerEventKind = "runner"
// HandlerPollingKind identifies polling and getUpdates runtime work.
HandlerPollingKind HandlerEventKind = "polling"
// HandlerSceneKind identifies a scene runtime handler wrapper.
HandlerSceneKind HandlerEventKind = "scene"
// HandlerSceneStepKind identifies a scene step handler.
HandlerSceneStepKind HandlerEventKind = "scene_step"
// HandlerSceneCommandKind identifies a scene-local command handler.
HandlerSceneCommandKind HandlerEventKind = "scene_command"
// HandlerSceneMessageKind identifies a scene message fallback handler.
HandlerSceneMessageKind HandlerEventKind = "scene_message"
)
// Event is the marker interface implemented by all observer runtime events.
type Event interface {
isEvent()
}
// UpdateReceivedEvent describes an update entering the bot runtime.
type UpdateReceivedEvent struct {
UpdateID int
UpdateType tgapi.UpdateType
FromID int64
ChatID int64
}
// UpdateHandledEvent describes a completed update execution path.
type UpdateHandledEvent struct {
UpdateID int
UpdateType tgapi.UpdateType
FromID int64
ChatID int64
Duration time.Duration
Handled bool
}
// HandlerStartedEvent describes a handler about to execute.
type HandlerStartedEvent struct {
UpdateID int
UpdateType tgapi.UpdateType
Plugin string
HandlerKind HandlerEventKind
HandlerName string
FromID int64
ChatID int64
}
// HandlerFinishedEvent describes a handler that has completed.
type HandlerFinishedEvent struct {
UpdateID int
UpdateType tgapi.UpdateType
Plugin string
HandlerKind HandlerEventKind
HandlerName string
FromID int64
ChatID int64
Duration time.Duration
Err error
UserFacing bool
}
// SceneTransitionEvent describes a scene state transition.
type SceneTransitionEvent struct {
Plugin string
Scene string
From string
To string
Action SceneAction
FromID int64
ChatID int64
}
// PolicyCheckedEvent describes the result of a policy evaluation.
type PolicyCheckedEvent struct {
Name string
Plugin string
FromID int64
ChatID int64
Passed bool
Err error
Internal bool
}
// RunnerFinishedEvent describes a completed background runner execution.
type RunnerFinishedEvent struct {
Name string
Duration time.Duration
Err error
}
// PollingRetryEvent describes a polling retry after a failed getUpdates call.
type PollingRetryEvent struct {
Attempt int
Delay time.Duration
Err error
}
// ErrorEvent describes an error routed through framework error handling.
type ErrorEvent struct {
UpdateID int
UpdateType tgapi.UpdateType
Plugin string
HandlerKind HandlerEventKind
HandlerName string
FromID int64
ChatID int64
Err error
UserFacing bool
}
func (UpdateReceivedEvent) isEvent() {}
func (UpdateHandledEvent) isEvent() {}
func (HandlerStartedEvent) isEvent() {}
func (HandlerFinishedEvent) isEvent() {}
func (SceneTransitionEvent) isEvent() {}
func (PolicyCheckedEvent) isEvent() {}
func (RunnerFinishedEvent) isEvent() {}
func (PollingRetryEvent) isEvent() {}
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)
OnHandlerStarted(ctx context.Context, event HandlerStartedEvent)
OnHandlerFinished(ctx context.Context, event HandlerFinishedEvent)
OnSceneTransition(ctx context.Context, event SceneTransitionEvent)
OnPolicyChecked(ctx context.Context, event PolicyCheckedEvent)
OnRunnerFinished(ctx context.Context, event RunnerFinishedEvent)
OnPollingRetry(ctx context.Context, event PollingRetryEvent)
OnError(ctx context.Context, event ErrorEvent)
}
func (bot *Bot[T]) safeEmitEvent(ctx context.Context, event Event) {
if bot.observer == nil {
return
}
defer func() {
if r := recover(); r != nil {
bot.logger.Errorln(fmt.Sprintf("panic in observer: %v", r))
}
}()
switch e := event.(type) {
case UpdateReceivedEvent:
bot.observer.OnReceiveUpdate(ctx, e)
case UpdateHandledEvent:
bot.observer.OnHandledUpdate(ctx, e)
case HandlerStartedEvent:
bot.observer.OnHandlerStarted(ctx, e)
case HandlerFinishedEvent:
bot.observer.OnHandlerFinished(ctx, e)
case SceneTransitionEvent:
bot.observer.OnSceneTransition(ctx, e)
case PolicyCheckedEvent:
bot.observer.OnPolicyChecked(ctx, e)
case RunnerFinishedEvent:
bot.observer.OnRunnerFinished(ctx, e)
case PollingRetryEvent:
bot.observer.OnPollingRetry(ctx, e)
case ErrorEvent:
bot.observer.OnError(ctx, e)
}
}
+109 -52
View File
@@ -4,11 +4,13 @@ import (
"errors" "errors"
"regexp" "regexp"
"git.nix13.pw/scuroneko/extypes" "git.scuroneko.dev/scuroneko/extypes"
"git.nix13.pw/scuroneko/slog" "git.scuroneko.dev/scuroneko/laniakea/tgapi"
"git.scuroneko.dev/scuroneko/laniakea/utils"
"git.scuroneko.dev/scuroneko/slog"
) )
// CommandValueType defines the expected type of a command argument. // CommandValueType defines the expected type of command argument.
type CommandValueType string type CommandValueType string
const ( const (
@@ -16,7 +18,7 @@ const (
CommandValueStringType CommandValueType = "string" CommandValueStringType CommandValueType = "string"
// CommandValueIntType expects a decimal integer (digits only). // CommandValueIntType expects a decimal integer (digits only).
CommandValueIntType CommandValueType = "int" CommandValueIntType CommandValueType = "int"
// CommandValueBoolType is reserved for future use (not implemented). // CommandValueBoolType expects a exact "true" or "false".
CommandValueBoolType CommandValueType = "bool" CommandValueBoolType CommandValueType = "bool"
// CommandValueAnyType accepts any input without validation. // CommandValueAnyType accepts any input without validation.
CommandValueAnyType CommandValueType = "any" CommandValueAnyType CommandValueType = "any"
@@ -38,6 +40,11 @@ var ErrCmdArgCountMismatch = errors.New("command arg count mismatch")
// ErrCmdArgRegexpMismatch is returned when an argument fails regex validation. // ErrCmdArgRegexpMismatch is returned when an argument fails regex validation.
var ErrCmdArgRegexpMismatch = errors.New("command arg regexp mismatch") 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, // CommandArg defines a single argument for a command, including type, regex,
// and whether it is required. // and whether it is required.
type CommandArg struct { type CommandArg struct {
@@ -50,12 +57,12 @@ type CommandArg struct {
// NewCommandArg creates a new CommandArg with the given text and type. // NewCommandArg creates a new CommandArg with the given text and type.
// Uses a default regex based on the type (string or int). // Uses a default regex based on the type (string or int).
// For CommandValueAnyType, no validation is performed. // For CommandValueAnyType, no validation is performed.
func NewCommandArg(text string) *CommandArg { func NewCommandArg(text string) CommandArg {
return &CommandArg{CommandValueAnyType, text, CommandRegexString, false} return CommandArg{CommandValueAnyType, text, CommandRegexString, false}
} }
// SetValueType sets expected value type and switches built-in validation regexp. // SetValueType sets expected value type and switches built-in validation regexp.
func (c *CommandArg) SetValueType(t CommandValueType) *CommandArg { func (c CommandArg) SetValueType(t CommandValueType) CommandArg {
regex := CommandRegexString regex := CommandRegexString
switch t { switch t {
case CommandValueIntType: case CommandValueIntType:
@@ -72,18 +79,19 @@ func (c *CommandArg) SetValueType(t CommandValueType) *CommandArg {
// SetRequired marks this argument as required. // SetRequired marks this argument as required.
// Returns the receiver for method chaining. // Returns the receiver for method chaining.
func (c *CommandArg) SetRequired() *CommandArg { func (c CommandArg) SetRequired() CommandArg {
c.required = true c.required = true
return c return c
} }
// CommandExecutor is the function type that executes a command. // CommandExecutor is the function type that executes a command.
// It receives the message context and a database context (generic). // It receives the message context and injected application data.
type CommandExecutor[T DbContext] func(ctx *MsgContext, dbContext *T) // 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. // Command represents a bot command with arguments, description, and executor.
// Can be registered in a Plugin and optionally skipped from auto-generation. // Can be registered in a Plugin and optionally skipped from auto-generation.
type Command[T DbContext] struct { type Command[T AppData] struct {
command string // The command trigger (e.g., "/start") command string // The command trigger (e.g., "/start")
description string // Human-readable description for help description string // Human-readable description for help
exec CommandExecutor[T] // Function to execute when command is triggered exec CommandExecutor[T] // Function to execute when command is triggered
@@ -123,14 +131,12 @@ func (c *Command[T]) SkipCommandAutoGen() *Command[T] {
return c return c
} }
// validateArgs checks if the provided arguments match the command's requirements. // Internal helper that validates provided command arguments.
// Returns ErrCmdArgCountMismatch if too few arguments are provided.
// Returns ErrCmdArgRegexpMismatch if any argument fails regex validation.
func (c *Command[T]) validateArgs(args []string) error { func (c *Command[T]) validateArgs(args []string) error {
// Count required args for i := range c.args.Len() {
requiredCount := c.args.Filter(func(a CommandArg) bool { return a.required }).Len() if i >= len(args) && c.args.Get(i).required {
if len(args) < requiredCount { return ErrCmdArgCountMismatch
return ErrCmdArgCountMismatch }
} }
// Validate each argument against its regex // Validate each argument against its regex
@@ -156,32 +162,43 @@ func (c *Command[T]) validateArgs(args []string) error {
// A Plugin is intended to be fully configured before it is passed to Bot.AddPlugins. // A Plugin is intended to be fully configured before it is passed to Bot.AddPlugins.
// After registration, treat the plugin as committed and do not mutate it further. // After registration, treat the plugin as committed and do not mutate it further.
// Post-registration changes through the original *Plugin are not a supported API. // Post-registration changes through the original *Plugin are not a supported API.
type Plugin[T DbContext] struct { type Plugin[T AppData] struct {
name string // Name of the plugin (e.g., "admin", "user") name string // Name of the plugin (e.g., "admin", "user")
commands map[string]*Command[T] // Registered commands (triggered by message) commands map[string]*Command[T] // Registered commands (triggered by message)
payloads map[string]*Command[T] // Registered payloads (triggered by callback data) payloads map[string]*Command[T] // Registered payloads (triggered by callback data)
scenes map[string]*Scene[T] // Optional scenes for multi-step interactions
middlewares extypes.Slice[Middleware[T]] // Shared middlewares for all commands/payloads 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 skipAutoCmd bool // If true, all commands in this plugin are excluded from auto-help
logger *slog.Logger logger *slog.Logger
handlers map[tgapi.UpdateType]CommandExecutor[T]
onClose func() error onClose func() error
} }
// NewPlugin creates a new Plugin with the given name. // NewPlugin creates a new Plugin with the given name.
func NewPlugin[T DbContext](name string) *Plugin[T] { func NewPlugin[T AppData](name string) *Plugin[T] {
return &Plugin[T]{ return &Plugin[T]{
name: name, name: name,
commands: make(map[string]*Command[T]), commands: make(map[string]*Command[T]),
payloads: make(map[string]*Command[T]), payloads: make(map[string]*Command[T]),
middlewares: make(extypes.Slice[Middleware[T]], 0), middlewares: make(extypes.Slice[Middleware[T]], 0),
scenes: make(map[string]*Scene[T]),
skipAutoCmd: false, skipAutoCmd: false,
logger: nil, logger: nil,
handlers: make(map[tgapi.UpdateType]CommandExecutor[T]),
} }
} }
// AddCommand registers a command in the plugin. // AddCommand registers a command in the plugin.
// The command's .command field is used as the key. // The command's .command field is used as the key.
func (p *Plugin[T]) AddCommand(command *Command[T]) *Plugin[T] { func (p *Plugin[T]) AddCommand(command *Command[T]) *Plugin[T] {
if command == nil {
if p.logger != nil {
p.logger.Warnln("trying to add nil command")
}
return p
}
p.commands[command.command] = command p.commands[command.command] = command
return p return p
} }
@@ -197,6 +214,12 @@ func (p *Plugin[T]) NewCommand(exec CommandExecutor[T], command string, args ...
// AddPayload registers a payload (e.g., callback query data) in the plugin. // AddPayload registers a payload (e.g., callback query data) in the plugin.
// Payloads are triggered by inline button callback_data, not by message text. // Payloads are triggered by inline button callback_data, not by message text.
func (p *Plugin[T]) AddPayload(command *Command[T]) *Plugin[T] { func (p *Plugin[T]) AddPayload(command *Command[T]) *Plugin[T] {
if command == nil {
if p.logger != nil {
p.logger.Warnln("trying to add nil command")
}
return p
}
p.payloads[command.command] = command p.payloads[command.command] = command
return p return p
} }
@@ -209,6 +232,49 @@ func (p *Plugin[T]) NewPayload(exec CommandExecutor[T], command string, args ...
return cmd return cmd
} }
// 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
return p
}
// UsePolicy registers a Policy as plugin middleware for all plugin handlers.
func (p *Plugin[T]) UsePolicy(name string, policy Policy[T]) *Plugin[T] {
mw := RequirePolicy(name, policy)
return p.AddMiddleware(mw)
}
// 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
}
// AddUpdateHandler registers a handler for a non-command update type.
// Message, channel post, and callback query updates stay on the command/payload flow.
func (p *Plugin[T]) AddUpdateHandler(t tgapi.UpdateType, handler CommandExecutor[T]) *Plugin[T] {
switch t {
case tgapi.UpdateTypeMessage, tgapi.UpdateTypeChannelPost, tgapi.UpdateTypeCallbackQuery:
if p.logger == nil {
logger := utils.CreateLogger(p.name, utils.GetLoggerLevel())
logger.Warnf("%s can't be registred through AddUpdateHandler. Use AddPayload/NewPayload or AddCommand/NewCommand", t)
_ = logger.Close()
return p
}
p.logger.Warnf("%s can't be registred through AddUpdateHandler. Use AddPayload/NewPayload or AddCommand/NewCommand", t)
return p
}
p.handlers[t] = handler
return p
}
// AddMiddleware adds a middleware to the plugin's global middleware chain. // AddMiddleware adds a middleware to the plugin's global middleware chain.
// Middlewares are executed before any command or payload. // Middlewares are executed before any command or payload.
func (p *Plugin[T]) AddMiddleware(middleware Middleware[T]) *Plugin[T] { func (p *Plugin[T]) AddMiddleware(middleware Middleware[T]) *Plugin[T] {
@@ -267,61 +333,52 @@ func (p *Plugin[T]) Close() error {
return errors.Join(e...) return errors.Join(e...)
} }
// executeCmd finds and executes a command by its trigger string. // Internal helper that validates and executes a command handler.
// Validates arguments and runs middlewares before executor. func (p *Plugin[T]) executeCmd(cmd string, ctx *MsgContext, db T) error {
// On error, sends an error message to the user via ctx.error().
func (p *Plugin[T]) executeCmd(cmd string, ctx *MsgContext, dbContext *T) {
command, exists := p.commands[cmd] command, exists := p.commands[cmd]
if !exists { if !exists {
ctx.error(errors.New("command not found")) return AsInternalError(errCommandNotFound)
return
} }
if err := command.validateArgs(ctx.Args); err != nil { if err := command.validateArgs(ctx.Args); err != nil {
ctx.error(err) return AsUserError(err)
return
} }
// Run command-specific middlewares // Run command-specific middlewares
for _, m := range command.middlewares { for _, m := range command.middlewares {
if !m.Execute(ctx, dbContext) { if !m.Execute(ctx, db) {
return return AsInternalError(errors.New("middleware blocked call"))
} }
} }
// Execute command // Execute command
command.exec(ctx, dbContext) return command.exec(ctx, db)
} }
// executePayload finds and executes a payload by its callback_data string. // Internal helper that validates and executes a payload handler.
// Validates arguments and runs middlewares before executor. func (p *Plugin[T]) executePayload(payload string, ctx *MsgContext, db T) error {
// On error, sends an error message to the user via ctx.error().
func (p *Plugin[T]) executePayload(payload string, ctx *MsgContext, dbContext *T) {
command, exists := p.payloads[payload] command, exists := p.payloads[payload]
if !exists { if !exists {
ctx.error(errors.New("payload not found")) return AsInternalError(errPayloadNotFound)
return
} }
if err := command.validateArgs(ctx.Args); err != nil { if err := command.validateArgs(ctx.Args); err != nil {
ctx.error(err) return AsUserError(err)
return
} }
// Run command-specific middlewares // Run command-specific middlewares
for _, m := range command.middlewares { for _, m := range command.middlewares {
if !m.Execute(ctx, dbContext) { if !m.Execute(ctx, db) {
return return AsInternalError(errors.New("middleware blocked call"))
} }
} }
// Execute payload // Execute payload
command.exec(ctx, dbContext) return command.exec(ctx, db)
} }
// executeMiddlewares runs all plugin middlewares in order. // Internal helper that runs plugin middlewares in order.
// Returns false if any middleware returns false (blocks execution). func (p *Plugin[T]) executeMiddlewares(ctx *MsgContext, db T) bool {
func (p *Plugin[T]) executeMiddlewares(ctx *MsgContext, db *T) bool {
for _, m := range p.middlewares { for _, m := range p.middlewares {
if !m.Execute(ctx, db) { if !m.Execute(ctx, db) {
return false return false
@@ -333,11 +390,11 @@ func (p *Plugin[T]) executeMiddlewares(ctx *MsgContext, db *T) bool {
// MiddlewareExecutor is the function type for middleware logic. // MiddlewareExecutor is the function type for middleware logic.
// Returns true to continue execution, false to block it. // Returns true to continue execution, false to block it.
// If async, return value is ignored. // If async, return value is ignored.
type MiddlewareExecutor[T DbContext] func(ctx *MsgContext, db *T) bool type MiddlewareExecutor[T AppData] func(ctx *MsgContext, db T) bool
// Middleware represents a reusable execution interceptor. // Middleware represents a reusable execution interceptor.
// Can be synchronous (blocking) or asynchronous (non-blocking). // Can be synchronous (blocking) or asynchronous (non-blocking).
type Middleware[T DbContext] struct { type Middleware[T AppData] struct {
name string // Human-readable name for logging/debugging name string // Human-readable name for logging/debugging
executor MiddlewareExecutor[T] // Function to execute executor MiddlewareExecutor[T] // Function to execute
order int // Optional sort order (not used yet) order int // Optional sort order (not used yet)
@@ -345,19 +402,19 @@ type Middleware[T DbContext] struct {
} }
// NewMiddleware creates a new synchronous middleware. // NewMiddleware creates a new synchronous middleware.
func NewMiddleware[T DbContext](name string, executor MiddlewareExecutor[T]) *Middleware[T] { func NewMiddleware[T AppData](name string, executor MiddlewareExecutor[T]) Middleware[T] {
return &Middleware[T]{name, executor, 0, false} return Middleware[T]{name, executor, 0, false}
} }
// SetOrder sets the execution order (currently ignored). // SetOrder sets the execution order (currently ignored).
func (m *Middleware[T]) SetOrder(order int) *Middleware[T] { func (m Middleware[T]) SetOrder(order int) Middleware[T] {
m.order = order m.order = order
return m return m
} }
// SetAsync marks the middleware to run asynchronously. // SetAsync marks the middleware to run asynchronously.
// Execution continues regardless of its return value. // Execution continues regardless of its return value.
func (m *Middleware[T]) SetAsync(async bool) *Middleware[T] { func (m Middleware[T]) SetAsync(async bool) Middleware[T] {
m.async = async m.async = async
return m return m
} }
@@ -365,7 +422,7 @@ func (m *Middleware[T]) SetAsync(async bool) *Middleware[T] {
// Execute runs the middleware. // Execute runs the middleware.
// If async, runs in a goroutine and returns true immediately. // If async, runs in a goroutine and returns true immediately.
// Otherwise, returns the result of the executor. // Otherwise, returns the result of the executor.
func (m *Middleware[T]) Execute(ctx *MsgContext, db *T) bool { func (m Middleware[T]) Execute(ctx *MsgContext, db T) bool {
if m.async { if m.async {
ctx := *ctx // copy context to avoid race condition ctx := *ctx // copy context to avoid race condition
go func(ctx MsgContext) { go func(ctx MsgContext) {
+18 -2
View File
@@ -6,7 +6,7 @@ import (
) )
func TestValidateArgsRequiresFullMatch(t *testing.T) { func TestValidateArgsRequiresFullMatch(t *testing.T) {
intCmd := NewCommand[NoDB](func(ctx *MsgContext, db *NoDB) {}, "int", *NewCommandArg("n").SetValueType(CommandValueIntType).SetRequired()) intCmd := NewCommand[NoData](func(ctx *MsgContext, db NoData) error { return nil }, "int", NewCommandArg("n").SetValueType(CommandValueIntType).SetRequired())
if err := intCmd.validateArgs([]string{"123"}); err != nil { if err := intCmd.validateArgs([]string{"123"}); err != nil {
t.Fatalf("expected valid integer argument, got %v", err) 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) t.Fatalf("expected ErrCmdArgRegexpMismatch for partial int match, got %v", err)
} }
boolCmd := NewCommand[NoDB](func(ctx *MsgContext, db *NoDB) {}, "bool", *NewCommandArg("flag").SetValueType(CommandValueBoolType).SetRequired()) boolCmd := NewCommand[NoData](func(ctx *MsgContext, db NoData) error { return nil }, "bool", NewCommandArg("flag").SetValueType(CommandValueBoolType).SetRequired())
if err := boolCmd.validateArgs([]string{"false"}); err != nil { if err := boolCmd.validateArgs([]string{"false"}); err != nil {
t.Fatalf("expected valid bool argument, got %v", err) t.Fatalf("expected valid bool argument, got %v", err)
} }
@@ -22,3 +22,19 @@ func TestValidateArgsRequiresFullMatch(t *testing.T) {
t.Fatalf("expected ErrCmdArgRegexpMismatch for partial bool match, got %v", err) t.Fatalf("expected ErrCmdArgRegexpMismatch for partial bool match, got %v", err)
} }
} }
func TestValidateArgsEnforcesRequiredArgIndex(t *testing.T) {
cmd := NewCommand[NoData](
func(ctx *MsgContext, db NoData) error { return nil },
"mixed",
NewCommandArg("optional"),
NewCommandArg("required").SetRequired(),
)
if err := cmd.validateArgs([]string{"only-optional"}); !errors.Is(err, ErrCmdArgCountMismatch) {
t.Fatalf("expected ErrCmdArgCountMismatch when required second arg is missing, got %v", err)
}
if err := cmd.validateArgs([]string{"optional", "required"}); err != nil {
t.Fatalf("expected both args to validate, got %v", err)
}
}
+224
View File
@@ -0,0 +1,224 @@
package laniakea
import (
"errors"
"fmt"
"git.scuroneko.dev/scuroneko/laniakea/tgapi"
)
// Policy defines a reusable authorization rule for the current update context.
type Policy[T AppData] func(ctx *MsgContext, 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 {
if err := p(ctx, data); err != nil {
ctx.emitPolicyChecked(PolicyCheckedEvent{
Name: name,
FromID: ctx.FromID,
ChatID: ctx.ChatID,
Passed: false,
Err: err,
Internal: IsInternalError(err),
})
ctx.error(err)
return false
}
ctx.emitPolicyChecked(PolicyCheckedEvent{
Name: name,
FromID: ctx.FromID,
ChatID: ctx.ChatID,
Passed: true,
})
return true
})
}
// AllPolicies composes policies that all must succeed.
func AllPolicies[T AppData](policies ...Policy[T]) Policy[T] {
return func(ctx *MsgContext, data T) error {
for _, p := range policies {
if err := p(ctx, data); err != nil {
return err
}
}
return nil
}
}
// 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 {
var firstDeny error
var internalErr error
for _, p := range policies {
err := p(ctx, data)
if err == nil {
return nil
}
if IsInternalError(err) {
if internalErr == nil {
internalErr = err
}
continue
}
if firstDeny == nil {
firstDeny = err
}
}
if internalErr != nil {
return internalErr
}
if firstDeny != nil {
return firstDeny
}
return AsUserError(errors.New("no policy matched"))
}
}
// 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 {
var err error
if err = policy(ctx, data); err == nil {
return AsUserError(errors.New("the action is not allowed due to policy violation"))
}
if IsInternalError(err) {
return err
}
return nil
}
}
// RequirePrivateChat allows execution only in private chats.
func RequirePrivateChat[T AppData]() Policy[T] {
return func(ctx *MsgContext, data T) error {
if ctx.Msg == nil || ctx.Msg.Chat == nil {
return AsInternalError(errors.New("private-chat policy requires message chat context"))
}
if ctx.Msg.Chat.Type != tgapi.ChatTypePrivate {
return AsUserError(errors.New("this action is only available in private chat"))
}
return nil
}
}
// RequireGroupChat allows execution only in group or supergroup chats.
func RequireGroupChat[T AppData]() Policy[T] {
return func(ctx *MsgContext, data T) error {
if ctx.Msg == nil || ctx.Msg.Chat == nil {
return AsInternalError(errors.New("group-chat policy requires message chat context"))
}
if ctx.Msg.Chat.Type != tgapi.ChatTypeGroup && ctx.Msg.Chat.Type != tgapi.ChatTypeSupergroup {
return AsUserError(errors.New("this action is only available in group chats"))
}
return nil
}
}
// RequireSupergroupChat allows execution only in supergroup chats.
func RequireSupergroupChat[T AppData]() Policy[T] {
return func(ctx *MsgContext, data T) error {
if ctx.Msg == nil || ctx.Msg.Chat == nil {
return AsInternalError(errors.New("supergroup-chat policy requires message chat context"))
}
if ctx.Msg.Chat.Type != tgapi.ChatTypeSupergroup {
return AsUserError(errors.New("this action is only available in supergroup chats"))
}
return nil
}
}
// RequireChatAdmin allows execution only for chat administrators or owners.
func RequireChatAdmin[T AppData]() Policy[T] {
return func(ctx *MsgContext, data T) error {
if ctx.FromID == 0 || ctx.ChatID == 0 {
return AsInternalError(errors.New("chat-admin policy requires message chat context"))
}
member, err := ctx.Api.GetChatMember(tgapi.GetChatMember{
ChatID: ctx.ChatID,
UserID: ctx.FromID,
})
if err != nil {
return AsInternalError(fmt.Errorf("failed to fetch chat member status: %w", err))
}
if member.Status != tgapi.ChatMemberStatusAdministrator && member.Status != tgapi.ChatMemberStatusOwner {
return AsUserError(errors.New("this action is only available to chat admins"))
}
return nil
}
}
// RequireChatCreator allows execution only for the chat owner.
func RequireChatCreator[T AppData]() Policy[T] {
return func(ctx *MsgContext, data T) error {
if ctx.FromID == 0 || ctx.ChatID == 0 {
return AsInternalError(errors.New("chat-creator policy requires message chat context"))
}
member, err := ctx.Api.GetChatMember(tgapi.GetChatMember{
ChatID: ctx.ChatID,
UserID: ctx.FromID,
})
if err != nil {
return AsInternalError(fmt.Errorf("failed to fetch chat creator: %w", err))
}
if member.Status != tgapi.ChatMemberStatusOwner {
return AsUserError(errors.New("this action is only available to the chat creator"))
}
return nil
}
}
// 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 {
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))
}
member, err := ctx.Api.GetChatMember(tgapi.GetChatMember{
ChatID: ctx.ChatID,
UserID: bot.ID,
})
if err != nil {
return AsInternalError(fmt.Errorf("failed to fetch bot member status: %w", err))
}
if member.Status != tgapi.ChatMemberStatusAdministrator && member.Status != tgapi.ChatMemberStatusOwner {
return AsUserError(errors.New("this action requires the bot to be an admin in the chat"))
}
return nil
}
}
// 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 {
if ctx.Update.CallbackQuery == nil {
return AsInternalError(errors.New("callback-user policy requires callback query context"))
}
if ctx.Update.CallbackQuery.From.IsBot {
return AsUserError(errors.New("this action is only available to human users"))
}
return nil
}
}
+281
View File
@@ -0,0 +1,281 @@
package laniakea
import (
"context"
"encoding/json"
"errors"
"io"
"net/http"
"strings"
"testing"
"git.scuroneko.dev/scuroneko/laniakea/tgapi"
"git.scuroneko.dev/scuroneko/slog"
)
func TestRequirePolicyStopsExecutionOnDeniedPolicy(t *testing.T) {
var requests int
var gotBody map[string]any
client := &http.Client{
Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
requests++
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":{"message_id":9,"date":1}}`)),
}, 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 := &MsgContext{
Api: api,
Msg: &tgapi.Message{Chat: &tgapi.Chat{ID: 42, Type: tgapi.ChatTypePrivate}},
Logger: slog.CreateLogger(),
errorTemplate: "Error: %s",
}
mw := RequirePolicy[NoData]("deny", func(ctx *MsgContext, data NoData) error {
return AsUserError(errors.New("blocked"))
})
if mw.Execute(ctx, NoData{}) {
t.Fatal("expected denied policy middleware to stop execution")
}
if requests != 1 {
t.Fatalf("expected one user-facing error reply, got %d requests", requests)
}
if got := gotBody["text"]; got != "Error: blocked" {
t.Fatalf("unexpected policy error reply text: %v", got)
}
}
func TestRequirePrivateChatAllowsPrivateChat(t *testing.T) {
ctx := &MsgContext{
Msg: &tgapi.Message{
Chat: &tgapi.Chat{ID: 42, Type: tgapi.ChatTypePrivate},
},
Logger: slog.CreateLogger(),
}
if err := RequirePrivateChat[NoData]()(ctx, NoData{}); err != nil {
t.Fatalf("RequirePrivateChat returned error: %v", err)
}
}
func TestRequirePrivateChatDeniesNonPrivateChat(t *testing.T) {
ctx := &MsgContext{
Msg: &tgapi.Message{
Chat: &tgapi.Chat{ID: -100, Type: tgapi.ChatTypeSupergroup},
},
Logger: slog.CreateLogger(),
}
err := RequirePrivateChat[NoData]()(ctx, NoData{})
if err == nil {
t.Fatal("expected RequirePrivateChat to deny non-private chats")
}
if !IsUserError(err) {
t.Fatalf("expected user-visible deny error, got %v", err)
}
}
func TestRequireChatAdminUsesNormalizedIDs(t *testing.T) {
var sawGetChatMember bool
var gotBody map[string]any
client := &http.Client{
Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
if !strings.Contains(req.URL.Path, "getChatMember") {
t.Fatalf("unexpected API method: %s", req.URL.Path)
}
sawGetChatMember = true
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":{"status":"administrator","user":{"id":55,"is_bot":false,"first_name":"tester"}}}`,
)),
}, 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 := &MsgContext{
Api: api,
ChatID: -2001,
FromID: 55,
Logger: slog.CreateLogger(),
}
if err := RequireChatAdmin[NoData]()(ctx, NoData{}); err != nil {
t.Fatalf("RequireChatAdmin returned error: %v", err)
}
if !sawGetChatMember {
t.Fatal("expected GetChatMember to be called")
}
if got := gotBody["chat_id"]; got != float64(-2001) {
t.Fatalf("unexpected chat_id in request: %v", got)
}
if got := gotBody["user_id"]; got != float64(55) {
t.Fatalf("unexpected user_id in request: %v", got)
}
}
func TestAllPoliciesReturnsFirstError(t *testing.T) {
want := AsUserError(errors.New("blocked"))
policy := AllPolicies[NoData](
func(ctx *MsgContext, data NoData) error { return nil },
func(ctx *MsgContext, data NoData) error { return want },
func(ctx *MsgContext, data NoData) error {
t.Fatal("unexpected evaluation after first failure")
return nil
},
)
err := policy(&MsgContext{Logger: slog.CreateLogger()}, NoData{})
if !errors.Is(err, want) {
t.Fatalf("expected first policy error, got %v", err)
}
}
func TestAnyPolicyAllowsLaterSuccessAfterInternalError(t *testing.T) {
policy := AnyPolicy[NoData](
func(ctx *MsgContext, data NoData) error { return AsInternalError(errors.New("temporary")) },
func(ctx *MsgContext, data NoData) error { return nil },
)
if err := policy(&MsgContext{Logger: slog.CreateLogger()}, NoData{}); err != nil {
t.Fatalf("expected later success to allow access, got %v", err)
}
}
func TestAnyPolicyReturnsInternalErrorWhenNonePass(t *testing.T) {
internal := AsInternalError(errors.New("temporary"))
policy := AnyPolicy[NoData](
func(ctx *MsgContext, data NoData) error { return AsUserError(errors.New("denied")) },
func(ctx *MsgContext, data NoData) error { return internal },
)
err := policy(&MsgContext{Logger: slog.CreateLogger()}, NoData{})
if !errors.Is(err, internal) {
t.Fatalf("expected internal error, got %v", err)
}
}
func TestAnyPolicyReturnsFirstDenyWhenNoPolicyPasses(t *testing.T) {
first := AsUserError(errors.New("first deny"))
policy := AnyPolicy[NoData](
func(ctx *MsgContext, data NoData) error { return first },
func(ctx *MsgContext, data NoData) error { return AsUserError(errors.New("second deny")) },
)
err := policy(&MsgContext{Logger: slog.CreateLogger()}, NoData{})
if !errors.Is(err, first) {
t.Fatalf("expected first deny error, got %v", err)
}
}
func TestNotPolicyInvertsUserDenyButPreservesInternalErrors(t *testing.T) {
inverted := NotPolicy[NoData](func(ctx *MsgContext, data NoData) error {
return AsUserError(errors.New("denied"))
})
if err := inverted(&MsgContext{Logger: slog.CreateLogger()}, NoData{}); err != nil {
t.Fatalf("expected inverted deny to succeed, got %v", err)
}
internal := AsInternalError(errors.New("temporary"))
preserve := NotPolicy[NoData](func(ctx *MsgContext, data NoData) error {
return internal
})
err := preserve(&MsgContext{Logger: slog.CreateLogger()}, NoData{})
if !errors.Is(err, internal) {
t.Fatalf("expected internal error to be preserved, got %v", err)
}
}
func TestRequirePolicyEmitsObserverEvents(t *testing.T) {
t.Run("allow", func(t *testing.T) {
observer := &recordingObserver{}
ctx := &MsgContext{
Logger: slog.CreateLogger(),
ctx: context.Background(),
observer: observer,
FromID: 10,
ChatID: 20,
}
mw := RequirePolicy[NoData]("allow", func(ctx *MsgContext, data NoData) error {
return nil
})
if !mw.Execute(ctx, NoData{}) {
t.Fatal("expected allowed policy middleware to continue execution")
}
if len(observer.policies) != 1 {
t.Fatalf("expected one policy event, got %d", len(observer.policies))
}
if got := observer.policies[0]; got.Name != "allow" || !got.Passed || got.Err != nil || got.Internal {
t.Fatalf("unexpected policy event: %#v", got)
}
})
t.Run("deny", func(t *testing.T) {
observer := &recordingObserver{}
ctx := &MsgContext{
Logger: slog.CreateLogger(),
ctx: context.Background(),
observer: observer,
errorTemplate: "%s",
}
mw := RequirePolicy[NoData]("deny", func(ctx *MsgContext, data NoData) error {
return AsInternalError(errors.New("blocked"))
})
if mw.Execute(ctx, NoData{}) {
t.Fatal("expected denied policy middleware to stop execution")
}
if len(observer.policies) != 1 {
t.Fatalf("expected one policy event, got %d", len(observer.policies))
}
if got := observer.policies[0]; got.Name != "deny" || got.Passed || got.Err == nil || !got.Internal {
t.Fatalf("unexpected policy event: %#v", got)
}
})
}
+48 -9
View File
@@ -7,7 +7,7 @@ import (
// RunnerFn is the function type for a runner. It receives a pointer to // RunnerFn is the function type for a runner. It receives a pointer to
// the Bot and returns an error if execution fails. // the Bot and returns an error if execution fails.
type RunnerFn[T DbContext] func(*Bot[T]) error type RunnerFn[T AppData] func(*Bot[T]) error
// Runner represents a configurable background or one-time task to be // Runner represents a configurable background or one-time task to be
// executed by a Bot. // executed by a Bot.
@@ -20,7 +20,7 @@ type RunnerFn[T DbContext] func(*Bot[T]) error
// - onetime=true, async=true: Run once in a goroutine (non-blocking). // - 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=true: Run repeatedly in a goroutine with timeout.
// - onetime=false, async=false: Invalid configuration — ignored with warning. // - onetime=false, async=false: Invalid configuration — ignored with warning.
type Runner[T DbContext] struct { type Runner[T AppData] struct {
name string // Human-readable name for logging name string // Human-readable name for logging
onetime bool // If true, runs once; if false, runs periodically onetime bool // If true, runs once; if false, runs periodically
async bool // If true, runs in a goroutine; else, runs synchronously async bool // If true, runs in a goroutine; else, runs synchronously
@@ -33,8 +33,8 @@ type Runner[T DbContext] struct {
// //
// Builder methods (Onetime, Async, Timeout) can be chained to customize behavior. // Builder methods (Onetime, Async, Timeout) can be chained to customize behavior.
// DO NOT call builder methods concurrently or after Execute(). // DO NOT call builder methods concurrently or after Execute().
func NewRunner[T DbContext](name string, fn RunnerFn[T]) *Runner[T] { func NewRunner[T AppData](name string, fn RunnerFn[T]) Runner[T] {
return &Runner[T]{ return Runner[T]{
name: name, name: name,
fn: fn, fn: fn,
async: true, // Default: run asynchronously async: true, // Default: run asynchronously
@@ -45,7 +45,7 @@ func NewRunner[T DbContext](name string, fn RunnerFn[T]) *Runner[T] {
// Onetime sets whether the runner executes once or repeatedly. // Onetime sets whether the runner executes once or repeatedly.
// If true, the runner runs only once. // If true, the runner runs only once.
// If false, the runner runs in a loop with the configured timeout. // If false, the runner runs in a loop with the configured timeout.
func (r *Runner[T]) Onetime(onetime bool) *Runner[T] { func (r Runner[T]) Onetime(onetime bool) Runner[T] {
r.onetime = onetime r.onetime = onetime
return r return r
} }
@@ -55,7 +55,7 @@ func (r *Runner[T]) Onetime(onetime bool) *Runner[T] {
// If false, the runner blocks the caller during execution. // 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: If onetime=false and async=false, the runner will be skipped with a warning.
func (r *Runner[T]) Async(async bool) *Runner[T] { func (r Runner[T]) Async(async bool) Runner[T] {
r.async = async r.async = async
return r return r
} }
@@ -69,7 +69,7 @@ func (r *Runner[T]) Async(async bool) *Runner[T] {
// //
// A zero value (time.Duration(0)) is allowed but may trigger a warning // A zero value (time.Duration(0)) is allowed but may trigger a warning
// if used with a background (non-onetime) async runner. // if used with a background (non-onetime) async runner.
func (r *Runner[T]) Timeout(timeout time.Duration) *Runner[T] { func (r Runner[T]) Timeout(timeout time.Duration) Runner[T] {
r.timeout = timeout r.timeout = timeout
return r return r
} }
@@ -88,7 +88,8 @@ func (r *Runner[T]) Timeout(timeout time.Duration) *Runner[T] {
// //
// Background runners listen for ctx.Done() and gracefully shut down when the context is canceled. // Background runners listen for ctx.Done() and gracefully shut down when the context is canceled.
// //
// This method is typically called once during bot startup in RunWithContext. // This method is typically called once during bot startup from RunWithContext or
// RunWebHookWithContext.
func (bot *Bot[T]) ExecRunners(ctx context.Context) { func (bot *Bot[T]) ExecRunners(ctx context.Context) {
bot.logger.Infoln("Executing runners...") bot.logger.Infoln("Executing runners...")
for _, runner := range bot.runners { for _, runner := range bot.runners {
@@ -107,8 +108,21 @@ func (bot *Bot[T]) ExecRunners(ctx context.Context) {
bot.runnerOnceWG.Add(1) bot.runnerOnceWG.Add(1)
go func(r Runner[T]) { go func(r Runner[T]) {
defer bot.runnerOnceWG.Done() defer bot.runnerOnceWG.Done()
startedAt := time.Now()
err := r.fn(bot) err := r.fn(bot)
bot.safeEmitEvent(ctx, RunnerFinishedEvent{
Name: r.name,
Duration: time.Since(startedAt),
Err: err,
})
if err != nil { 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) }(runner)
@@ -116,10 +130,22 @@ func (bot *Bot[T]) ExecRunners(ctx context.Context) {
// One-time sync: block until done // One-time sync: block until done
t := time.Now() t := time.Now()
err := runner.fn(bot) err := runner.fn(bot)
elapsed := time.Since(t)
bot.safeEmitEvent(ctx, RunnerFinishedEvent{
Name: runner.name,
Duration: elapsed,
Err: err,
})
if err != nil { if err != nil {
bot.safeEmitEvent(ctx, ErrorEvent{
Plugin: "bot",
HandlerKind: HandlerRunnerKind,
HandlerName: runner.name,
Err: err,
UserFacing: false,
})
bot.logger.Warnf("Runner %s failed: %s\n", runner.name, err) bot.logger.Warnf("Runner %s failed: %s\n", runner.name, err)
} }
elapsed := time.Since(t)
if elapsed > time.Second*2 { if elapsed > time.Second*2 {
bot.logger.Warnf("Runner %s too slow. Elapsed time %v >= 2s\n", runner.name, elapsed) bot.logger.Warnf("Runner %s too slow. Elapsed time %v >= 2s\n", runner.name, elapsed)
} }
@@ -135,8 +161,21 @@ func (bot *Bot[T]) ExecRunners(ctx context.Context) {
case <-ctx.Done(): case <-ctx.Done():
return return
case <-ticker.C: case <-ticker.C:
startedAt := time.Now()
err := r.fn(bot) err := r.fn(bot)
bot.safeEmitEvent(ctx, RunnerFinishedEvent{
Name: r.name,
Duration: time.Since(startedAt),
Err: err,
})
if err != nil { 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)
} }
} }
+97
View File
@@ -0,0 +1,97 @@
package laniakea
import (
"context"
"errors"
"sync/atomic"
"testing"
"time"
"git.scuroneko.dev/scuroneko/slog"
)
type runnerObserver struct {
recordingObserver
}
func TestExecRunnersRunsOnetimeSyncRunner(t *testing.T) {
var calls atomic.Int32
bot := &Bot[NoData]{
logger: slog.CreateLogger(),
runners: []Runner[NoData]{
NewRunner("sync-once", func(*Bot[NoData]) error {
calls.Add(1)
return nil
}).Onetime(true).Async(false),
},
}
bot.ExecRunners(context.Background())
if got := calls.Load(); got != 1 {
t.Fatalf("unexpected sync runner call count: %d", got)
}
}
func TestExecRunnersStopsBackgroundRunnerOnCancel(t *testing.T) {
var calls atomic.Int32
triggered := make(chan struct{}, 1)
ctx, cancel := context.WithCancel(context.Background())
bot := &Bot[NoData]{
logger: slog.CreateLogger(),
runners: []Runner[NoData]{
NewRunner("background", func(*Bot[NoData]) error {
if calls.Add(1) == 1 {
triggered <- struct{}{}
}
return nil
}).Timeout(5 * time.Millisecond),
},
}
bot.ExecRunners(ctx)
select {
case <-triggered:
case <-time.After(time.Second):
t.Fatal("background runner did not execute")
}
cancel()
bot.runnerBgWG.Wait()
if calls.Load() == 0 {
t.Fatal("expected background runner to be called at least once")
}
}
func TestExecRunnersEmitObserverEvents(t *testing.T) {
observer := &runnerObserver{}
wantErr := errors.New("runner failed")
bot := &Bot[NoData]{
logger: slog.CreateLogger(),
observer: observer,
runners: []Runner[NoData]{
NewRunner("sync-once", func(*Bot[NoData]) error {
return wantErr
}).Onetime(true).Async(false),
},
}
bot.ExecRunners(context.Background())
if len(observer.runners) != 1 {
t.Fatalf("expected one runner-finished event, got %d", len(observer.runners))
}
if got := observer.runners[0]; got.Name != "sync-once" || !errors.Is(got.Err, wantErr) {
t.Fatalf("unexpected runner-finished event: %#v", got)
}
if len(observer.errors) != 1 {
t.Fatalf("expected one error event, got %d", len(observer.errors))
}
if got := observer.errors[0]; got.HandlerKind != HandlerRunnerKind || got.HandlerName != "sync-once" || !errors.Is(got.Err, wantErr) {
t.Fatalf("unexpected runner error event: %#v", got)
}
}
+238
View File
@@ -0,0 +1,238 @@
package laniakea
import (
"encoding/json"
"sync"
)
// SceneHandler handles a scene step, scene command, or fallback message.
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
steps map[string]SceneHandler[T]
commands map[string]SceneHandler[T]
message SceneHandler[T]
}
// 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: "",
steps: make(map[string]SceneHandler[T]),
commands: make(map[string]SceneHandler[T]),
message: nil,
}
}
// SetScope changes how scene sessions are keyed and shared.
func (s *Scene[T]) SetScope(scope SceneScope) *Scene[T] {
s.Scope = scope
return s
}
// SetEntry sets the initial step entered by MsgContext.EnterScene.
func (s *Scene[T]) SetEntry(step string) *Scene[T] {
s.Entry = step
return s
}
func (s *Scene[T]) setPluginName(name string) *Scene[T] {
s.PluginName = name
return s
}
// OnStep registers a handler for a named scene step.
func (s *Scene[T]) OnStep(step string, handler SceneHandler[T]) *Scene[T] {
s.steps[step] = handler
return s
}
// OnCommand registers a command handler active while the scene is running.
func (s *Scene[T]) OnCommand(cmd string, handler SceneHandler[T]) *Scene[T] {
s.commands[cmd] = handler
return s
}
// OnMessage registers a fallback handler used when no scene command or step matches.
func (s *Scene[T]) OnMessage(handler SceneHandler[T]) *Scene[T] {
s.message = handler
return s
}
func (s *Scene[T]) executeCommand(cmd string, ctx *SceneContext, db T) (SceneResult, bool, error) {
handler, ok := s.commands[cmd]
if !ok {
return SceneResult{}, false, nil
}
result, err := handler(ctx, db)
return result, true, err
}
func (s *Scene[T]) executeStep(step string, ctx *SceneContext, db T) (SceneResult, bool, error) {
handler, ok := s.steps[step]
if !ok {
return SceneResult{}, false, nil
}
result, err := handler(ctx, db)
return result, true, err
}
func (s *Scene[T]) executeMessage(ctx *SceneContext, db T) (SceneResult, bool, error) {
if s.message == nil {
return SceneResult{}, false, nil
}
result, err := s.message(ctx, db)
return result, true, err
}
// 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
}
// SetData stores arbitrary opaque session data.
func (s *SceneSession) SetData(data []byte) {
s.Data = data
}
// GetData returns the raw session data payload.
func (s *SceneSession) GetData() []byte {
return s.Data
}
// HasData reports whether the session has a non-empty data payload.
func (s *SceneSession) HasData() bool {
return len(s.Data) > 0
}
// ClearData removes any stored session data.
func (s *SceneSession) ClearData() {
s.Data = nil
}
// BindData unmarshals the stored JSON payload into v.
func (s *SceneSession) BindData(v any) error {
if len(s.Data) == 0 {
return nil
}
return json.Unmarshal(s.Data, v)
}
// SaveData marshals v as JSON and stores it in the session.
func (s *SceneSession) SaveData(v any) error {
data, err := json.Marshal(v)
if err != nil {
return err
}
s.Data = data
return nil
}
// SessionStore persists scene sessions by key.
type SessionStore interface {
Get(key string) (SceneSession, error)
Set(key string, session SceneSession) error
Delete(key string) error
}
// MemorySessionStore stores scene sessions in memory.
type MemorySessionStore struct {
store map[string]SceneSession
mu sync.RWMutex
}
// NewMemorySessionStore creates an empty in-memory session store.
func NewMemorySessionStore() *MemorySessionStore {
return &MemorySessionStore{
store: make(map[string]SceneSession),
}
}
// Get returns the session stored under key, or the zero session when absent.
func (s *MemorySessionStore) Get(key string) (SceneSession, error) {
s.mu.RLock()
defer s.mu.RUnlock()
if session, ok := s.store[key]; ok {
return session, nil
}
return SceneSession{}, nil
}
// Set stores session under key.
func (s *MemorySessionStore) Set(key string, session SceneSession) error {
s.mu.Lock()
s.store[key] = session
s.mu.Unlock()
return nil
}
// Delete removes the session stored under key.
func (s *MemorySessionStore) Delete(key string) error {
s.mu.Lock()
delete(s.store, key)
s.mu.Unlock()
return nil
}
// SceneResult describes how scene execution should proceed after a handler returns.
type SceneResult struct {
Action SceneAction
Next string
}
// SceneAction controls how the bot updates scene state after a handler returns.
type SceneAction int
const (
// SceneActionStay keeps the current scene and step active.
SceneActionStay SceneAction = iota
// SceneActionNext moves the session to another named step.
SceneActionNext
// SceneActionExit removes the current scene session.
SceneActionExit
// SceneActionPass lets normal bot routing continue after the scene handler.
SceneActionPass
)
// SceneScope defines how scene sessions are keyed.
type SceneScope int
const (
// SceneScopeUser shares a scene across all chats for one user.
SceneScopeUser SceneScope = iota
// SceneScopeChat shares a scene across all users in one chat.
SceneScopeChat
// SceneScopeUserChat isolates a scene per user-chat pair.
SceneScopeUserChat
)
type sceneRuntime interface {
findScene(name string) (*sceneMeta, bool)
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)
}
type sceneMeta struct {
Name string
Scope SceneScope
Entry string
Steps map[string]struct{}
}
+41
View File
@@ -0,0 +1,41 @@
package laniakea
// SceneContext wraps MsgContext with scene session state for scene handlers.
type SceneContext struct {
*MsgContext
sess SceneSession
key string
}
// Next advances the current scene to step.
func (ctx *SceneContext) Next(step string) SceneResult {
return SceneResult{
Action: SceneActionNext,
Next: step,
}
}
// Stay keeps the current scene step active.
func (ctx *SceneContext) Stay() SceneResult {
return SceneResult{Action: SceneActionStay}
}
// Exit leaves the current scene.
func (ctx *SceneContext) Exit() SceneResult {
return SceneResult{Action: SceneActionExit}
}
// Pass stops scene handling and lets normal routing continue.
func (ctx *SceneContext) Pass() SceneResult {
return SceneResult{Action: SceneActionPass}
}
// BindData unmarshals the current scene session payload into v.
func (ctx *SceneContext) BindData(v any) error {
return ctx.sess.BindData(v)
}
// SaveData marshals v and stores it in the current scene session payload.
func (ctx *SceneContext) SaveData(v any) error {
return ctx.sess.SaveData(v)
}
+255
View File
@@ -0,0 +1,255 @@
package laniakea
import (
"errors"
"fmt"
"strings"
"time"
)
func (bot *Bot[T]) tryHandleScene(ctx *MsgContext) (bool, error) {
key, session, err := bot.findSceneSession(ctx)
if err != nil {
if errors.Is(err, ErrCantFindSession) || errors.Is(err, ErrMessageNil) {
return false, nil
}
return false, err
}
if session.Scene == "" {
return false, nil
}
for _, plugin := range bot.plugins {
scene, ok := plugin.scenes[session.Scene]
if !ok {
continue
}
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,
}
return bot.executeScene(sceneCtx, scene)
}
return false, ErrSceneNotFound
}
func (bot *Bot[T]) executeScene(ctx *SceneContext, scene *Scene[T]) (bool, error) {
if ctx.MsgContext == nil || ctx.sess.Scene == "" {
return false, nil
}
var text string
if ctx.Msg != nil {
text = ctx.Msg.Text
if text == "" {
text = ctx.Msg.Caption
}
}
text = strings.TrimSpace(text)
prefix, cmd, args := bot.parseCommand(text)
if cmd != "" {
ctx.Prefix = prefix
ctx.Text = args
ctx.Args = strings.Fields(args)
if _, ok := scene.commands[cmd]; ok {
startTime := time.Now()
bot.emitSceneStarted(ctx, scene, HandlerSceneCommandKind, cmd)
res, _, err := scene.executeCommand(cmd, ctx, bot.appData)
if err != nil {
bot.emitSceneFinished(ctx, scene, HandlerSceneCommandKind, cmd, startTime, err)
bot.emitSceneError(ctx, scene, HandlerSceneCommandKind, cmd, err)
return false, err
}
from := ctx.sess.Step
ok, err := bot.applySceneResult(scene, ctx, res)
bot.emitSceneFinished(ctx, scene, HandlerSceneCommandKind, cmd, startTime, err)
if err != nil {
bot.emitSceneError(ctx, scene, HandlerSceneCommandKind, cmd, err)
}
if ok {
bot.emitSceneTransition(ctx, scene, from, res)
}
return ok, err
}
}
ctx.Text = text
ctx.Args = nil
ctx.Prefix = ""
if ctx.sess.Step != "" {
step := ctx.sess.Step
if _, ok := scene.steps[step]; ok {
startTime := time.Now()
bot.emitSceneStarted(ctx, scene, HandlerSceneStepKind, step)
res, _, err := scene.executeStep(step, ctx, bot.appData)
if err != nil {
bot.emitSceneFinished(ctx, scene, HandlerSceneStepKind, step, startTime, err)
bot.emitSceneError(ctx, scene, HandlerSceneStepKind, step, err)
return false, err
}
from := step
ok, err := bot.applySceneResult(scene, ctx, res)
bot.emitSceneFinished(ctx, scene, HandlerSceneStepKind, step, startTime, err)
if err != nil {
bot.emitSceneError(ctx, scene, HandlerSceneStepKind, from, err)
}
if ok {
bot.emitSceneTransition(ctx, scene, from, res)
}
return ok, err
}
}
if scene.message != nil {
startTime := time.Now()
bot.emitSceneStarted(ctx, scene, HandlerSceneMessageKind, "message_fallback")
res, _, err := scene.executeMessage(ctx, bot.appData)
if err != nil {
bot.emitSceneFinished(ctx, scene, HandlerSceneMessageKind, "message_fallback", startTime, err)
bot.emitSceneError(ctx, scene, HandlerSceneMessageKind, "message_fallback", err)
return false, err
}
from := ctx.sess.Step
ok, err := bot.applySceneResult(scene, ctx, res)
bot.emitSceneFinished(ctx, scene, HandlerSceneMessageKind, "message_fallback", startTime, err)
if err != nil {
bot.emitSceneError(ctx, scene, HandlerSceneMessageKind, "message_fallback", err)
}
if ok {
bot.emitSceneTransition(ctx, scene, from, res)
}
return ok, err
}
return false, nil
}
func (bot *Bot[T]) emitSceneStarted(ctx *SceneContext, scene *Scene[T], kind HandlerEventKind, name string) {
bot.safeEmitEvent(ctx.Context(), HandlerStartedEvent{
UpdateID: ctx.Update.UpdateID,
UpdateType: ctx.Update.Type,
Plugin: scene.PluginName,
HandlerKind: kind,
HandlerName: name,
FromID: ctx.FromID,
ChatID: ctx.ChatID,
})
}
func (bot *Bot[T]) emitSceneFinished(ctx *SceneContext, scene *Scene[T], kind HandlerEventKind, name string, startedAt time.Time, err error) {
bot.safeEmitEvent(ctx.Context(), HandlerFinishedEvent{
UpdateID: ctx.Update.UpdateID,
UpdateType: ctx.Update.Type,
Plugin: scene.PluginName,
HandlerKind: kind,
HandlerName: name,
FromID: ctx.FromID,
ChatID: ctx.ChatID,
Duration: time.Since(startedAt),
Err: err,
UserFacing: IsUserError(err),
})
}
func (bot *Bot[T]) emitSceneError(ctx *SceneContext, scene *Scene[T], kind HandlerEventKind, name string, err error) {
bot.safeEmitEvent(ctx.Context(), ErrorEvent{
UpdateID: ctx.Update.UpdateID,
UpdateType: ctx.Update.Type,
Plugin: scene.PluginName,
HandlerKind: kind,
HandlerName: name,
FromID: ctx.FromID,
ChatID: ctx.ChatID,
Err: err,
UserFacing: IsUserError(err),
})
}
func (bot *Bot[T]) emitSceneTransition(ctx *SceneContext, scene *Scene[T], from string, result SceneResult) {
if result.Action == SceneActionPass {
return
}
to := from
switch result.Action {
case SceneActionNext:
to = result.Next
case SceneActionExit:
to = ""
}
bot.safeEmitEvent(ctx.Context(), SceneTransitionEvent{
Plugin: scene.PluginName,
Scene: scene.Name,
From: from,
To: to,
Action: result.Action,
FromID: ctx.FromID,
ChatID: ctx.ChatID,
})
}
func (bot *Bot[T]) applySceneResult(scene *Scene[T], ctx *SceneContext, result SceneResult) (bool, error) {
switch result.Action {
case SceneActionStay:
if err := bot.sessionStore.Set(ctx.key, ctx.sess); err != nil {
return false, err
}
return true, nil
case SceneActionNext:
if result.Next == "" {
return false, ErrSceneStepNotFound
}
if _, ok := scene.steps[result.Next]; !ok {
return false, ErrSceneStepNotFound
}
ctx.sess.Step = result.Next
if err := bot.sessionStore.Set(ctx.key, ctx.sess); err != nil {
return false, err
}
return true, nil
case SceneActionExit:
if err := bot.sessionStore.Delete(ctx.key); err != nil {
return false, err
}
return true, nil
case SceneActionPass:
return false, nil
default:
return false, nil
}
}
func buildSceneKey(scope SceneScope, ctx *MsgContext) (string, bool) {
if ctx == nil {
return "", false
}
switch scope {
case SceneScopeUserChat:
if ctx.Msg == nil || ctx.Msg.Chat == nil || ctx.FromID == 0 {
return "", false
}
return fmt.Sprintf("user_id:%d:chat_id:%d", ctx.FromID, ctx.Msg.Chat.ID), true
case SceneScopeChat:
if ctx.Msg == nil || ctx.Msg.Chat == nil {
return "", false
}
return fmt.Sprintf("chat_id:%d", ctx.Msg.Chat.ID), true
case SceneScopeUser:
if ctx.FromID == 0 {
return "", false
}
return fmt.Sprintf("user_id:%d", ctx.FromID), true
default:
return "", false
}
}
+632
View File
@@ -0,0 +1,632 @@
package laniakea
import (
"context"
"errors"
"testing"
"git.scuroneko.dev/scuroneko/laniakea/tgapi"
"git.scuroneko.dev/scuroneko/slog"
)
type failingSessionStore struct {
getErr error
setErr error
deleteErr error
}
func (s failingSessionStore) Get(key string) (SceneSession, error) {
return SceneSession{}, s.getErr
}
func (s failingSessionStore) Set(key string, session SceneSession) error {
return s.setErr
}
func (s failingSessionStore) Delete(key string) error {
return s.deleteErr
}
func TestPluginAddSceneRegistersScene(t *testing.T) {
plugin := NewPlugin[NoData]("wizard")
scene := NewScene[NoData]("signup")
plugin.AddScene(scene)
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")
}
}
func TestBotAddPluginsPreservesScenesAndHandlesThem(t *testing.T) {
called := false
plugin := NewPlugin[NoData]("wizard")
plugin.NewScene("signup").
SetEntry("start").
OnStep("start", func(ctx *SceneContext, db NoData) (SceneResult, error) {
called = true
if ctx.Text != "hello there" {
t.Fatalf("unexpected scene text: got %q want %q", ctx.Text, "hello there")
}
return ctx.Exit(), nil
})
bot := &Bot[NoData]{
logger: slog.CreateLogger(),
prefixes: []string{"/"},
sessionStore: NewMemorySessionStore(),
sceneScopePriority: []SceneScope{SceneScopeUserChat, SceneScopeChat, SceneScopeUser},
}
bot.AddPlugins(plugin)
sceneMeta, ok := bot.findScene("signup")
if !ok {
t.Fatal("expected scene metadata to be available after plugin registration")
}
if sceneMeta.Entry != "start" {
t.Fatalf("unexpected scene entry: got %q want %q", sceneMeta.Entry, "start")
}
enterCtx := &MsgContext{
Msg: &tgapi.Message{Chat: &tgapi.Chat{ID: 100, Type: tgapi.ChatTypePrivate}},
FromID: 42,
sceneRuntime: bot,
}
if err := enterCtx.EnterScene("signup"); err != nil {
t.Fatalf("EnterScene returned error: %v", err)
}
bot.handle(context.Background(), &tgapi.Update{
UpdateID: 1,
Type: tgapi.UpdateTypeMessage,
Message: &tgapi.Message{
MessageID: 7,
Text: "hello there",
Chat: &tgapi.Chat{ID: 100, Type: tgapi.ChatTypePrivate},
From: &tgapi.User{ID: 42},
},
})
if !called {
t.Fatal("expected scene step handler to be called")
}
lookupCtx := &MsgContext{
Msg: &tgapi.Message{Chat: &tgapi.Chat{ID: 100, Type: tgapi.ChatTypePrivate}},
FromID: 42,
}
if _, session, err := bot.findSceneSession(lookupCtx); err == nil && session.Scene != "" {
t.Fatalf("expected scene session to be removed after exit, got %#v", session)
}
}
func TestBuildSceneKeyRejectsMissingContextFields(t *testing.T) {
tests := []struct {
name string
scope SceneScope
ctx *MsgContext
}{
{
name: "nil context",
scope: SceneScopeUserChat,
ctx: nil,
},
{
name: "missing message for chat scope",
scope: SceneScopeChat,
ctx: &MsgContext{},
},
{
name: "missing from id for user scope",
scope: SceneScopeUser,
ctx: &MsgContext{},
},
{
name: "missing from id for user chat scope",
scope: SceneScopeUserChat,
ctx: &MsgContext{
Msg: &tgapi.Message{Chat: &tgapi.Chat{ID: 100, Type: tgapi.ChatTypePrivate}},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if key, ok := buildSceneKey(tt.scope, tt.ctx); ok || key != "" {
t.Fatalf("expected invalid scene key, got key=%q ok=%v", key, ok)
}
})
}
}
func TestEnterSceneRejectsMissingEntryConfiguration(t *testing.T) {
t.Run("empty entry", func(t *testing.T) {
plugin := NewPlugin[NoData]("wizard")
plugin.NewScene("signup")
bot := &Bot[NoData]{
logger: slog.CreateLogger(),
sessionStore: NewMemorySessionStore(),
sceneScopePriority: []SceneScope{SceneScopeUserChat, SceneScopeChat, SceneScopeUser},
}
bot.AddPlugins(plugin)
ctx := &MsgContext{
Msg: &tgapi.Message{Chat: &tgapi.Chat{ID: 100, Type: tgapi.ChatTypePrivate}},
FromID: 42,
sceneRuntime: bot,
}
err := ctx.EnterScene("signup")
if !errors.Is(err, ErrSceneEntryNotSet) {
t.Fatalf("expected ErrSceneEntryNotSet, got %v", err)
}
})
t.Run("missing entry step", func(t *testing.T) {
plugin := NewPlugin[NoData]("wizard")
plugin.NewScene("signup").SetEntry("start")
bot := &Bot[NoData]{
logger: slog.CreateLogger(),
sessionStore: NewMemorySessionStore(),
sceneScopePriority: []SceneScope{SceneScopeUserChat, SceneScopeChat, SceneScopeUser},
}
bot.AddPlugins(plugin)
ctx := &MsgContext{
Msg: &tgapi.Message{Chat: &tgapi.Chat{ID: 100, Type: tgapi.ChatTypePrivate}},
FromID: 42,
sceneRuntime: bot,
}
err := ctx.EnterScene("signup")
if !errors.Is(err, ErrSceneStepNotFound) {
t.Fatalf("expected ErrSceneStepNotFound, got %v", err)
}
})
}
func TestSceneContextMethodsRequireRuntime(t *testing.T) {
ctx := &MsgContext{}
if err := ctx.EnterScene("signup"); !errors.Is(err, ErrSceneRuntimeNil) {
t.Fatalf("expected ErrSceneRuntimeNil from EnterScene, got %v", err)
}
if err := ctx.EnterSceneStep("signup", "start"); !errors.Is(err, ErrSceneRuntimeNil) {
t.Fatalf("expected ErrSceneRuntimeNil from EnterSceneStep, got %v", err)
}
if err := ctx.ExitScene(); !errors.Is(err, ErrSceneRuntimeNil) {
t.Fatalf("expected ErrSceneRuntimeNil from ExitScene, got %v", err)
}
}
func TestSceneCommandHandlerRunsBeforeStep(t *testing.T) {
sceneCommandCalled := false
stepCalled := false
plugin := NewPlugin[NoData]("wizard")
plugin.NewScene("signup").
SetEntry("start").
OnStep("start", func(ctx *SceneContext, db NoData) (SceneResult, error) {
stepCalled = true
return ctx.Stay(), nil
}).
OnCommand("cancel", func(ctx *SceneContext, db NoData) (SceneResult, error) {
sceneCommandCalled = true
if ctx.Prefix != "/" {
t.Fatalf("unexpected prefix: got %q want /", ctx.Prefix)
}
if ctx.Text != "right now" {
t.Fatalf("unexpected scene command text: got %q want %q", ctx.Text, "right now")
}
if len(ctx.Args) != 2 || ctx.Args[0] != "right" || ctx.Args[1] != "now" {
t.Fatalf("unexpected scene command args: %#v", ctx.Args)
}
return ctx.Exit(), nil
})
bot := &Bot[NoData]{
logger: slog.CreateLogger(),
prefixes: []string{"/"},
sessionStore: NewMemorySessionStore(),
sceneScopePriority: []SceneScope{SceneScopeUserChat, SceneScopeChat, SceneScopeUser},
}
bot.AddPlugins(plugin)
enterCtx := &MsgContext{
Msg: &tgapi.Message{Chat: &tgapi.Chat{ID: 100, Type: tgapi.ChatTypePrivate}},
FromID: 42,
sceneRuntime: bot,
}
if err := enterCtx.EnterScene("signup"); err != nil {
t.Fatalf("EnterScene returned error: %v", err)
}
bot.handle(context.Background(), &tgapi.Update{
UpdateID: 2,
Type: tgapi.UpdateTypeMessage,
Message: &tgapi.Message{
MessageID: 8,
Text: "/cancel right now",
Chat: &tgapi.Chat{ID: 100, Type: tgapi.ChatTypePrivate},
From: &tgapi.User{ID: 42},
},
})
if !sceneCommandCalled {
t.Fatal("expected scene command handler to be called")
}
if stepCalled {
t.Fatal("expected scene command to short-circuit the scene step")
}
}
func TestSceneCommandObserverEmitsLifecycleEvents(t *testing.T) {
observer := &recordingObserver{}
plugin := NewPlugin[NoData]("wizard")
plugin.NewScene("signup").
SetEntry("start").
OnStep("start", func(ctx *SceneContext, db NoData) (SceneResult, error) {
return ctx.Stay(), nil
}).
OnCommand("cancel", func(ctx *SceneContext, db NoData) (SceneResult, error) {
return ctx.Exit(), nil
})
bot := &Bot[NoData]{
logger: slog.CreateLogger(),
prefixes: []string{"/"},
sessionStore: NewMemorySessionStore(),
sceneScopePriority: []SceneScope{SceneScopeUserChat, SceneScopeChat, SceneScopeUser},
observer: observer,
}
bot.AddPlugins(plugin)
enterCtx := &MsgContext{
Msg: &tgapi.Message{Chat: &tgapi.Chat{ID: 100, Type: tgapi.ChatTypePrivate}},
FromID: 42,
sceneRuntime: bot,
}
if err := enterCtx.EnterScene("signup"); err != nil {
t.Fatalf("EnterScene returned error: %v", err)
}
bot.handle(context.Background(), &tgapi.Update{
UpdateID: 22,
Type: tgapi.UpdateTypeMessage,
Message: &tgapi.Message{
MessageID: 9,
Text: "/cancel",
Chat: &tgapi.Chat{ID: 100, Type: tgapi.ChatTypePrivate},
From: &tgapi.User{ID: 42},
},
})
if len(observer.started) != 1 {
t.Fatalf("expected one scene started event, got %d", len(observer.started))
}
if got := observer.started[0]; got.HandlerKind != HandlerSceneCommandKind || got.HandlerName != "cancel" || got.Plugin != "wizard" {
t.Fatalf("unexpected scene started event: %#v", got)
}
if len(observer.finished) != 1 {
t.Fatalf("expected one scene finished event, got %d", len(observer.finished))
}
if got := observer.finished[0]; got.HandlerKind != HandlerSceneCommandKind || got.HandlerName != "cancel" || got.Plugin != "wizard" || got.Err != nil {
t.Fatalf("unexpected scene finished event: %#v", got)
}
}
func TestSceneStepObserverEmitsLifecycleEvents(t *testing.T) {
observer := &recordingObserver{}
plugin := NewPlugin[NoData]("wizard")
plugin.NewScene("signup").
SetEntry("start").
OnStep("start", func(ctx *SceneContext, db NoData) (SceneResult, error) {
return ctx.Stay(), nil
})
bot := &Bot[NoData]{
logger: slog.CreateLogger(),
prefixes: []string{"/"},
sessionStore: NewMemorySessionStore(),
sceneScopePriority: []SceneScope{SceneScopeUserChat, SceneScopeChat, SceneScopeUser},
observer: observer,
}
bot.AddPlugins(plugin)
enterCtx := &MsgContext{
Msg: &tgapi.Message{Chat: &tgapi.Chat{ID: 100, Type: tgapi.ChatTypePrivate}},
FromID: 42,
sceneRuntime: bot,
}
if err := enterCtx.EnterScene("signup"); err != nil {
t.Fatalf("EnterScene returned error: %v", err)
}
bot.handle(context.Background(), &tgapi.Update{
UpdateID: 23,
Type: tgapi.UpdateTypeMessage,
Message: &tgapi.Message{
MessageID: 10,
Text: "hello there",
Chat: &tgapi.Chat{ID: 100, Type: tgapi.ChatTypePrivate},
From: &tgapi.User{ID: 42},
},
})
if len(observer.started) != 1 {
t.Fatalf("expected one scene started event, got %d", len(observer.started))
}
if got := observer.started[0]; got.HandlerKind != HandlerSceneStepKind || got.HandlerName != "start" || got.Plugin != "wizard" {
t.Fatalf("unexpected scene step started event: %#v", got)
}
if len(observer.finished) != 1 {
t.Fatalf("expected one scene finished event, got %d", len(observer.finished))
}
if got := observer.finished[0]; got.HandlerKind != HandlerSceneStepKind || got.HandlerName != "start" || got.Plugin != "wizard" || got.Err != nil {
t.Fatalf("unexpected scene step finished event: %#v", got)
}
}
func TestSceneMessageObserverEmitsLifecycleEvents(t *testing.T) {
observer := &recordingObserver{}
plugin := NewPlugin[NoData]("wizard")
scene := plugin.NewScene("signup").
SetEntry("start").
OnStep("start", func(ctx *SceneContext, db NoData) (SceneResult, error) {
return ctx.Stay(), nil
}).
OnMessage(func(ctx *SceneContext, db NoData) (SceneResult, error) {
return ctx.Exit(), nil
})
bot := &Bot[NoData]{
logger: slog.CreateLogger(),
prefixes: []string{"/"},
sessionStore: NewMemorySessionStore(),
sceneScopePriority: []SceneScope{SceneScopeUserChat, SceneScopeChat, SceneScopeUser},
observer: observer,
}
bot.AddPlugins(plugin)
key, ok := buildSceneKey(SceneScopeUserChat, &MsgContext{
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 {
t.Fatalf("failed to seed scene session: %v", err)
}
bot.handle(context.Background(), &tgapi.Update{
UpdateID: 24,
Type: tgapi.UpdateTypeMessage,
Message: &tgapi.Message{
MessageID: 11,
Text: "hello there",
Chat: &tgapi.Chat{ID: 100, Type: tgapi.ChatTypePrivate},
From: &tgapi.User{ID: 42},
},
})
if len(observer.started) != 1 {
t.Fatalf("expected one scene started event, got %d", len(observer.started))
}
if got := observer.started[0]; got.HandlerKind != HandlerSceneMessageKind || got.HandlerName != "message_fallback" || got.Plugin != "wizard" {
t.Fatalf("unexpected scene message started event: %#v", got)
}
if len(observer.finished) != 1 {
t.Fatalf("expected one scene finished event, got %d", len(observer.finished))
}
if got := observer.finished[0]; got.HandlerKind != HandlerSceneMessageKind || got.HandlerName != "message_fallback" || got.Plugin != "wizard" || got.Err != nil {
t.Fatalf("unexpected scene message finished event: %#v", got)
}
}
func TestScenePassDoesNotPersistSessionData(t *testing.T) {
commandCalled := false
plugin := NewPlugin[NoData]("wizard")
plugin.NewCommand(func(ctx *MsgContext, db NoData) error {
commandCalled = true
return nil
}, "ping")
plugin.NewScene("signup").
SetEntry("start").
OnStep("start", func(ctx *SceneContext, db NoData) (SceneResult, error) {
if err := ctx.SaveData(struct {
Value string `json:"value"`
}{Value: "changed"}); err != nil {
t.Fatalf("SaveData returned error: %v", err)
}
return ctx.Pass(), nil
})
bot := &Bot[NoData]{
logger: slog.CreateLogger(),
prefixes: []string{"/"},
sessionStore: NewMemorySessionStore(),
sceneScopePriority: []SceneScope{SceneScopeUserChat, SceneScopeChat, SceneScopeUser},
}
bot.AddPlugins(plugin)
enterCtx := &MsgContext{
Msg: &tgapi.Message{Chat: &tgapi.Chat{ID: 100, Type: tgapi.ChatTypePrivate}},
FromID: 42,
sceneRuntime: bot,
}
if err := enterCtx.EnterScene("signup"); err != nil {
t.Fatalf("EnterScene returned error: %v", err)
}
key, ok := buildSceneKey(SceneScopeUserChat, &MsgContext{
Msg: &tgapi.Message{Chat: &tgapi.Chat{ID: 100, Type: tgapi.ChatTypePrivate}},
FromID: 42,
})
if !ok {
t.Fatal("expected scene key to be built")
}
before, err := bot.sessionStore.Get(key)
if err != nil {
t.Fatalf("Get before handle returned error: %v", err)
}
if before.HasData() {
t.Fatalf("expected empty session data before handle, got %#v", before)
}
bot.handle(context.Background(), &tgapi.Update{
UpdateID: 3,
Type: tgapi.UpdateTypeMessage,
Message: &tgapi.Message{
MessageID: 9,
Text: "/ping",
Chat: &tgapi.Chat{ID: 100, Type: tgapi.ChatTypePrivate},
From: &tgapi.User{ID: 42},
},
})
if !commandCalled {
t.Fatal("expected normal command routing to continue after SceneActionPass")
}
after, err := bot.sessionStore.Get(key)
if err != nil {
t.Fatalf("Get after handle returned error: %v", err)
}
if after.Scene != "signup" || after.Step != "start" {
t.Fatalf("unexpected session after pass: %#v", after)
}
if after.HasData() {
t.Fatalf("expected SceneActionPass to leave session data unchanged, got %#v", after)
}
}
func TestSceneMessageFallbackRunsWhenNoCommandOrStepMatch(t *testing.T) {
fallbackCalled := false
plugin := NewPlugin[NoData]("wizard")
plugin.NewScene("signup").
SetEntry("start").
OnStep("start", func(ctx *SceneContext, db NoData) (SceneResult, error) {
return ctx.Stay(), nil
}).
OnMessage(func(ctx *SceneContext, db NoData) (SceneResult, error) {
fallbackCalled = true
if ctx.Text != "hello fallback" {
t.Fatalf("unexpected fallback text: got %q want %q", ctx.Text, "hello fallback")
}
return ctx.Exit(), nil
})
bot := &Bot[NoData]{
logger: slog.CreateLogger(),
prefixes: []string{"/"},
sessionStore: NewMemorySessionStore(),
sceneScopePriority: []SceneScope{SceneScopeUserChat, SceneScopeChat, SceneScopeUser},
}
bot.AddPlugins(plugin)
enterCtx := &MsgContext{
Msg: &tgapi.Message{Chat: &tgapi.Chat{ID: 100, Type: tgapi.ChatTypePrivate}},
FromID: 42,
sceneRuntime: bot,
}
if err := enterCtx.EnterScene("signup"); err != nil {
t.Fatalf("EnterScene returned error: %v", err)
}
key, ok := buildSceneKey(SceneScopeUserChat, &MsgContext{
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: "signup", Step: "unknown"}); err != nil {
t.Fatalf("Set returned error: %v", err)
}
bot.handle(context.Background(), &tgapi.Update{
UpdateID: 4,
Type: tgapi.UpdateTypeMessage,
Message: &tgapi.Message{
MessageID: 10,
Text: "hello fallback",
Chat: &tgapi.Chat{ID: 100, Type: tgapi.ChatTypePrivate},
From: &tgapi.User{ID: 42},
},
})
if !fallbackCalled {
t.Fatal("expected scene fallback handler to be called")
}
}
func TestFindSceneSessionSupportsUserScopeWithoutMessage(t *testing.T) {
bot := &Bot[NoData]{
logger: slog.CreateLogger(),
sessionStore: NewMemorySessionStore(),
sceneScopePriority: []SceneScope{SceneScopeUser, SceneScopeChat, SceneScopeUserChat},
}
if err := bot.sessionStore.Set("user_id:42", SceneSession{Scene: "signup", Step: "start"}); err != nil {
t.Fatalf("Set returned error: %v", err)
}
key, session, err := bot.findSceneSession(&MsgContext{FromID: 42})
if err != nil {
t.Fatalf("findSceneSession returned error: %v", err)
}
if key != "user_id:42" {
t.Fatalf("unexpected session key: got %q want %q", key, "user_id:42")
}
if session.Scene != "signup" || session.Step != "start" {
t.Fatalf("unexpected session: %#v", session)
}
}
func TestSceneStoreErrorsPropagate(t *testing.T) {
getErr := errors.New("get failed")
setErr := errors.New("set failed")
t.Run("find scene session get error", func(t *testing.T) {
bot := &Bot[NoData]{
logger: slog.CreateLogger(),
sessionStore: failingSessionStore{getErr: getErr},
sceneScopePriority: []SceneScope{SceneScopeUser},
}
_, _, err := bot.findSceneSession(&MsgContext{FromID: 42})
if !errors.Is(err, getErr) {
t.Fatalf("expected getErr, got %v", err)
}
})
t.Run("apply scene result set error", func(t *testing.T) {
scene := NewScene[NoData]("signup").OnStep("start", func(ctx *SceneContext, db NoData) (SceneResult, error) {
return ctx.Stay(), nil
})
bot := &Bot[NoData]{
logger: slog.CreateLogger(),
sessionStore: failingSessionStore{setErr: setErr},
sceneScopePriority: []SceneScope{SceneScopeUserChat, SceneScopeChat, SceneScopeUser},
}
_, err := bot.applySceneResult(scene, &SceneContext{
MsgContext: &MsgContext{},
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)
}
})
}
+44
View File
@@ -0,0 +1,44 @@
package laniakea
// SplitMessageText splits plain text into Telegram-safe message chunks.
//
// The function preserves the original text exactly: concatenating all returned
// chunks reconstructs text byte-for-byte. It prefers splitting at newlines or
// spaces within the Telegram message limit and falls back to hard rune-based
// splits when no separator is available.
func SplitMessageText(text string) []string {
return splitTextByLimit(text, maxMessageTextLen)
}
func splitTextByLimit(text string, limit int) []string {
if text == "" {
return nil
}
runes := []rune(text)
chunks := make([]string, 0, len(runes)/limit+1)
for start := 0; start < len(runes); {
end := start + limit
if end >= len(runes) {
chunks = append(chunks, string(runes[start:]))
break
}
splitAt := -1
for i := end - 1; i > start; i-- {
if runes[i] == '\n' || runes[i] == ' ' {
splitAt = i + 1
break
}
}
if splitAt == -1 {
splitAt = end
}
chunks = append(chunks, string(runes[start:splitAt]))
start = splitAt
}
return chunks
}
+19 -21
View File
@@ -9,8 +9,8 @@ import (
"net/http" "net/http"
"time" "time"
"git.nix13.pw/scuroneko/laniakea/utils" "git.scuroneko.dev/scuroneko/laniakea/utils"
"git.nix13.pw/scuroneko/slog" "git.scuroneko.dev/scuroneko/slog"
) )
// APIOpts holds configuration options for initializing the Telegram API client. // APIOpts holds configuration options for initializing the Telegram API client.
@@ -98,6 +98,11 @@ type API struct {
// Always call Close() when done to release resources. // Always call Close() when done to release resources.
func NewAPI(opts *APIOpts) *API { func NewAPI(opts *APIOpts) *API {
l := utils.CreateLogger("API", utils.GetLoggerLevel()) l := utils.CreateLogger("API", utils.GetLoggerLevel())
if opts == nil {
l.Errorln("Set API options")
_ = l.Close()
return nil
}
client := opts.client client := opts.client
if client == nil { if client == nil {
@@ -124,6 +129,9 @@ func NewAPI(opts *APIOpts) *API {
// See https://core.telegram.org/bots/api // See https://core.telegram.org/bots/api
func (api *API) Close() error { func (api *API) Close() error {
api.pool.stop() api.pool.stop()
if api.client != nil {
api.client.CloseIdleConnections()
}
return api.logger.Close() return api.logger.Close()
} }
@@ -149,37 +157,29 @@ type ApiResponse[R any] struct {
Parameters *ResponseParameters `json:"parameters,omitempty"` Parameters *ResponseParameters `json:"parameters,omitempty"`
} }
// TelegramRequest is an internal helper struct. // TelegramRequest is a low-level Telegram API request wrapper.
// DO NOT USE NewRequest or NewRequestWithChatID — they are unsafe and discouraged.
// Instead, use explicit methods like SendMessage, GetUpdates, etc.
// //
// Why? Because using generics with arbitrary types P and R leads to: // Prefer method-specific helpers such as SendMessage or GetUpdates. TelegramRequest
// - No compile-time validation of parameters // bypasses method-specific parameter types and convenience helpers, so callers are
// - No IDE autocompletion // responsible for using the correct method name and compatible request and response types.
// - Runtime panics on malformed JSON // In that sense it is an unsafe escape hatch compared with the typed API surface.
// - Hard-to-debug errors
//
// Recommended: Define specific methods for each Telegram method (see below).
type TelegramRequest[R, P any] struct { type TelegramRequest[R, P any] struct {
method string method string
params P params P
chatId int64 chatId int64
} }
// NewRequest creates an untyped TelegramRequest for the given method and params with no chat ID. // NewRequest creates a low-level TelegramRequest with no associated chat ID.
func NewRequest[R, P any](method string, params P) TelegramRequest[R, P] { func NewRequest[R, P any](method string, params P) TelegramRequest[R, P] {
return TelegramRequest[R, P]{method, params, 0} return TelegramRequest[R, P]{method, params, 0}
} }
// NewRequestWithChatID creates an untyped TelegramRequest with an associated chat ID. // NewRequestWithChatID creates a low-level TelegramRequest with an associated chat ID.
// The chat ID is used for per-chat rate limiting. // The chat ID is used for per-chat rate limiting.
func NewRequestWithChatID[R, P any](method string, params P, chatId int64) TelegramRequest[R, P] { func NewRequestWithChatID[R, P any](method string, params P, chatId int64) TelegramRequest[R, P] {
return TelegramRequest[R, P]{method, params, chatId} return TelegramRequest[R, P]{method, params, chatId}
} }
// doRequest performs a single HTTP request to Telegram API.
// Handles rate limiting, retries on 429, and parses responses.
// Must be called within a worker pool context if using DoWithContext.
func (r TelegramRequest[R, P]) doRequest(ctx context.Context, api *API) (R, error) { func (r TelegramRequest[R, P]) doRequest(ctx context.Context, api *API) (R, error) {
var zero R var zero R
reqData, err := json.Marshal(r.params) reqData, err := json.Marshal(r.params)
@@ -296,15 +296,13 @@ func (r TelegramRequest[R, P]) Do(api *API) (R, error) {
return r.DoWithContext(context.Background(), api) return r.DoWithContext(context.Background(), api)
} }
// readBody reads and limits response body to prevent memory exhaustion. // Internal helper that reads and caps a Telegram response body.
// Telegram responses are typically small (<1MB), but we cap at 10MB.
func readBody(body io.ReadCloser) ([]byte, error) { func readBody(body io.ReadCloser) ([]byte, error) {
reader := io.LimitReader(body, 10<<20) // 10 MB reader := io.LimitReader(body, 10<<20) // 10 MB
return io.ReadAll(reader) return io.ReadAll(reader)
} }
// parseBody unmarshals a Telegram API response into a typed ApiResponse. // Internal helper that parses a typed Telegram API response body.
// Only returns an error on malformed JSON; non-OK responses are left for the caller to handle.
func parseBody[R any](data []byte) (ApiResponse[R], error) { func parseBody[R any](data []byte) (ApiResponse[R], error) {
var resp ApiResponse[R] var resp ApiResponse[R]
err := json.Unmarshal(data, &resp) err := json.Unmarshal(data, &resp)
+34
View File
@@ -13,6 +13,15 @@ func (fn roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {
return fn(req) return fn(req)
} }
type closingTransport struct {
roundTripFunc
closed bool
}
func (t *closingTransport) CloseIdleConnections() {
t.closed = true
}
func TestAPILeavesAcceptEncodingToHTTPTransport(t *testing.T) { func TestAPILeavesAcceptEncodingToHTTPTransport(t *testing.T) {
var gotPath string var gotPath string
var gotAcceptEncoding string var gotAcceptEncoding string
@@ -54,3 +63,28 @@ func TestAPILeavesAcceptEncodingToHTTPTransport(t *testing.T) {
t.Fatalf("expected empty Accept-Encoding header, got %q", gotAcceptEncoding) t.Fatalf("expected empty Accept-Encoding header, got %q", gotAcceptEncoding)
} }
} }
func TestAPICloseClosesIdleConnections(t *testing.T) {
transport := &closingTransport{
roundTripFunc: func(req *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":true,"result":{"id":1,"is_bot":true,"first_name":"Test"}}`)),
}, nil
},
}
api := NewAPI(
NewAPIOpts("token").
SetAPIUrl("https://example.test").
SetHTTPClient(&http.Client{Transport: transport}),
)
if err := api.Close(); err != nil {
t.Fatalf("Close returned error: %v", err)
}
if !transport.closed {
t.Fatal("expected Close to close idle HTTP connections")
}
}
+36 -36
View File
@@ -2,9 +2,9 @@ package tgapi
import "context" import "context"
// SendPhotoP holds parameters for the sendPhoto method. // SendPhoto holds parameters for the sendPhoto method.
// See https://core.telegram.org/bots/api#sendphoto // See https://core.telegram.org/bots/api#sendphoto
type SendPhotoP struct { type SendPhoto struct {
BusinessConnectionID string `json:"business_connection_id,omitempty"` BusinessConnectionID string `json:"business_connection_id,omitempty"`
ChatID int64 `json:"chat_id"` ChatID int64 `json:"chat_id"`
MessageThreadID int `json:"message_thread_id,omitempty"` MessageThreadID int `json:"message_thread_id,omitempty"`
@@ -29,7 +29,7 @@ type SendPhotoP struct {
// SendPhoto sends a photo. // SendPhoto sends a photo.
// See https://core.telegram.org/bots/api#sendphoto // See https://core.telegram.org/bots/api#sendphoto
func (api *API) SendPhoto(params SendPhotoP) (Message, error) { func (api *API) SendPhoto(params SendPhoto) (Message, error) {
req := NewRequestWithChatID[Message]("sendPhoto", params, params.ChatID) req := NewRequestWithChatID[Message]("sendPhoto", params, params.ChatID)
return req.Do(api) return req.Do(api)
} }
@@ -37,14 +37,14 @@ func (api *API) SendPhoto(params SendPhotoP) (Message, error) {
// SendPhotoWithContext is the context-aware variant of SendPhoto. // SendPhotoWithContext is the context-aware variant of SendPhoto.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#sendphoto // See https://core.telegram.org/bots/api#sendphoto
func (api *API) SendPhotoWithContext(ctx context.Context, params SendPhotoP) (Message, error) { func (api *API) SendPhotoWithContext(ctx context.Context, params SendPhoto) (Message, error) {
req := NewRequestWithChatID[Message]("sendPhoto", params, params.ChatID) req := NewRequestWithChatID[Message]("sendPhoto", params, params.ChatID)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// SendAudioP holds parameters for the sendAudio method. // SendAudio holds parameters for the sendAudio method.
// See https://core.telegram.org/bots/api#sendaudio // See https://core.telegram.org/bots/api#sendaudio
type SendAudioP struct { type SendAudio struct {
BusinessConnectionID string `json:"business_connection_id,omitempty"` BusinessConnectionID string `json:"business_connection_id,omitempty"`
ChatID int64 `json:"chat_id"` ChatID int64 `json:"chat_id"`
MessageThreadID int `json:"message_thread_id,omitempty"` MessageThreadID int `json:"message_thread_id,omitempty"`
@@ -71,7 +71,7 @@ type SendAudioP struct {
// SendAudio sends an audio file. // SendAudio sends an audio file.
// See https://core.telegram.org/bots/api#sendaudio // See https://core.telegram.org/bots/api#sendaudio
func (api *API) SendAudio(params SendAudioP) (Message, error) { func (api *API) SendAudio(params SendAudio) (Message, error) {
req := NewRequestWithChatID[Message]("sendAudio", params, params.ChatID) req := NewRequestWithChatID[Message]("sendAudio", params, params.ChatID)
return req.Do(api) return req.Do(api)
} }
@@ -79,14 +79,14 @@ func (api *API) SendAudio(params SendAudioP) (Message, error) {
// SendAudioWithContext is the context-aware variant of SendAudio. // SendAudioWithContext is the context-aware variant of SendAudio.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#sendaudio // See https://core.telegram.org/bots/api#sendaudio
func (api *API) SendAudioWithContext(ctx context.Context, params SendAudioP) (Message, error) { func (api *API) SendAudioWithContext(ctx context.Context, params SendAudio) (Message, error) {
req := NewRequestWithChatID[Message]("sendAudio", params, params.ChatID) req := NewRequestWithChatID[Message]("sendAudio", params, params.ChatID)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// SendDocumentP holds parameters for the sendDocument method. // SendDocument holds parameters for the sendDocument method.
// See https://core.telegram.org/bots/api#senddocument // See https://core.telegram.org/bots/api#senddocument
type SendDocumentP struct { type SendDocument struct {
BusinessConnectionID string `json:"business_connection_id,omitempty"` BusinessConnectionID string `json:"business_connection_id,omitempty"`
ChatID int64 `json:"chat_id"` ChatID int64 `json:"chat_id"`
MessageThreadID int `json:"message_thread_id,omitempty"` MessageThreadID int `json:"message_thread_id,omitempty"`
@@ -111,7 +111,7 @@ type SendDocumentP struct {
// SendDocument sends a document. // SendDocument sends a document.
// See https://core.telegram.org/bots/api#senddocument // See https://core.telegram.org/bots/api#senddocument
func (api *API) SendDocument(params SendDocumentP) (Message, error) { func (api *API) SendDocument(params SendDocument) (Message, error) {
req := NewRequestWithChatID[Message]("sendDocument", params, params.ChatID) req := NewRequestWithChatID[Message]("sendDocument", params, params.ChatID)
return req.Do(api) return req.Do(api)
} }
@@ -119,14 +119,14 @@ func (api *API) SendDocument(params SendDocumentP) (Message, error) {
// SendDocumentWithContext is the context-aware variant of SendDocument. // SendDocumentWithContext is the context-aware variant of SendDocument.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#senddocument // See https://core.telegram.org/bots/api#senddocument
func (api *API) SendDocumentWithContext(ctx context.Context, params SendDocumentP) (Message, error) { func (api *API) SendDocumentWithContext(ctx context.Context, params SendDocument) (Message, error) {
req := NewRequestWithChatID[Message]("sendDocument", params, params.ChatID) req := NewRequestWithChatID[Message]("sendDocument", params, params.ChatID)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// SendVideoP holds parameters for the sendVideo method. // SendVideo holds parameters for the sendVideo method.
// See https://core.telegram.org/bots/api#sendvideo // See https://core.telegram.org/bots/api#sendvideo
type SendVideoP struct { type SendVideo struct {
BusinessConnectionID string `json:"business_connection_id,omitempty"` BusinessConnectionID string `json:"business_connection_id,omitempty"`
ChatID int64 `json:"chat_id"` ChatID int64 `json:"chat_id"`
MessageThreadID int `json:"message_thread_id,omitempty"` MessageThreadID int `json:"message_thread_id,omitempty"`
@@ -159,7 +159,7 @@ type SendVideoP struct {
// SendVideo sends a video. // SendVideo sends a video.
// See https://core.telegram.org/bots/api#sendvideo // See https://core.telegram.org/bots/api#sendvideo
func (api *API) SendVideo(params SendVideoP) (Message, error) { func (api *API) SendVideo(params SendVideo) (Message, error) {
req := NewRequestWithChatID[Message]("sendVideo", params, params.ChatID) req := NewRequestWithChatID[Message]("sendVideo", params, params.ChatID)
return req.Do(api) return req.Do(api)
} }
@@ -167,14 +167,14 @@ func (api *API) SendVideo(params SendVideoP) (Message, error) {
// SendVideoWithContext is the context-aware variant of SendVideo. // SendVideoWithContext is the context-aware variant of SendVideo.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#sendvideo // See https://core.telegram.org/bots/api#sendvideo
func (api *API) SendVideoWithContext(ctx context.Context, params SendVideoP) (Message, error) { func (api *API) SendVideoWithContext(ctx context.Context, params SendVideo) (Message, error) {
req := NewRequestWithChatID[Message]("sendVideo", params, params.ChatID) req := NewRequestWithChatID[Message]("sendVideo", params, params.ChatID)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// SendAnimationP holds parameters for the sendAnimation method. // SendAnimation holds parameters for the sendAnimation method.
// See https://core.telegram.org/bots/api#sendanimation // See https://core.telegram.org/bots/api#sendanimation
type SendAnimationP struct { type SendAnimation struct {
BusinessConnectionID string `json:"business_connection_id,omitempty"` BusinessConnectionID string `json:"business_connection_id,omitempty"`
ChatID int64 `json:"chat_id"` ChatID int64 `json:"chat_id"`
MessageThreadID int `json:"message_thread_id,omitempty"` MessageThreadID int `json:"message_thread_id,omitempty"`
@@ -203,7 +203,7 @@ type SendAnimationP struct {
// SendAnimation sends an animation file (GIF or H.264/MPEG-4 AVC video without sound). // SendAnimation sends an animation file (GIF or H.264/MPEG-4 AVC video without sound).
// See https://core.telegram.org/bots/api#sendanimation // See https://core.telegram.org/bots/api#sendanimation
func (api *API) SendAnimation(params SendAnimationP) (Message, error) { func (api *API) SendAnimation(params SendAnimation) (Message, error) {
req := NewRequestWithChatID[Message]("sendAnimation", params, params.ChatID) req := NewRequestWithChatID[Message]("sendAnimation", params, params.ChatID)
return req.Do(api) return req.Do(api)
} }
@@ -211,14 +211,14 @@ func (api *API) SendAnimation(params SendAnimationP) (Message, error) {
// SendAnimationWithContext is the context-aware variant of SendAnimation. // SendAnimationWithContext is the context-aware variant of SendAnimation.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#sendanimation // See https://core.telegram.org/bots/api#sendanimation
func (api *API) SendAnimationWithContext(ctx context.Context, params SendAnimationP) (Message, error) { func (api *API) SendAnimationWithContext(ctx context.Context, params SendAnimation) (Message, error) {
req := NewRequestWithChatID[Message]("sendAnimation", params, params.ChatID) req := NewRequestWithChatID[Message]("sendAnimation", params, params.ChatID)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// SendVoiceP holds parameters for the sendVoice method. // SendVoice holds parameters for the sendVoice method.
// See https://core.telegram.org/bots/api#sendvoice // See https://core.telegram.org/bots/api#sendvoice
type SendVoiceP struct { type SendVoice struct {
BusinessConnectionID string `json:"business_connection_id,omitempty"` BusinessConnectionID string `json:"business_connection_id,omitempty"`
ChatID int64 `json:"chat_id"` ChatID int64 `json:"chat_id"`
MessageThreadID int `json:"message_thread_id,omitempty"` MessageThreadID int `json:"message_thread_id,omitempty"`
@@ -241,7 +241,7 @@ type SendVoiceP struct {
// SendVoice sends a voice note. // SendVoice sends a voice note.
// See https://core.telegram.org/bots/api#sendvoice // See https://core.telegram.org/bots/api#sendvoice
func (api *API) SendVoice(params *SendVoiceP) (Message, error) { func (api *API) SendVoice(params SendVoice) (Message, error) {
req := NewRequestWithChatID[Message]("sendVoice", params, params.ChatID) req := NewRequestWithChatID[Message]("sendVoice", params, params.ChatID)
return req.Do(api) return req.Do(api)
} }
@@ -249,14 +249,14 @@ func (api *API) SendVoice(params *SendVoiceP) (Message, error) {
// SendVoiceWithContext is the context-aware variant of SendVoice. // SendVoiceWithContext is the context-aware variant of SendVoice.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#sendvoice // See https://core.telegram.org/bots/api#sendvoice
func (api *API) SendVoiceWithContext(ctx context.Context, params *SendVoiceP) (Message, error) { func (api *API) SendVoiceWithContext(ctx context.Context, params SendVoice) (Message, error) {
req := NewRequestWithChatID[Message]("sendVoice", params, params.ChatID) req := NewRequestWithChatID[Message]("sendVoice", params, params.ChatID)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// SendVideoNoteP holds parameters for the sendVideoNote method. // SendVideoNote holds parameters for the sendVideoNote method.
// See https://core.telegram.org/bots/api#sendvideonote // See https://core.telegram.org/bots/api#sendvideonote
type SendVideoNoteP struct { type SendVideoNote struct {
BusinessConnectionID string `json:"business_connection_id,omitempty"` BusinessConnectionID string `json:"business_connection_id,omitempty"`
ChatID int64 `json:"chat_id"` ChatID int64 `json:"chat_id"`
MessageThreadID int `json:"message_thread_id,omitempty"` MessageThreadID int `json:"message_thread_id,omitempty"`
@@ -278,7 +278,7 @@ type SendVideoNoteP struct {
// SendVideoNote sends a video note (rounded video message). // SendVideoNote sends a video note (rounded video message).
// See https://core.telegram.org/bots/api#sendvideonote // See https://core.telegram.org/bots/api#sendvideonote
func (api *API) SendVideoNote(params SendVideoNoteP) (Message, error) { func (api *API) SendVideoNote(params SendVideoNote) (Message, error) {
req := NewRequestWithChatID[Message]("sendVideoNote", params, params.ChatID) req := NewRequestWithChatID[Message]("sendVideoNote", params, params.ChatID)
return req.Do(api) return req.Do(api)
} }
@@ -286,14 +286,14 @@ func (api *API) SendVideoNote(params SendVideoNoteP) (Message, error) {
// SendVideoNoteWithContext is the context-aware variant of SendVideoNote. // SendVideoNoteWithContext is the context-aware variant of SendVideoNote.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#sendvideonote // See https://core.telegram.org/bots/api#sendvideonote
func (api *API) SendVideoNoteWithContext(ctx context.Context, params SendVideoNoteP) (Message, error) { func (api *API) SendVideoNoteWithContext(ctx context.Context, params SendVideoNote) (Message, error) {
req := NewRequestWithChatID[Message]("sendVideoNote", params, params.ChatID) req := NewRequestWithChatID[Message]("sendVideoNote", params, params.ChatID)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// SendPaidMediaP holds parameters for the sendPaidMedia method. // SendPaidMedia holds parameters for the sendPaidMedia method.
// See https://core.telegram.org/bots/api#sendpaidmedia // See https://core.telegram.org/bots/api#sendpaidmedia
type SendPaidMediaP struct { type SendPaidMedia struct {
BusinessConnectionID string `json:"business_connection_id,omitempty"` BusinessConnectionID string `json:"business_connection_id,omitempty"`
ChatID int64 `json:"chat_id"` ChatID int64 `json:"chat_id"`
MessageThreadID int `json:"message_thread_id,omitempty"` MessageThreadID int `json:"message_thread_id,omitempty"`
@@ -317,7 +317,7 @@ type SendPaidMediaP struct {
// SendPaidMedia sends paid media. // SendPaidMedia sends paid media.
// See https://core.telegram.org/bots/api#sendpaidmedia // See https://core.telegram.org/bots/api#sendpaidmedia
func (api *API) SendPaidMedia(params SendPaidMediaP) (Message, error) { func (api *API) SendPaidMedia(params SendPaidMedia) (Message, error) {
req := NewRequestWithChatID[Message]("sendPaidMedia", params, params.ChatID) req := NewRequestWithChatID[Message]("sendPaidMedia", params, params.ChatID)
return req.Do(api) return req.Do(api)
} }
@@ -325,14 +325,14 @@ func (api *API) SendPaidMedia(params SendPaidMediaP) (Message, error) {
// SendPaidMediaWithContext is the context-aware variant of SendPaidMedia. // SendPaidMediaWithContext is the context-aware variant of SendPaidMedia.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#sendpaidmedia // See https://core.telegram.org/bots/api#sendpaidmedia
func (api *API) SendPaidMediaWithContext(ctx context.Context, params SendPaidMediaP) (Message, error) { func (api *API) SendPaidMediaWithContext(ctx context.Context, params SendPaidMedia) (Message, error) {
req := NewRequestWithChatID[Message]("sendPaidMedia", params, params.ChatID) req := NewRequestWithChatID[Message]("sendPaidMedia", params, params.ChatID)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// SendMediaGroupP holds parameters for the sendMediaGroup method. // SendMediaGroup holds parameters for the sendMediaGroup method.
// See https://core.telegram.org/bots/api#sendmediagroup // See https://core.telegram.org/bots/api#sendmediagroup
type SendMediaGroupP struct { type SendMediaGroup struct {
BusinessConnectionID string `json:"business_connection_id,omitempty"` BusinessConnectionID string `json:"business_connection_id,omitempty"`
ChatID int64 `json:"chat_id"` ChatID int64 `json:"chat_id"`
MessageThreadID int `json:"message_thread_id,omitempty"` MessageThreadID int `json:"message_thread_id,omitempty"`
@@ -348,7 +348,7 @@ type SendMediaGroupP struct {
// SendMediaGroup sends a group of photos, videos, documents or audios as an album. // SendMediaGroup sends a group of photos, videos, documents or audios as an album.
// See https://core.telegram.org/bots/api#sendmediagroup // See https://core.telegram.org/bots/api#sendmediagroup
func (api *API) SendMediaGroup(params SendMediaGroupP) ([]Message, error) { func (api *API) SendMediaGroup(params SendMediaGroup) ([]Message, error) {
req := NewRequestWithChatID[[]Message]("sendMediaGroup", params, params.ChatID) req := NewRequestWithChatID[[]Message]("sendMediaGroup", params, params.ChatID)
return req.Do(api) return req.Do(api)
} }
@@ -356,7 +356,7 @@ func (api *API) SendMediaGroup(params SendMediaGroupP) ([]Message, error) {
// SendMediaGroupWithContext is the context-aware variant of SendMediaGroup. // SendMediaGroupWithContext is the context-aware variant of SendMediaGroup.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#sendmediagroup // See https://core.telegram.org/bots/api#sendmediagroup
func (api *API) SendMediaGroupWithContext(ctx context.Context, params SendMediaGroupP) ([]Message, error) { func (api *API) SendMediaGroupWithContext(ctx context.Context, params SendMediaGroup) ([]Message, error) {
req := NewRequestWithChatID[[]Message]("sendMediaGroup", params, params.ChatID) req := NewRequestWithChatID[[]Message]("sendMediaGroup", params, params.ChatID)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
+254 -6
View File
@@ -1,5 +1,253 @@
package tgapi package tgapi
type Animation struct {
FileID string `json:"file_id"`
FileUniqueID string `json:"file_unique_id"`
Width int `json:"width"`
Height int `json:"height"`
Duration int `json:"duration"`
Thumbnail *PhotoSize `json:"thumbnail,omitempty"`
FileName string `json:"file_name"`
MimeType string `json:"mime_type"`
FileSize int `json:"file_size"`
}
// Audio represents an audio file to be treated as music by the Telegram clients.
// See https://core.telegram.org/bots/api#audio
type Audio struct {
FileID string `json:"file_id"`
FileUniqueID string `json:"file_unique_id"`
Duration int `json:"duration"`
Performer string `json:"performer,omitempty"`
Title string `json:"title,omitempty"`
FileName string `json:"file_name,omitempty"`
MimeType string `json:"mime_type,omitempty"`
FileSize int64 `json:"file_size,omitempty"`
Thumbnail *PhotoSize `json:"thumbnail,omitempty"`
}
type Document struct {
FileID string `json:"file_id"`
FileUniqueID string `json:"file_unique_id"`
Thumbnail *PhotoSize `json:"thumbnail,omitempty"`
FileName string `json:"file_name"`
MimeType string `json:"mime_type"`
FileSize int `json:"file_size,omitempty"`
}
// Story represents a story.
type Story struct {
Chat Chat `json:"chat"`
ID int `json:"id"`
}
type Video struct {
FileID string `json:"file_id"`
FileUniqueID string `json:"file_unique_id"`
Width int `json:"width"`
Height int `json:"height"`
Duration int `json:"duration"`
Thumbnail *PhotoSize `json:"thumbnail,omitempty"`
Cover []PhotoSize `json:"cover,omitempty"`
StartTimestamp int64 `json:"start_timestamp"`
Qualities []VideoQuality `json:"qualities,omitempty"`
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.
// See https://core.telegram.org/bots/api#videoquality
type VideoQuality struct {
FileID string `json:"file_id"`
FileUniqueID string `json:"file_unique_id"`
Width int `json:"width"`
Height int `json:"height"`
Codec string `json:"codec"`
FileSize int64 `json:"file_size,omitempty"`
}
type VideoNote struct {
FileID string `json:"file_id"`
FileUniqueID string `json:"file_unique_id"`
Length int `json:"length"`
Duration int `json:"duration"`
Thumbnail *PhotoSize `json:"thumbnail,omitempty"`
FileSize int64 `json:"file_size,omitempty"`
}
type Voice struct {
FileID string `json:"file_id"`
FileUniqueID string `json:"file_unique_id"`
Duration int `json:"duration"`
MimeType string `json:"mime_type,omitempty"`
FileSize int `json:"file_size,omitempty"`
}
type PaidMediaInfo struct {
StarCount int `json:"star_count"`
PaidMedia []PaidMedia `json:"paid_media"`
}
type PaidMediaType string
const (
PaidMediaPreviewType PaidMediaType = "preview"
PaidMediaPhotoType PaidMediaType = "photo"
PaidMediaVideoType PaidMediaType = "video"
)
type PaidMedia struct {
Type PaidMediaType `json:"type,omitempty"`
Width int `json:"width,omitempty"`
Height int `json:"height,omitempty"`
Duration int `json:"duration,omitempty"`
Photo []PhotoSize `json:"photo,omitempty"`
Video *Video `json:"video,omitempty"`
}
type Contact struct {
PhoneNumber string `json:"phone_number"`
FirstName string `json:"first_name"`
LastName string `json:"last_name,omitempty"`
UserID int64 `json:"user_id,omitempty"`
Vcard string `json:"vcard,omitempty"`
}
type Dice struct {
Emoji string `json:"emoji"`
Value int `json:"value"`
}
// PollOption contains information about one answer option in a poll.
// See https://core.telegram.org/bots/api#polloption
type PollOption struct {
PersistentID string `json:"persistent_id"`
Text string `json:"text"`
TextEntities []MessageEntity `json:"text_entities"`
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"`
}
// InputPollOption contains information about one answer option in a poll to be sent.
// 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"`
}
type PollOptionAdded struct {
PollMessage *InaccessibleMessage `json:"poll_message,omitempty"`
OptionPersistentID string `json:"option_persistent_id"`
OptionText string `json:"option_text"`
OptionTextEntities []MessageEntity `json:"option_text_entities,omitempty"`
}
type PollOptionDeleted struct {
PollMessage *InaccessibleMessage `json:"poll_message,omitempty"`
OptionPersistentID string `json:"option_persistent_id"`
OptionText string `json:"option_text"`
OptionTextEntities []MessageEntity `json:"option_text_entities,omitempty"`
}
// PollType represents the type of a poll.
type PollType string
const (
// PollTypeRegular identifies a regular poll.
PollTypeRegular PollType = "regular"
// PollTypeQuiz identifies a quiz poll.
PollTypeQuiz PollType = "quiz"
)
// PollAnswer represents an answer of a user in a poll.
// See https://core.telegram.org/bots/api#pollanswer
type PollAnswer struct {
PollID string `json:"poll_id"`
VoterChat Chat `json:"voter_chat"`
User User `json:"user"`
OptionIDs []int `json:"option_ids"`
OptionPersistentIDs []string `json:"option_persistent_ids"`
}
// Poll contains information about a poll.
// See https://core.telegram.org/bots/api#poll
type Poll struct {
ID string `json:"id"`
Question string `json:"question"`
QuestionEntities []MessageEntity `json:"question_entities"`
Options []PollOption `json:"options"`
TotalVoterCount int `json:"total_voter_count"`
IsClosed bool `json:"is_closed"`
IsAnonymous bool `json:"is_anonymous"`
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"`
}
type ChecklistTask struct {
ID int `json:"id"`
Text string `json:"text"`
TextEntities []MessageEntity `json:"text_entities,omitempty"`
CompletedByUser *User `json:"completed_by_user,omitempty"`
CompletedByChat *Chat `json:"completed_by_chat,omitempty"`
CompletionDate int `json:"completion_date,omitempty"`
}
type Checklist struct {
Title string `json:"title"`
TitleEntities []MessageEntity `json:"title_entities,omitempty"`
Tasks []ChecklistTask `json:"tasks"`
OthersCanAddTasks bool `json:"others_can_add_tasks,omitempty"`
OthersCanMarkTasksAsDone bool `json:"others_can_mark_tasks_as_done,omitempty"`
}
// InputChecklistTask describes a task in a checklist.
type InputChecklistTask struct {
ID int `json:"id"`
Text string `json:"text"`
ParseMode ParseMode `json:"parse_mode,omitempty"`
TextEntities []MessageEntity `json:"text_entities,omitempty"`
}
// InputChecklist represents a checklist to be sent.
type InputChecklist struct {
Title string `json:"title"`
ParseMode ParseMode `json:"parse_mode,omitempty"`
TitleEntities []MessageEntity `json:"title_entities,omitempty"`
Tasks []InputChecklistTask `json:"tasks"`
OtherCanAddTasks bool `json:"other_can_add_tasks,omitempty"`
OtherCanMarkTasksAsDone bool `json:"other_can_mark_tasks_as_done,omitempty"`
}
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"`
}
type ChecklistTasksAdded struct {
ChecklistMessage *Message `json:"checklist_message,omitempty"`
Tasks []ChecklistTask `json:"tasks"`
}
// InputMediaType represents the type of input media. // InputMediaType represents the type of input media.
type InputMediaType string type InputMediaType string
@@ -55,12 +303,12 @@ type InputPaidMedia struct {
Type InputPaidMediaType `json:"type"` Type InputPaidMediaType `json:"type"`
Media string `json:"media"` Media string `json:"media"`
Cover string `json:"cover"` Cover *string `json:"cover,omitempty"`
StartTimestamp int64 `json:"start_timestamp"` StartTimestamp *int64 `json:"start_timestamp,omitempty"`
Width int `json:"width"` Width *int `json:"width,omitempty"`
Height int `json:"height"` Height *int `json:"height,omitempty"`
Duration int `json:"duration"` Duration *int `json:"duration,omitempty"`
SupportsStreaming bool `json:"supports_streaming"` SupportsStreaming *bool `json:"supports_streaming,omitempty"`
} }
// PhotoSize represents one size of a photo or a file/sticker thumbnail. // PhotoSize represents one size of a photo or a file/sticker thumbnail.
+41 -41
View File
@@ -2,9 +2,9 @@ package tgapi
import "context" import "context"
// SetMyCommandsP holds parameters for the setMyCommands method. // SetMyCommands holds parameters for the setMyCommands method.
// See https://core.telegram.org/bots/api#setmycommands // See https://core.telegram.org/bots/api#setmycommands
type SetMyCommandsP struct { type SetMyCommands struct {
Commands []BotCommand `json:"commands"` Commands []BotCommand `json:"commands"`
Scope *BotCommandScope `json:"scope,omitempty"` Scope *BotCommandScope `json:"scope,omitempty"`
Language string `json:"language_code,omitempty"` Language string `json:"language_code,omitempty"`
@@ -13,7 +13,7 @@ type SetMyCommandsP struct {
// SetMyCommands changes the list of the bot's commands. // SetMyCommands changes the list of the bot's commands.
// Returns true on success. // Returns true on success.
// See https://core.telegram.org/bots/api#setmycommands // See https://core.telegram.org/bots/api#setmycommands
func (api *API) SetMyCommands(params SetMyCommandsP) (bool, error) { func (api *API) SetMyCommands(params SetMyCommands) (bool, error) {
req := NewRequest[bool]("setMyCommands", params) req := NewRequest[bool]("setMyCommands", params)
return req.Do(api) return req.Do(api)
} }
@@ -21,14 +21,14 @@ func (api *API) SetMyCommands(params SetMyCommandsP) (bool, error) {
// SetMyCommandsWithContext is the context-aware variant of SetMyCommands. // SetMyCommandsWithContext is the context-aware variant of SetMyCommands.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#setmycommands // See https://core.telegram.org/bots/api#setmycommands
func (api *API) SetMyCommandsWithContext(ctx context.Context, params SetMyCommandsP) (bool, error) { func (api *API) SetMyCommandsWithContext(ctx context.Context, params SetMyCommands) (bool, error) {
req := NewRequest[bool]("setMyCommands", params) req := NewRequest[bool]("setMyCommands", params)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// DeleteMyCommandsP holds parameters for the deleteMyCommands method. // DeleteMyCommands holds parameters for the deleteMyCommands method.
// See https://core.telegram.org/bots/api#deletemycommands // See https://core.telegram.org/bots/api#deletemycommands
type DeleteMyCommandsP struct { type DeleteMyCommands struct {
Scope *BotCommandScope `json:"scope,omitempty"` Scope *BotCommandScope `json:"scope,omitempty"`
Language string `json:"language_code,omitempty"` Language string `json:"language_code,omitempty"`
} }
@@ -36,7 +36,7 @@ type DeleteMyCommandsP struct {
// DeleteMyCommands deletes the list of the bot's commands for the given scope and user language. // DeleteMyCommands deletes the list of the bot's commands for the given scope and user language.
// Returns true on success. // Returns true on success.
// See https://core.telegram.org/bots/api#deletemycommands // See https://core.telegram.org/bots/api#deletemycommands
func (api *API) DeleteMyCommands(params DeleteMyCommandsP) (bool, error) { func (api *API) DeleteMyCommands(params DeleteMyCommands) (bool, error) {
req := NewRequest[bool]("deleteMyCommands", params) req := NewRequest[bool]("deleteMyCommands", params)
return req.Do(api) return req.Do(api)
} }
@@ -44,7 +44,7 @@ func (api *API) DeleteMyCommands(params DeleteMyCommandsP) (bool, error) {
// DeleteMyCommandsWithContext is the context-aware variant of DeleteMyCommands. // DeleteMyCommandsWithContext is the context-aware variant of DeleteMyCommands.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#deletemycommands // See https://core.telegram.org/bots/api#deletemycommands
func (api *API) DeleteMyCommandsWithContext(ctx context.Context, params DeleteMyCommandsP) (bool, error) { func (api *API) DeleteMyCommandsWithContext(ctx context.Context, params DeleteMyCommands) (bool, error) {
req := NewRequest[bool]("deleteMyCommands", params) req := NewRequest[bool]("deleteMyCommands", params)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
@@ -203,16 +203,16 @@ func (api *API) GetMyShortDescriptionWithContext(ctx context.Context, params Get
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// SetMyProfilePhotoP holds parameters for the setMyProfilePhoto method. // SetMyProfilePhoto holds parameters for the setMyProfilePhoto method.
// See https://core.telegram.org/bots/api#setmyprofilephoto // See https://core.telegram.org/bots/api#setmyprofilephoto
type SetMyProfilePhotoP struct { type SetMyProfilePhoto struct {
Photo InputProfilePhoto `json:"photo"` Photo InputProfilePhoto `json:"photo"`
} }
// SetMyProfilePhoto changes the bot's profile photo. // SetMyProfilePhoto changes the bot's profile photo.
// Returns true on success. // Returns true on success.
// See https://core.telegram.org/bots/api#setmyprofilephoto // See https://core.telegram.org/bots/api#setmyprofilephoto
func (api *API) SetMyProfilePhoto(params SetMyProfilePhotoP) (bool, error) { func (api *API) SetMyProfilePhoto(params SetMyProfilePhoto) (bool, error) {
req := NewRequest[bool]("setMyProfilePhoto", params) req := NewRequest[bool]("setMyProfilePhoto", params)
return req.Do(api) return req.Do(api)
} }
@@ -220,7 +220,7 @@ func (api *API) SetMyProfilePhoto(params SetMyProfilePhotoP) (bool, error) {
// SetMyProfilePhotoWithContext is the context-aware variant of SetMyProfilePhoto. // SetMyProfilePhotoWithContext is the context-aware variant of SetMyProfilePhoto.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#setmyprofilephoto // See https://core.telegram.org/bots/api#setmyprofilephoto
func (api *API) SetMyProfilePhotoWithContext(ctx context.Context, params SetMyProfilePhotoP) (bool, error) { func (api *API) SetMyProfilePhotoWithContext(ctx context.Context, params SetMyProfilePhoto) (bool, error) {
req := NewRequest[bool]("setMyProfilePhoto", params) req := NewRequest[bool]("setMyProfilePhoto", params)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
@@ -241,17 +241,17 @@ func (api *API) RemoveMyProfilePhotoWithContext(ctx context.Context) (bool, erro
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// SetChatMenuButtonP holds parameters for the setChatMenuButton method. // SetChatMenuButton holds parameters for the setChatMenuButton method.
// See https://core.telegram.org/bots/api#setchatmenubutton // See https://core.telegram.org/bots/api#setchatmenubutton
type SetChatMenuButtonP struct { type SetChatMenuButton struct {
ChatID int64 `json:"chat_id"` ChatID int64 `json:"chat_id,omitempty"`
MenuButton MenuButtonType `json:"menu_button"` MenuButton *MenuButton `json:"menu_button,omitempty"`
} }
// SetChatMenuButton changes the menu button for a given chat or the default menu button. // SetChatMenuButton changes the menu button for a given chat or the default menu button.
// Returns true on success. // Returns true on success.
// See https://core.telegram.org/bots/api#setchatmenubutton // See https://core.telegram.org/bots/api#setchatmenubutton
func (api *API) SetChatMenuButton(params SetChatMenuButtonP) (bool, error) { func (api *API) SetChatMenuButton(params SetChatMenuButton) (bool, error) {
req := NewRequest[bool]("setChatMenuButton", params) req := NewRequest[bool]("setChatMenuButton", params)
return req.Do(api) return req.Do(api)
} }
@@ -259,35 +259,35 @@ func (api *API) SetChatMenuButton(params SetChatMenuButtonP) (bool, error) {
// SetChatMenuButtonWithContext is the context-aware variant of SetChatMenuButton. // SetChatMenuButtonWithContext is the context-aware variant of SetChatMenuButton.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#setchatmenubutton // See https://core.telegram.org/bots/api#setchatmenubutton
func (api *API) SetChatMenuButtonWithContext(ctx context.Context, params SetChatMenuButtonP) (bool, error) { func (api *API) SetChatMenuButtonWithContext(ctx context.Context, params SetChatMenuButton) (bool, error) {
req := NewRequest[bool]("setChatMenuButton", params) req := NewRequest[bool]("setChatMenuButton", params)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// GetChatMenuButtonP holds parameters for the getChatMenuButton method. // GetChatMenuButton holds parameters for the getChatMenuButton method.
// See https://core.telegram.org/bots/api#getchatmenubutton // See https://core.telegram.org/bots/api#getchatmenubutton
type GetChatMenuButtonP struct { type GetChatMenuButton struct {
ChatID int64 `json:"chat_id"` ChatID int64 `json:"chat_id,omitempty"`
} }
// GetChatMenuButton returns the current menu button for the given chat. // GetChatMenuButton returns the current menu button for the given chat.
// See https://core.telegram.org/bots/api#getchatmenubutton // See https://core.telegram.org/bots/api#getchatmenubutton
func (api *API) GetChatMenuButton(params GetChatMenuButtonP) (BaseMenuButton, error) { func (api *API) GetChatMenuButton(params GetChatMenuButton) (MenuButton, error) {
req := NewRequest[BaseMenuButton]("getChatMenuButton", params) req := NewRequest[MenuButton]("getChatMenuButton", params)
return req.Do(api) return req.Do(api)
} }
// GetChatMenuButtonWithContext is the context-aware variant of GetChatMenuButton. // GetChatMenuButtonWithContext is the context-aware variant of GetChatMenuButton.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#getchatmenubutton // See https://core.telegram.org/bots/api#getchatmenubutton
func (api *API) GetChatMenuButtonWithContext(ctx context.Context, params GetChatMenuButtonP) (BaseMenuButton, error) { func (api *API) GetChatMenuButtonWithContext(ctx context.Context, params GetChatMenuButton) (MenuButton, error) {
req := NewRequest[BaseMenuButton]("getChatMenuButton", params) req := NewRequest[MenuButton]("getChatMenuButton", params)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// SetMyDefaultAdministratorRightsP holds parameters for the setMyDefaultAdministratorRights method. // SetMyDefaultAdministratorRights holds parameters for the setMyDefaultAdministratorRights method.
// See https://core.telegram.org/bots/api#setmydefaultadministratorrights // See https://core.telegram.org/bots/api#setmydefaultadministratorrights
type SetMyDefaultAdministratorRightsP struct { type SetMyDefaultAdministratorRights struct {
Rights *ChatAdministratorRights `json:"rights"` Rights *ChatAdministratorRights `json:"rights"`
ForChannels bool `json:"for_channels"` ForChannels bool `json:"for_channels"`
} }
@@ -295,7 +295,7 @@ type SetMyDefaultAdministratorRightsP struct {
// SetMyDefaultAdministratorRights changes the default administrator rights for the bot. // SetMyDefaultAdministratorRights changes the default administrator rights for the bot.
// Returns true on success. // Returns true on success.
// See https://core.telegram.org/bots/api#setmydefaultadministratorrights // See https://core.telegram.org/bots/api#setmydefaultadministratorrights
func (api *API) SetMyDefaultAdministratorRights(params SetMyDefaultAdministratorRightsP) (bool, error) { func (api *API) SetMyDefaultAdministratorRights(params SetMyDefaultAdministratorRights) (bool, error) {
req := NewRequest[bool]("setMyDefaultAdministratorRights", params) req := NewRequest[bool]("setMyDefaultAdministratorRights", params)
return req.Do(api) return req.Do(api)
} }
@@ -303,20 +303,20 @@ func (api *API) SetMyDefaultAdministratorRights(params SetMyDefaultAdministrator
// SetMyDefaultAdministratorRightsWithContext is the context-aware variant of SetMyDefaultAdministratorRights. // SetMyDefaultAdministratorRightsWithContext is the context-aware variant of SetMyDefaultAdministratorRights.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#setmydefaultadministratorrights // See https://core.telegram.org/bots/api#setmydefaultadministratorrights
func (api *API) SetMyDefaultAdministratorRightsWithContext(ctx context.Context, params SetMyDefaultAdministratorRightsP) (bool, error) { func (api *API) SetMyDefaultAdministratorRightsWithContext(ctx context.Context, params SetMyDefaultAdministratorRights) (bool, error) {
req := NewRequest[bool]("setMyDefaultAdministratorRights", params) req := NewRequest[bool]("setMyDefaultAdministratorRights", params)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// GetMyDefaultAdministratorRightsP holds parameters for the getMyDefaultAdministratorRights method. // GetMyDefaultAdministratorRights holds parameters for the getMyDefaultAdministratorRights method.
// See https://core.telegram.org/bots/api#getmydefaultadministratorrights // See https://core.telegram.org/bots/api#getmydefaultadministratorrights
type GetMyDefaultAdministratorRightsP struct { type GetMyDefaultAdministratorRights struct {
ForChannels bool `json:"for_channels"` ForChannels bool `json:"for_channels"`
} }
// GetMyDefaultAdministratorRights returns the current default administrator rights for the bot. // GetMyDefaultAdministratorRights returns the current default administrator rights for the bot.
// See https://core.telegram.org/bots/api#getmydefaultadministratorrights // See https://core.telegram.org/bots/api#getmydefaultadministratorrights
func (api *API) GetMyDefaultAdministratorRights(params GetMyDefaultAdministratorRightsP) (ChatAdministratorRights, error) { func (api *API) GetMyDefaultAdministratorRights(params GetMyDefaultAdministratorRights) (ChatAdministratorRights, error) {
req := NewRequest[ChatAdministratorRights]("getMyDefaultAdministratorRights", params) req := NewRequest[ChatAdministratorRights]("getMyDefaultAdministratorRights", params)
return req.Do(api) return req.Do(api)
} }
@@ -324,7 +324,7 @@ func (api *API) GetMyDefaultAdministratorRights(params GetMyDefaultAdministrator
// GetMyDefaultAdministratorRightsWithContext is the context-aware variant of GetMyDefaultAdministratorRights. // GetMyDefaultAdministratorRightsWithContext is the context-aware variant of GetMyDefaultAdministratorRights.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#getmydefaultadministratorrights // See https://core.telegram.org/bots/api#getmydefaultadministratorrights
func (api *API) GetMyDefaultAdministratorRightsWithContext(ctx context.Context, params GetMyDefaultAdministratorRightsP) (ChatAdministratorRights, error) { func (api *API) GetMyDefaultAdministratorRightsWithContext(ctx context.Context, params GetMyDefaultAdministratorRights) (ChatAdministratorRights, error) {
req := NewRequest[ChatAdministratorRights]("getMyDefaultAdministratorRights", params) req := NewRequest[ChatAdministratorRights]("getMyDefaultAdministratorRights", params)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
@@ -344,9 +344,9 @@ func (api *API) GetAvailableGiftsWithContext(ctx context.Context) (Gifts, error)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// SendGiftP holds parameters for the sendGift method. // SendGift holds parameters for the sendGift method.
// See https://core.telegram.org/bots/api#sendgift // See https://core.telegram.org/bots/api#sendgift
type SendGiftP struct { type SendGift struct {
UserID int64 `json:"user_id,omitempty"` UserID int64 `json:"user_id,omitempty"`
ChatID int64 `json:"chat_id,omitempty"` ChatID int64 `json:"chat_id,omitempty"`
GiftID string `json:"gift_id"` GiftID string `json:"gift_id"`
@@ -359,7 +359,7 @@ type SendGiftP struct {
// SendGift sends a gift to the given user or chat. // SendGift sends a gift to the given user or chat.
// Returns true on success. // Returns true on success.
// See https://core.telegram.org/bots/api#sendgift // See https://core.telegram.org/bots/api#sendgift
func (api *API) SendGift(params SendGiftP) (bool, error) { func (api *API) SendGift(params SendGift) (bool, error) {
req := NewRequest[bool]("sendGift", params) req := NewRequest[bool]("sendGift", params)
return req.Do(api) return req.Do(api)
} }
@@ -367,14 +367,14 @@ func (api *API) SendGift(params SendGiftP) (bool, error) {
// SendGiftWithContext is the context-aware variant of SendGift. // SendGiftWithContext is the context-aware variant of SendGift.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#sendgift // See https://core.telegram.org/bots/api#sendgift
func (api *API) SendGiftWithContext(ctx context.Context, params SendGiftP) (bool, error) { func (api *API) SendGiftWithContext(ctx context.Context, params SendGift) (bool, error) {
req := NewRequest[bool]("sendGift", params) req := NewRequest[bool]("sendGift", params)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// GiftPremiumSubscriptionP holds parameters for the giftPremiumSubscription method. // GiftPremiumSubscription holds parameters for the giftPremiumSubscription method.
// See https://core.telegram.org/bots/api#giftpremiumsubscription // See https://core.telegram.org/bots/api#giftpremiumsubscription
type GiftPremiumSubscriptionP struct { type GiftPremiumSubscription struct {
UserID int64 `json:"user_id"` UserID int64 `json:"user_id"`
MonthCount int `json:"month_count"` MonthCount int `json:"month_count"`
StarCount int `json:"star_count"` StarCount int `json:"star_count"`
@@ -386,7 +386,7 @@ type GiftPremiumSubscriptionP struct {
// GiftPremiumSubscription gifts a Telegram Premium subscription to the user. // GiftPremiumSubscription gifts a Telegram Premium subscription to the user.
// Returns true on success. // Returns true on success.
// See https://core.telegram.org/bots/api#giftpremiumsubscription // See https://core.telegram.org/bots/api#giftpremiumsubscription
func (api *API) GiftPremiumSubscription(params GiftPremiumSubscriptionP) (bool, error) { func (api *API) GiftPremiumSubscription(params GiftPremiumSubscription) (bool, error) {
req := NewRequest[bool]("giftPremiumSubscription", params) req := NewRequest[bool]("giftPremiumSubscription", params)
return req.Do(api) return req.Do(api)
} }
@@ -394,7 +394,7 @@ func (api *API) GiftPremiumSubscription(params GiftPremiumSubscriptionP) (bool,
// GiftPremiumSubscriptionWithContext is the context-aware variant of GiftPremiumSubscription. // GiftPremiumSubscriptionWithContext is the context-aware variant of GiftPremiumSubscription.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#giftpremiumsubscription // See https://core.telegram.org/bots/api#giftpremiumsubscription
func (api *API) GiftPremiumSubscriptionWithContext(ctx context.Context, params GiftPremiumSubscriptionP) (bool, error) { func (api *API) GiftPremiumSubscriptionWithContext(ctx context.Context, params GiftPremiumSubscription) (bool, error) {
req := NewRequest[bool]("giftPremiumSubscription", params) req := NewRequest[bool]("giftPremiumSubscription", params)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
+12 -7
View File
@@ -54,7 +54,9 @@ type BotShortDescription struct {
type InputProfilePhotoType string type InputProfilePhotoType string
const ( const (
InputProfilePhotoStaticType InputProfilePhotoType = "static" // InputProfilePhotoStaticType identifies a static profile photo input.
InputProfilePhotoStaticType InputProfilePhotoType = "static"
// InputProfilePhotoAnimatedType identifies an animated profile photo input.
InputProfilePhotoAnimatedType InputProfilePhotoType = "animated" InputProfilePhotoAnimatedType InputProfilePhotoType = "animated"
) )
@@ -75,17 +77,20 @@ type InputProfilePhoto struct {
type MenuButtonType string type MenuButtonType string
const ( const (
// MenuButtonCommandsType identifies a commands menu button.
MenuButtonCommandsType MenuButtonType = "commands" MenuButtonCommandsType MenuButtonType = "commands"
MenuButtonWebAppType MenuButtonType = "web_app" // MenuButtonWebAppType identifies a web app menu button.
MenuButtonDefaultType MenuButtonType = "default" MenuButtonWebAppType MenuButtonType = "web_app"
// MenuButtonDefaultType identifies Telegram's default menu button.
MenuButtonDefaultType MenuButtonType = "default"
) )
// BaseMenuButton represents a menu button. // MenuButton represents a menu button.
// See https://core.telegram.org/bots/api#menubutton // See https://core.telegram.org/bots/api#menubutton
type BaseMenuButton struct { type MenuButton struct {
Type MenuButtonType `json:"type"` Type MenuButtonType `json:"type"`
// WebApp fields (for web_app button) // WebApp fields (for web_app button)
Text string `json:"text"` Text *string `json:"text"`
WebApp WebAppInfo `json:"web_app"` WebApp *WebAppInfo `json:"web_app"`
} }
+90 -105
View File
@@ -2,9 +2,9 @@ package tgapi
import "context" import "context"
// VerifyUserP holds parameters for the verifyUser method. // VerifyUser holds parameters for the verifyUser method.
// See https://core.telegram.org/bots/api#verifyuser // See https://core.telegram.org/bots/api#verifyuser
type VerifyUserP struct { type VerifyUser struct {
UserID int64 `json:"user_id"` UserID int64 `json:"user_id"`
CustomDescription string `json:"custom_description,omitempty"` CustomDescription string `json:"custom_description,omitempty"`
} }
@@ -12,7 +12,7 @@ type VerifyUserP struct {
// VerifyUser verifies a user. // VerifyUser verifies a user.
// Returns true on success. // Returns true on success.
// See https://core.telegram.org/bots/api#verifyuser // See https://core.telegram.org/bots/api#verifyuser
func (api *API) VerifyUser(params VerifyUserP) (bool, error) { func (api *API) VerifyUser(params VerifyUser) (bool, error) {
req := NewRequest[bool]("verifyUser", params) req := NewRequest[bool]("verifyUser", params)
return req.Do(api) return req.Do(api)
} }
@@ -20,14 +20,14 @@ func (api *API) VerifyUser(params VerifyUserP) (bool, error) {
// VerifyUserWithContext is the context-aware variant of VerifyUser. // VerifyUserWithContext is the context-aware variant of VerifyUser.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#verifyuser // See https://core.telegram.org/bots/api#verifyuser
func (api *API) VerifyUserWithContext(ctx context.Context, params VerifyUserP) (bool, error) { func (api *API) VerifyUserWithContext(ctx context.Context, params VerifyUser) (bool, error) {
req := NewRequest[bool]("verifyUser", params) req := NewRequest[bool]("verifyUser", params)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// VerifyChatP holds parameters for the verifyChat method. // VerifyChat holds parameters for the verifyChat method.
// See https://core.telegram.org/bots/api#verifychat // See https://core.telegram.org/bots/api#verifychat
type VerifyChatP struct { type VerifyChat struct {
ChatID int64 `json:"chat_id"` ChatID int64 `json:"chat_id"`
CustomDescription string `json:"custom_description,omitempty"` CustomDescription string `json:"custom_description,omitempty"`
} }
@@ -35,7 +35,7 @@ type VerifyChatP struct {
// VerifyChat verifies a chat. // VerifyChat verifies a chat.
// Returns true on success. // Returns true on success.
// See https://core.telegram.org/bots/api#verifychat // See https://core.telegram.org/bots/api#verifychat
func (api *API) VerifyChat(params VerifyChatP) (bool, error) { func (api *API) VerifyChat(params VerifyChat) (bool, error) {
req := NewRequest[bool]("verifyChat", params) req := NewRequest[bool]("verifyChat", params)
return req.Do(api) return req.Do(api)
} }
@@ -43,21 +43,21 @@ func (api *API) VerifyChat(params VerifyChatP) (bool, error) {
// VerifyChatWithContext is the context-aware variant of VerifyChat. // VerifyChatWithContext is the context-aware variant of VerifyChat.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#verifychat // See https://core.telegram.org/bots/api#verifychat
func (api *API) VerifyChatWithContext(ctx context.Context, params VerifyChatP) (bool, error) { func (api *API) VerifyChatWithContext(ctx context.Context, params VerifyChat) (bool, error) {
req := NewRequest[bool]("verifyChat", params) req := NewRequest[bool]("verifyChat", params)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// RemoveUserVerificationP holds parameters for the removeUserVerification method. // RemoveUserVerification holds parameters for the removeUserVerification method.
// See https://core.telegram.org/bots/api#removeuserverification // See https://core.telegram.org/bots/api#removeuserverification
type RemoveUserVerificationP struct { type RemoveUserVerification struct {
UserID int64 `json:"user_id"` UserID int64 `json:"user_id"`
} }
// RemoveUserVerification removes a user's verification. // RemoveUserVerification removes a user's verification.
// Returns true on success. // Returns true on success.
// See https://core.telegram.org/bots/api#removeuserverification // See https://core.telegram.org/bots/api#removeuserverification
func (api *API) RemoveUserVerification(params RemoveUserVerificationP) (bool, error) { func (api *API) RemoveUserVerification(params RemoveUserVerification) (bool, error) {
req := NewRequest[bool]("removeUserVerification", params) req := NewRequest[bool]("removeUserVerification", params)
return req.Do(api) return req.Do(api)
} }
@@ -65,21 +65,21 @@ func (api *API) RemoveUserVerification(params RemoveUserVerificationP) (bool, er
// RemoveUserVerificationWithContext is the context-aware variant of RemoveUserVerification. // RemoveUserVerificationWithContext is the context-aware variant of RemoveUserVerification.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#removeuserverification // See https://core.telegram.org/bots/api#removeuserverification
func (api *API) RemoveUserVerificationWithContext(ctx context.Context, params RemoveUserVerificationP) (bool, error) { func (api *API) RemoveUserVerificationWithContext(ctx context.Context, params RemoveUserVerification) (bool, error) {
req := NewRequest[bool]("removeUserVerification", params) req := NewRequest[bool]("removeUserVerification", params)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// RemoveChatVerificationP holds parameters for the removeChatVerification method. // RemoveChatVerification holds parameters for the removeChatVerification method.
// See https://core.telegram.org/bots/api#removechatverification // See https://core.telegram.org/bots/api#removechatverification
type RemoveChatVerificationP struct { type RemoveChatVerification struct {
ChatID int64 `json:"chat_id"` ChatID int64 `json:"chat_id"`
} }
// RemoveChatVerification removes a chat's verification. // RemoveChatVerification removes a chat's verification.
// Returns true on success. // Returns true on success.
// See https://core.telegram.org/bots/api#removechatverification // See https://core.telegram.org/bots/api#removechatverification
func (api *API) RemoveChatVerification(params RemoveChatVerificationP) (bool, error) { func (api *API) RemoveChatVerification(params RemoveChatVerification) (bool, error) {
req := NewRequest[bool]("removeChatVerification", params) req := NewRequest[bool]("removeChatVerification", params)
return req.Do(api) return req.Do(api)
} }
@@ -87,14 +87,14 @@ func (api *API) RemoveChatVerification(params RemoveChatVerificationP) (bool, er
// RemoveChatVerificationWithContext is the context-aware variant of RemoveChatVerification. // RemoveChatVerificationWithContext is the context-aware variant of RemoveChatVerification.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#removechatverification // See https://core.telegram.org/bots/api#removechatverification
func (api *API) RemoveChatVerificationWithContext(ctx context.Context, params RemoveChatVerificationP) (bool, error) { func (api *API) RemoveChatVerificationWithContext(ctx context.Context, params RemoveChatVerification) (bool, error) {
req := NewRequest[bool]("removeChatVerification", params) req := NewRequest[bool]("removeChatVerification", params)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// ReadBusinessMessageP holds parameters for the readBusinessMessage method. // ReadBusinessMessage holds parameters for the readBusinessMessage method.
// See https://core.telegram.org/bots/api#readbusinessmessage // See https://core.telegram.org/bots/api#readbusinessmessage
type ReadBusinessMessageP struct { type ReadBusinessMessage struct {
BusinessConnectionID string `json:"business_connection_id"` BusinessConnectionID string `json:"business_connection_id"`
ChatID int64 `json:"chat_id"` ChatID int64 `json:"chat_id"`
MessageID int `json:"message_id"` MessageID int `json:"message_id"`
@@ -103,7 +103,7 @@ type ReadBusinessMessageP struct {
// ReadBusinessMessage marks a business message as read. // ReadBusinessMessage marks a business message as read.
// Returns true on success. // Returns true on success.
// See https://core.telegram.org/bots/api#readbusinessmessage // See https://core.telegram.org/bots/api#readbusinessmessage
func (api *API) ReadBusinessMessage(params ReadBusinessMessageP) (bool, error) { func (api *API) ReadBusinessMessage(params ReadBusinessMessage) (bool, error) {
req := NewRequest[bool]("readBusinessMessage", params) req := NewRequest[bool]("readBusinessMessage", params)
return req.Do(api) return req.Do(api)
} }
@@ -111,20 +111,20 @@ func (api *API) ReadBusinessMessage(params ReadBusinessMessageP) (bool, error) {
// ReadBusinessMessageWithContext is the context-aware variant of ReadBusinessMessage. // ReadBusinessMessageWithContext is the context-aware variant of ReadBusinessMessage.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#readbusinessmessage // See https://core.telegram.org/bots/api#readbusinessmessage
func (api *API) ReadBusinessMessageWithContext(ctx context.Context, params ReadBusinessMessageP) (bool, error) { func (api *API) ReadBusinessMessageWithContext(ctx context.Context, params ReadBusinessMessage) (bool, error) {
req := NewRequest[bool]("readBusinessMessage", params) req := NewRequest[bool]("readBusinessMessage", params)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// GetBusinessConnectionP holds parameters for the getBusinessConnection method. // GetBusinessConnection holds parameters for the getBusinessConnection method.
// See https://core.telegram.org/bots/api#getbusinessconnection // See https://core.telegram.org/bots/api#getbusinessconnection
type GetBusinessConnectionP struct { type GetBusinessConnection struct {
BusinessConnectionID string `json:"business_connection_id"` BusinessConnectionID string `json:"business_connection_id"`
} }
// GetBusinessConnection returns information about a business connection. // GetBusinessConnection returns information about a business connection.
// See https://core.telegram.org/bots/api#getbusinessconnection // See https://core.telegram.org/bots/api#getbusinessconnection
func (api *API) GetBusinessConnection(params GetBusinessConnectionP) (BusinessConnection, error) { func (api *API) GetBusinessConnection(params GetBusinessConnection) (BusinessConnection, error) {
req := NewRequest[BusinessConnection]("getBusinessConnection", params) req := NewRequest[BusinessConnection]("getBusinessConnection", params)
return req.Do(api) return req.Do(api)
} }
@@ -132,14 +132,14 @@ func (api *API) GetBusinessConnection(params GetBusinessConnectionP) (BusinessCo
// GetBusinessConnectionWithContext is the context-aware variant of GetBusinessConnection. // GetBusinessConnectionWithContext is the context-aware variant of GetBusinessConnection.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#getbusinessconnection // See https://core.telegram.org/bots/api#getbusinessconnection
func (api *API) GetBusinessConnectionWithContext(ctx context.Context, params GetBusinessConnectionP) (BusinessConnection, error) { func (api *API) GetBusinessConnectionWithContext(ctx context.Context, params GetBusinessConnection) (BusinessConnection, error) {
req := NewRequest[BusinessConnection]("getBusinessConnection", params) req := NewRequest[BusinessConnection]("getBusinessConnection", params)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// DeleteBusinessMessagesP holds parameters for the deleteBusinessMessages method. // DeleteBusinessMessages holds parameters for the deleteBusinessMessages method.
// See https://core.telegram.org/bots/api#deletebusinessmessages // See https://core.telegram.org/bots/api#deletebusinessmessages
type DeleteBusinessMessagesP struct { type DeleteBusinessMessages struct {
BusinessConnectionID string `json:"business_connection_id"` BusinessConnectionID string `json:"business_connection_id"`
MessageIDs []int `json:"message_ids"` MessageIDs []int `json:"message_ids"`
} }
@@ -147,7 +147,7 @@ type DeleteBusinessMessagesP struct {
// DeleteBusinessMessages deletes business messages. // DeleteBusinessMessages deletes business messages.
// Returns true on success. // Returns true on success.
// See https://core.telegram.org/bots/api#deletebusinessmessages // See https://core.telegram.org/bots/api#deletebusinessmessages
func (api *API) DeleteBusinessMessages(params DeleteBusinessMessagesP) (bool, error) { func (api *API) DeleteBusinessMessages(params DeleteBusinessMessages) (bool, error) {
req := NewRequest[bool]("deleteBusinessMessages", params) req := NewRequest[bool]("deleteBusinessMessages", params)
return req.Do(api) return req.Do(api)
} }
@@ -155,14 +155,14 @@ func (api *API) DeleteBusinessMessages(params DeleteBusinessMessagesP) (bool, er
// DeleteBusinessMessagesWithContext is the context-aware variant of DeleteBusinessMessages. // DeleteBusinessMessagesWithContext is the context-aware variant of DeleteBusinessMessages.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#deletebusinessmessages // See https://core.telegram.org/bots/api#deletebusinessmessages
func (api *API) DeleteBusinessMessagesWithContext(ctx context.Context, params DeleteBusinessMessagesP) (bool, error) { func (api *API) DeleteBusinessMessagesWithContext(ctx context.Context, params DeleteBusinessMessages) (bool, error) {
req := NewRequest[bool]("deleteBusinessMessages", params) req := NewRequest[bool]("deleteBusinessMessages", params)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// SetBusinessAccountNameP holds parameters for the setBusinessAccountName method. // SetBusinessAccountName holds parameters for the setBusinessAccountName method.
// See https://core.telegram.org/bots/api#setbusinessaccountname // See https://core.telegram.org/bots/api#setbusinessaccountname
type SetBusinessAccountNameP struct { type SetBusinessAccountName struct {
BusinessConnectionID string `json:"business_connection_id"` BusinessConnectionID string `json:"business_connection_id"`
FirstName string `json:"first_name"` FirstName string `json:"first_name"`
LastName string `json:"last_name,omitempty"` LastName string `json:"last_name,omitempty"`
@@ -171,7 +171,7 @@ type SetBusinessAccountNameP struct {
// SetBusinessAccountName sets the first and last name of a business account. // SetBusinessAccountName sets the first and last name of a business account.
// Returns true on success. // Returns true on success.
// See https://core.telegram.org/bots/api#setbusinessaccountname // See https://core.telegram.org/bots/api#setbusinessaccountname
func (api *API) SetBusinessAccountName(params SetBusinessAccountNameP) (bool, error) { func (api *API) SetBusinessAccountName(params SetBusinessAccountName) (bool, error) {
req := NewRequest[bool]("setBusinessAccountName", params) req := NewRequest[bool]("setBusinessAccountName", params)
return req.Do(api) return req.Do(api)
} }
@@ -179,14 +179,14 @@ func (api *API) SetBusinessAccountName(params SetBusinessAccountNameP) (bool, er
// SetBusinessAccountNameWithContext is the context-aware variant of SetBusinessAccountName. // SetBusinessAccountNameWithContext is the context-aware variant of SetBusinessAccountName.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#setbusinessaccountname // See https://core.telegram.org/bots/api#setbusinessaccountname
func (api *API) SetBusinessAccountNameWithContext(ctx context.Context, params SetBusinessAccountNameP) (bool, error) { func (api *API) SetBusinessAccountNameWithContext(ctx context.Context, params SetBusinessAccountName) (bool, error) {
req := NewRequest[bool]("setBusinessAccountName", params) req := NewRequest[bool]("setBusinessAccountName", params)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// SetBusinessAccountUsernameP holds parameters for the setBusinessAccountUsername method. // SetBusinessAccountUsername holds parameters for the setBusinessAccountUsername method.
// See https://core.telegram.org/bots/api#setbusinessaccountusername // See https://core.telegram.org/bots/api#setbusinessaccountusername
type SetBusinessAccountUsernameP struct { type SetBusinessAccountUsername struct {
BusinessConnectionID string `json:"business_connection_id"` BusinessConnectionID string `json:"business_connection_id"`
Username string `json:"username,omitempty"` Username string `json:"username,omitempty"`
} }
@@ -194,7 +194,7 @@ type SetBusinessAccountUsernameP struct {
// SetBusinessAccountUsername sets the username of a business account. // SetBusinessAccountUsername sets the username of a business account.
// Returns true on success. // Returns true on success.
// See https://core.telegram.org/bots/api#setbusinessaccountusername // See https://core.telegram.org/bots/api#setbusinessaccountusername
func (api *API) SetBusinessAccountUsername(params SetBusinessAccountUsernameP) (bool, error) { func (api *API) SetBusinessAccountUsername(params SetBusinessAccountUsername) (bool, error) {
req := NewRequest[bool]("setBusinessAccountUsername", params) req := NewRequest[bool]("setBusinessAccountUsername", params)
return req.Do(api) return req.Do(api)
} }
@@ -202,14 +202,14 @@ func (api *API) SetBusinessAccountUsername(params SetBusinessAccountUsernameP) (
// SetBusinessAccountUsernameWithContext is the context-aware variant of SetBusinessAccountUsername. // SetBusinessAccountUsernameWithContext is the context-aware variant of SetBusinessAccountUsername.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#setbusinessaccountusername // See https://core.telegram.org/bots/api#setbusinessaccountusername
func (api *API) SetBusinessAccountUsernameWithContext(ctx context.Context, params SetBusinessAccountUsernameP) (bool, error) { func (api *API) SetBusinessAccountUsernameWithContext(ctx context.Context, params SetBusinessAccountUsername) (bool, error) {
req := NewRequest[bool]("setBusinessAccountUsername", params) req := NewRequest[bool]("setBusinessAccountUsername", params)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// SetBusinessAccountBioP holds parameters for the setBusinessAccountBio method. // SetBusinessAccountBio holds parameters for the setBusinessAccountBio method.
// See https://core.telegram.org/bots/api#setbusinessaccountbio // See https://core.telegram.org/bots/api#setbusinessaccountbio
type SetBusinessAccountBioP struct { type SetBusinessAccountBio struct {
BusinessConnectionID string `json:"business_connection_id"` BusinessConnectionID string `json:"business_connection_id"`
Bio string `json:"bio,omitempty"` Bio string `json:"bio,omitempty"`
} }
@@ -217,7 +217,7 @@ type SetBusinessAccountBioP struct {
// SetBusinessAccountBio sets the bio of a business account. // SetBusinessAccountBio sets the bio of a business account.
// Returns true on success. // Returns true on success.
// See https://core.telegram.org/bots/api#setbusinessaccountbio // See https://core.telegram.org/bots/api#setbusinessaccountbio
func (api *API) SetBusinessAccountBio(params SetBusinessAccountBioP) (bool, error) { func (api *API) SetBusinessAccountBio(params SetBusinessAccountBio) (bool, error) {
req := NewRequest[bool]("setBusinessAccountBio", params) req := NewRequest[bool]("setBusinessAccountBio", params)
return req.Do(api) return req.Do(api)
} }
@@ -225,7 +225,7 @@ func (api *API) SetBusinessAccountBio(params SetBusinessAccountBioP) (bool, erro
// SetBusinessAccountBioWithContext is the context-aware variant of SetBusinessAccountBio. // SetBusinessAccountBioWithContext is the context-aware variant of SetBusinessAccountBio.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#setbusinessaccountbio // See https://core.telegram.org/bots/api#setbusinessaccountbio
func (api *API) SetBusinessAccountBioWithContext(ctx context.Context, params SetBusinessAccountBioP) (bool, error) { func (api *API) SetBusinessAccountBioWithContext(ctx context.Context, params SetBusinessAccountBio) (bool, error) {
req := NewRequest[bool]("setBusinessAccountBio", params) req := NewRequest[bool]("setBusinessAccountBio", params)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
@@ -254,9 +254,9 @@ func (api *API) SetBusinessAccountProfilePhotoWithContext(ctx context.Context, p
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// RemoveBusinessAccountProfilePhotoP holds parameters for the removeBusinessAccountProfilePhoto method. // RemoveBusinessAccountProfilePhoto holds parameters for the removeBusinessAccountProfilePhoto method.
// See https://core.telegram.org/bots/api#removebusinessaccountprofilephoto // See https://core.telegram.org/bots/api#removebusinessaccountprofilephoto
type RemoveBusinessAccountProfilePhotoP struct { type RemoveBusinessAccountProfilePhoto struct {
BusinessConnectionID string `json:"business_connection_id"` BusinessConnectionID string `json:"business_connection_id"`
IsPublic bool `json:"is_public,omitempty"` IsPublic bool `json:"is_public,omitempty"`
} }
@@ -264,7 +264,7 @@ type RemoveBusinessAccountProfilePhotoP struct {
// RemoveBusinessAccountProfilePhoto removes the profile photo of a business account. // RemoveBusinessAccountProfilePhoto removes the profile photo of a business account.
// Returns true on success. // Returns true on success.
// See https://core.telegram.org/bots/api#removebusinessaccountprofilephoto // See https://core.telegram.org/bots/api#removebusinessaccountprofilephoto
func (api *API) RemoveBusinessAccountProfilePhoto(params RemoveBusinessAccountProfilePhotoP) (bool, error) { func (api *API) RemoveBusinessAccountProfilePhoto(params RemoveBusinessAccountProfilePhoto) (bool, error) {
req := NewRequest[bool]("removeBusinessAccountProfilePhoto", params) req := NewRequest[bool]("removeBusinessAccountProfilePhoto", params)
return req.Do(api) return req.Do(api)
} }
@@ -272,14 +272,14 @@ func (api *API) RemoveBusinessAccountProfilePhoto(params RemoveBusinessAccountPr
// RemoveBusinessAccountProfilePhotoWithContext is the context-aware variant of RemoveBusinessAccountProfilePhoto. // RemoveBusinessAccountProfilePhotoWithContext is the context-aware variant of RemoveBusinessAccountProfilePhoto.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#removebusinessaccountprofilephoto // See https://core.telegram.org/bots/api#removebusinessaccountprofilephoto
func (api *API) RemoveBusinessAccountProfilePhotoWithContext(ctx context.Context, params RemoveBusinessAccountProfilePhotoP) (bool, error) { func (api *API) RemoveBusinessAccountProfilePhotoWithContext(ctx context.Context, params RemoveBusinessAccountProfilePhoto) (bool, error) {
req := NewRequest[bool]("removeBusinessAccountProfilePhoto", params) req := NewRequest[bool]("removeBusinessAccountProfilePhoto", params)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// SetBusinessAccountGiftSettingsP holds parameters for the setBusinessAccountGiftSettings method. // SetBusinessAccountGiftSettings holds parameters for the setBusinessAccountGiftSettings method.
// See https://core.telegram.org/bots/api#setbusinessaccountgiftsettings // See https://core.telegram.org/bots/api#setbusinessaccountgiftsettings
type SetBusinessAccountGiftSettingsP struct { type SetBusinessAccountGiftSettings struct {
BusinessConnectionID string `json:"business_connection_id"` BusinessConnectionID string `json:"business_connection_id"`
ShowGiftButton bool `json:"show_gift_button"` ShowGiftButton bool `json:"show_gift_button"`
AcceptedGiftTypes AcceptedGiftTypes `json:"accepted_gift_types"` AcceptedGiftTypes AcceptedGiftTypes `json:"accepted_gift_types"`
@@ -288,7 +288,7 @@ type SetBusinessAccountGiftSettingsP struct {
// SetBusinessAccountGiftSettings sets gift settings for a business account. // SetBusinessAccountGiftSettings sets gift settings for a business account.
// Returns true on success. // Returns true on success.
// See https://core.telegram.org/bots/api#setbusinessaccountgiftsettings // See https://core.telegram.org/bots/api#setbusinessaccountgiftsettings
func (api *API) SetBusinessAccountGiftSettings(params SetBusinessAccountGiftSettingsP) (bool, error) { func (api *API) SetBusinessAccountGiftSettings(params SetBusinessAccountGiftSettings) (bool, error) {
req := NewRequest[bool]("setBusinessAccountGiftSettings", params) req := NewRequest[bool]("setBusinessAccountGiftSettings", params)
return req.Do(api) return req.Do(api)
} }
@@ -296,20 +296,20 @@ func (api *API) SetBusinessAccountGiftSettings(params SetBusinessAccountGiftSett
// SetBusinessAccountGiftSettingsWithContext is the context-aware variant of SetBusinessAccountGiftSettings. // SetBusinessAccountGiftSettingsWithContext is the context-aware variant of SetBusinessAccountGiftSettings.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#setbusinessaccountgiftsettings // See https://core.telegram.org/bots/api#setbusinessaccountgiftsettings
func (api *API) SetBusinessAccountGiftSettingsWithContext(ctx context.Context, params SetBusinessAccountGiftSettingsP) (bool, error) { func (api *API) SetBusinessAccountGiftSettingsWithContext(ctx context.Context, params SetBusinessAccountGiftSettings) (bool, error) {
req := NewRequest[bool]("setBusinessAccountGiftSettings", params) req := NewRequest[bool]("setBusinessAccountGiftSettings", params)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// GetBusinessAccountStarBalanceP holds parameters for the getBusinessAccountStarBalance method. // GetBusinessAccountStarBalance holds parameters for the getBusinessAccountStarBalance method.
// See https://core.telegram.org/bots/api#getbusinessaccountstarbalance // See https://core.telegram.org/bots/api#getbusinessaccountstarbalance
type GetBusinessAccountStarBalanceP struct { type GetBusinessAccountStarBalance struct {
BusinessConnectionID string `json:"business_connection_id"` BusinessConnectionID string `json:"business_connection_id"`
} }
// GetBusinessAccountStarBalance returns the star balance of a business account. // GetBusinessAccountStarBalance returns the star balance of a business account.
// See https://core.telegram.org/bots/api#getbusinessaccountstarbalance // See https://core.telegram.org/bots/api#getbusinessaccountstarbalance
func (api *API) GetBusinessAccountStarBalance(params GetBusinessAccountStarBalanceP) (StarAmount, error) { func (api *API) GetBusinessAccountStarBalance(params GetBusinessAccountStarBalance) (StarAmount, error) {
req := NewRequest[StarAmount]("getBusinessAccountStarBalance", params) req := NewRequest[StarAmount]("getBusinessAccountStarBalance", params)
return req.Do(api) return req.Do(api)
} }
@@ -317,14 +317,14 @@ func (api *API) GetBusinessAccountStarBalance(params GetBusinessAccountStarBalan
// GetBusinessAccountStarBalanceWithContext is the context-aware variant of GetBusinessAccountStarBalance. // GetBusinessAccountStarBalanceWithContext is the context-aware variant of GetBusinessAccountStarBalance.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#getbusinessaccountstarbalance // See https://core.telegram.org/bots/api#getbusinessaccountstarbalance
func (api *API) GetBusinessAccountStarBalanceWithContext(ctx context.Context, params GetBusinessAccountStarBalanceP) (StarAmount, error) { func (api *API) GetBusinessAccountStarBalanceWithContext(ctx context.Context, params GetBusinessAccountStarBalance) (StarAmount, error) {
req := NewRequest[StarAmount]("getBusinessAccountStarBalance", params) req := NewRequest[StarAmount]("getBusinessAccountStarBalance", params)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// TransferBusinessAccountStarsP holds parameters for the transferBusinessAccountStars method. // TransferBusinessAccountStars holds parameters for the transferBusinessAccountStars method.
// See https://core.telegram.org/bots/api#transferbusinessaccountstars // See https://core.telegram.org/bots/api#transferbusinessaccountstars
type TransferBusinessAccountStarsP struct { type TransferBusinessAccountStars struct {
BusinessConnectionID string `json:"business_connection_id"` BusinessConnectionID string `json:"business_connection_id"`
StarCount int `json:"star_count"` StarCount int `json:"star_count"`
} }
@@ -332,7 +332,7 @@ type TransferBusinessAccountStarsP struct {
// TransferBusinessAccountStars transfers stars from a business account. // TransferBusinessAccountStars transfers stars from a business account.
// Returns true on success. // Returns true on success.
// See https://core.telegram.org/bots/api#transferbusinessaccountstars // See https://core.telegram.org/bots/api#transferbusinessaccountstars
func (api *API) TransferBusinessAccountStars(params TransferBusinessAccountStarsP) (bool, error) { func (api *API) TransferBusinessAccountStars(params TransferBusinessAccountStars) (bool, error) {
req := NewRequest[bool]("transferBusinessAccountStars", params) req := NewRequest[bool]("transferBusinessAccountStars", params)
return req.Do(api) return req.Do(api)
} }
@@ -340,14 +340,14 @@ func (api *API) TransferBusinessAccountStars(params TransferBusinessAccountStars
// TransferBusinessAccountStarsWithContext is the context-aware variant of TransferBusinessAccountStars. // TransferBusinessAccountStarsWithContext is the context-aware variant of TransferBusinessAccountStars.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#transferbusinessaccountstars // See https://core.telegram.org/bots/api#transferbusinessaccountstars
func (api *API) TransferBusinessAccountStarsWithContext(ctx context.Context, params TransferBusinessAccountStarsP) (bool, error) { func (api *API) TransferBusinessAccountStarsWithContext(ctx context.Context, params TransferBusinessAccountStars) (bool, error) {
req := NewRequest[bool]("transferBusinessAccountStars", params) req := NewRequest[bool]("transferBusinessAccountStars", params)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// GetBusinessAccountGiftsP holds parameters for the getBusinessAccountGifts method. // GetBusinessAccountGifts holds parameters for the getBusinessAccountGifts method.
// See https://core.telegram.org/bots/api#getbusinessaccountgifts // See https://core.telegram.org/bots/api#getbusinessaccountgifts
type GetBusinessAccountGiftsP struct { type GetBusinessAccountGifts struct {
BusinessConnectionID string `json:"business_connection_id"` BusinessConnectionID string `json:"business_connection_id"`
ExcludeUnsaved bool `json:"exclude_unsaved,omitempty"` ExcludeUnsaved bool `json:"exclude_unsaved,omitempty"`
ExcludeSaved bool `json:"exclude_saved,omitempty"` ExcludeSaved bool `json:"exclude_saved,omitempty"`
@@ -363,7 +363,7 @@ type GetBusinessAccountGiftsP struct {
// GetBusinessAccountGifts returns gifts owned by a business account. // GetBusinessAccountGifts returns gifts owned by a business account.
// See https://core.telegram.org/bots/api#getbusinessaccountgifts // See https://core.telegram.org/bots/api#getbusinessaccountgifts
func (api *API) GetBusinessAccountGifts(params GetBusinessAccountGiftsP) (OwnedGifts, error) { func (api *API) GetBusinessAccountGifts(params GetBusinessAccountGifts) (OwnedGifts, error) {
req := NewRequest[OwnedGifts]("getBusinessAccountGifts", params) req := NewRequest[OwnedGifts]("getBusinessAccountGifts", params)
return req.Do(api) return req.Do(api)
} }
@@ -371,14 +371,14 @@ func (api *API) GetBusinessAccountGifts(params GetBusinessAccountGiftsP) (OwnedG
// GetBusinessAccountGiftsWithContext is the context-aware variant of GetBusinessAccountGifts. // GetBusinessAccountGiftsWithContext is the context-aware variant of GetBusinessAccountGifts.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#getbusinessaccountgifts // See https://core.telegram.org/bots/api#getbusinessaccountgifts
func (api *API) GetBusinessAccountGiftsWithContext(ctx context.Context, params GetBusinessAccountGiftsP) (OwnedGifts, error) { func (api *API) GetBusinessAccountGiftsWithContext(ctx context.Context, params GetBusinessAccountGifts) (OwnedGifts, error) {
req := NewRequest[OwnedGifts]("getBusinessAccountGifts", params) req := NewRequest[OwnedGifts]("getBusinessAccountGifts", params)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// ConvertGiftToStarsP holds parameters for the convertGiftToStars method. // ConvertGiftToStars holds parameters for the convertGiftToStars method.
// See https://core.telegram.org/bots/api#convertgifttostars // See https://core.telegram.org/bots/api#convertgifttostars
type ConvertGiftToStarsP struct { type ConvertGiftToStars struct {
BusinessConnectionID string `json:"business_connection_id"` BusinessConnectionID string `json:"business_connection_id"`
OwnedGiftID string `json:"owned_gift_id"` OwnedGiftID string `json:"owned_gift_id"`
} }
@@ -386,7 +386,7 @@ type ConvertGiftToStarsP struct {
// ConvertGiftToStars converts a gift to Telegram Stars. // ConvertGiftToStars converts a gift to Telegram Stars.
// Returns true on success. // Returns true on success.
// See https://core.telegram.org/bots/api#convertgifttostars // See https://core.telegram.org/bots/api#convertgifttostars
func (api *API) ConvertGiftToStars(params ConvertGiftToStarsP) (bool, error) { func (api *API) ConvertGiftToStars(params ConvertGiftToStars) (bool, error) {
req := NewRequest[bool]("convertGiftToStars", params) req := NewRequest[bool]("convertGiftToStars", params)
return req.Do(api) return req.Do(api)
} }
@@ -394,14 +394,14 @@ func (api *API) ConvertGiftToStars(params ConvertGiftToStarsP) (bool, error) {
// ConvertGiftToStarsWithContext is the context-aware variant of ConvertGiftToStars. // ConvertGiftToStarsWithContext is the context-aware variant of ConvertGiftToStars.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#convertgifttostars // See https://core.telegram.org/bots/api#convertgifttostars
func (api *API) ConvertGiftToStarsWithContext(ctx context.Context, params ConvertGiftToStarsP) (bool, error) { func (api *API) ConvertGiftToStarsWithContext(ctx context.Context, params ConvertGiftToStars) (bool, error) {
req := NewRequest[bool]("convertGiftToStars", params) req := NewRequest[bool]("convertGiftToStars", params)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// UpgradeGiftP holds parameters for the upgradeGift method. // UpgradeGift holds parameters for the upgradeGift method.
// See https://core.telegram.org/bots/api#upgradegift // See https://core.telegram.org/bots/api#upgradegift
type UpgradeGiftP struct { type UpgradeGift struct {
BusinessConnectionID string `json:"business_connection_id"` BusinessConnectionID string `json:"business_connection_id"`
OwnedGiftID string `json:"owned_gift_id"` OwnedGiftID string `json:"owned_gift_id"`
KeepOriginalDetails bool `json:"keep_original_details,omitempty"` KeepOriginalDetails bool `json:"keep_original_details,omitempty"`
@@ -411,7 +411,7 @@ type UpgradeGiftP struct {
// UpgradeGift upgrades a gift. // UpgradeGift upgrades a gift.
// Returns true on success. // Returns true on success.
// See https://core.telegram.org/bots/api#upgradegift // See https://core.telegram.org/bots/api#upgradegift
func (api *API) UpgradeGift(params UpgradeGiftP) (bool, error) { func (api *API) UpgradeGift(params UpgradeGift) (bool, error) {
req := NewRequest[bool]("upgradeGift", params) req := NewRequest[bool]("upgradeGift", params)
return req.Do(api) return req.Do(api)
} }
@@ -419,14 +419,14 @@ func (api *API) UpgradeGift(params UpgradeGiftP) (bool, error) {
// UpgradeGiftWithContext is the context-aware variant of UpgradeGift. // UpgradeGiftWithContext is the context-aware variant of UpgradeGift.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#upgradegift // See https://core.telegram.org/bots/api#upgradegift
func (api *API) UpgradeGiftWithContext(ctx context.Context, params UpgradeGiftP) (bool, error) { func (api *API) UpgradeGiftWithContext(ctx context.Context, params UpgradeGift) (bool, error) {
req := NewRequest[bool]("upgradeGift", params) req := NewRequest[bool]("upgradeGift", params)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// TransferGiftP holds parameters for the transferGift method. // TransferGift holds parameters for the transferGift method.
// See https://core.telegram.org/bots/api#transfergift // See https://core.telegram.org/bots/api#transfergift
type TransferGiftP struct { type TransferGift struct {
BusinessConnectionID string `json:"business_connection_id"` BusinessConnectionID string `json:"business_connection_id"`
OwnedGiftID string `json:"owned_gift_id"` OwnedGiftID string `json:"owned_gift_id"`
NewOwnerChatID int64 `json:"new_owner_chat_id"` NewOwnerChatID int64 `json:"new_owner_chat_id"`
@@ -436,7 +436,7 @@ type TransferGiftP struct {
// TransferGift transfers a gift to another chat. // TransferGift transfers a gift to another chat.
// Returns true on success. // Returns true on success.
// See https://core.telegram.org/bots/api#transfergift // See https://core.telegram.org/bots/api#transfergift
func (api *API) TransferGift(params TransferGiftP) (bool, error) { func (api *API) TransferGift(params TransferGift) (bool, error) {
req := NewRequest[bool]("transferGift", params) req := NewRequest[bool]("transferGift", params)
return req.Do(api) return req.Do(api)
} }
@@ -444,14 +444,14 @@ func (api *API) TransferGift(params TransferGiftP) (bool, error) {
// TransferGiftWithContext is the context-aware variant of TransferGift. // TransferGiftWithContext is the context-aware variant of TransferGift.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#transfergift // See https://core.telegram.org/bots/api#transfergift
func (api *API) TransferGiftWithContext(ctx context.Context, params TransferGiftP) (bool, error) { func (api *API) TransferGiftWithContext(ctx context.Context, params TransferGift) (bool, error) {
req := NewRequest[bool]("transferGift", params) req := NewRequest[bool]("transferGift", params)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// PostStoryP holds parameters for the postStory method. // PostStory holds parameters for the postStory method.
// See https://core.telegram.org/bots/api#poststory // See https://core.telegram.org/bots/api#poststory
type PostStoryP struct { type PostStory struct {
BusinessConnectionID string `json:"business_connection_id"` BusinessConnectionID string `json:"business_connection_id"`
Content InputStoryContent `json:"content"` Content InputStoryContent `json:"content"`
ActivePeriod int `json:"active_period"` ActivePeriod int `json:"active_period"`
@@ -465,39 +465,24 @@ type PostStoryP struct {
ProtectContent bool `json:"protect_content,omitempty"` ProtectContent bool `json:"protect_content,omitempty"`
} }
// PostStoryPhoto posts a story with a photo. // PostStory posts a story with a photo.
// See https://core.telegram.org/bots/api#poststory // See https://core.telegram.org/bots/api#poststory
func (api *API) PostStoryPhoto(params PostStoryP) (Story, error) { func (api *API) PostStory(params PostStory) (Story, error) {
req := NewRequest[Story]("postStory", params) req := NewRequest[Story]("postStory", params)
return req.Do(api) return req.Do(api)
} }
// PostStoryPhotoWithContext is the context-aware variant of PostStoryPhoto. // PostStoryWithContext is the context-aware variant of PostStoryPhoto.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#poststory // See https://core.telegram.org/bots/api#poststory
func (api *API) PostStoryPhotoWithContext(ctx context.Context, params PostStoryP) (Story, error) { func (api *API) PostStoryWithContext(ctx context.Context, params PostStory) (Story, error) {
req := NewRequest[Story]("postStory", params) req := NewRequest[Story]("postStory", params)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// PostStoryVideo posts a story with a video. // RepostStory holds parameters for the repostStory method.
// See https://core.telegram.org/bots/api#poststory
func (api *API) PostStoryVideo(params PostStoryP) (Story, error) {
req := NewRequest[Story]("postStory", params)
return req.Do(api)
}
// PostStoryVideoWithContext is the context-aware variant of PostStoryVideo.
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#poststory
func (api *API) PostStoryVideoWithContext(ctx context.Context, params PostStoryP) (Story, error) {
req := NewRequest[Story]("postStory", params)
return req.DoWithContext(ctx, api)
}
// RepostStoryP holds parameters for the repostStory method.
// See https://core.telegram.org/bots/api#repoststory // See https://core.telegram.org/bots/api#repoststory
type RepostStoryP struct { type RepostStory struct {
BusinessConnectionID string `json:"business_connection_id"` BusinessConnectionID string `json:"business_connection_id"`
FromChatID int64 `json:"from_chat_id"` FromChatID int64 `json:"from_chat_id"`
FromStoryID int `json:"from_story_id"` FromStoryID int `json:"from_story_id"`
@@ -509,7 +494,7 @@ type RepostStoryP struct {
// RepostStory reposts a story from another chat. // RepostStory reposts a story from another chat.
// Returns the reposted story. // Returns the reposted story.
// See https://core.telegram.org/bots/api#repoststory // See https://core.telegram.org/bots/api#repoststory
func (api *API) RepostStory(params RepostStoryP) (Story, error) { func (api *API) RepostStory(params RepostStory) (Story, error) {
req := NewRequest[Story]("repostStory", params) req := NewRequest[Story]("repostStory", params)
return req.Do(api) return req.Do(api)
} }
@@ -517,14 +502,14 @@ func (api *API) RepostStory(params RepostStoryP) (Story, error) {
// RepostStoryWithContext is the context-aware variant of RepostStory. // RepostStoryWithContext is the context-aware variant of RepostStory.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#repoststory // See https://core.telegram.org/bots/api#repoststory
func (api *API) RepostStoryWithContext(ctx context.Context, params RepostStoryP) (Story, error) { func (api *API) RepostStoryWithContext(ctx context.Context, params RepostStory) (Story, error) {
req := NewRequest[Story]("repostStory", params) req := NewRequest[Story]("repostStory", params)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// EditStoryP holds parameters for the editStory method. // EditStory holds parameters for the editStory method.
// See https://core.telegram.org/bots/api#editstory // See https://core.telegram.org/bots/api#editstory
type EditStoryP struct { type EditStory struct {
BusinessConnectionID string `json:"business_connection_id"` BusinessConnectionID string `json:"business_connection_id"`
StoryID int `json:"story_id"` StoryID int `json:"story_id"`
Content InputStoryContent `json:"content"` Content InputStoryContent `json:"content"`
@@ -538,7 +523,7 @@ type EditStoryP struct {
// EditStory edits an existing story. // EditStory edits an existing story.
// Returns the updated story. // Returns the updated story.
// See https://core.telegram.org/bots/api#editstory // See https://core.telegram.org/bots/api#editstory
func (api *API) EditStory(params EditStoryP) (Story, error) { func (api *API) EditStory(params EditStory) (Story, error) {
req := NewRequest[Story]("editStory", params) req := NewRequest[Story]("editStory", params)
return req.Do(api) return req.Do(api)
} }
@@ -546,14 +531,14 @@ func (api *API) EditStory(params EditStoryP) (Story, error) {
// EditStoryWithContext is the context-aware variant of EditStory. // EditStoryWithContext is the context-aware variant of EditStory.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#editstory // See https://core.telegram.org/bots/api#editstory
func (api *API) EditStoryWithContext(ctx context.Context, params EditStoryP) (Story, error) { func (api *API) EditStoryWithContext(ctx context.Context, params EditStory) (Story, error) {
req := NewRequest[Story]("editStory", params) req := NewRequest[Story]("editStory", params)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// DeleteStoryP holds parameters for the deleteStory method. // DeleteStory holds parameters for the deleteStory method.
// See https://core.telegram.org/bots/api#deletestory // See https://core.telegram.org/bots/api#deletestory
type DeleteStoryP struct { type DeleteStory struct {
BusinessConnectionID string `json:"business_connection_id"` BusinessConnectionID string `json:"business_connection_id"`
StoryID int `json:"story_id"` StoryID int `json:"story_id"`
} }
@@ -561,7 +546,7 @@ type DeleteStoryP struct {
// DeleteStory deletes a story. // DeleteStory deletes a story.
// Returns true on success. // Returns true on success.
// See https://core.telegram.org/bots/api#deletestory // See https://core.telegram.org/bots/api#deletestory
func (api *API) DeleteStory(params DeleteStoryP) (bool, error) { func (api *API) DeleteStory(params DeleteStory) (bool, error) {
req := NewRequest[bool]("deleteStory", params) req := NewRequest[bool]("deleteStory", params)
return req.Do(api) return req.Do(api)
} }
@@ -569,7 +554,7 @@ func (api *API) DeleteStory(params DeleteStoryP) (bool, error) {
// DeleteStoryWithContext is the context-aware variant of DeleteStory. // DeleteStoryWithContext is the context-aware variant of DeleteStory.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#deletestory // See https://core.telegram.org/bots/api#deletestory
func (api *API) DeleteStoryWithContext(ctx context.Context, params DeleteStoryP) (bool, error) { func (api *API) DeleteStoryWithContext(ctx context.Context, params DeleteStory) (bool, error) {
req := NewRequest[bool]("deleteStory", params) req := NewRequest[bool]("deleteStory", params)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
+11 -4
View File
@@ -72,7 +72,9 @@ type BusinessMessagesDeleted struct {
type InputStoryContentType string type InputStoryContentType string
const ( const (
// InputStoryContentPhotoType identifies photo story content.
InputStoryContentPhotoType InputStoryContentType = "photo" InputStoryContentPhotoType InputStoryContentType = "photo"
// InputStoryContentVideoType identifies video story content.
InputStoryContentVideoType InputStoryContentType = "video" InputStoryContentVideoType InputStoryContentType = "video"
) )
@@ -106,10 +108,15 @@ type StoryAreaPosition struct {
type StoryAreaTypeType string type StoryAreaTypeType string
const ( const (
StoryAreaTypeLocationType StoryAreaTypeType = "location" // StoryAreaTypeLocationType identifies a location story area.
StoryAreaTypeReactionType StoryAreaTypeType = "suggested_reaction" StoryAreaTypeLocationType StoryAreaTypeType = "location"
StoryAreaTypeLinkType StoryAreaTypeType = "link" // StoryAreaTypeReactionType identifies a suggested reaction story area.
StoryAreaTypeWeatherType StoryAreaTypeType = "weather" StoryAreaTypeReactionType StoryAreaTypeType = "suggested_reaction"
// StoryAreaTypeLinkType identifies a link story area.
StoryAreaTypeLinkType StoryAreaTypeType = "link"
// StoryAreaTypeWeatherType identifies a weather story area.
StoryAreaTypeWeatherType StoryAreaTypeType = "weather"
// StoryAreaTypeUniqueGiftType identifies a unique gift story area.
StoryAreaTypeUniqueGiftType StoryAreaTypeType = "unique_gift" StoryAreaTypeUniqueGiftType StoryAreaTypeType = "unique_gift"
) )
+127 -127
View File
@@ -2,9 +2,9 @@ package tgapi
import "context" import "context"
// BanChatMemberP holds parameters for the banChatMember method. // BanChatMember holds parameters for the banChatMember method.
// See https://core.telegram.org/bots/api#banchatmember // See https://core.telegram.org/bots/api#banchatmember
type BanChatMemberP struct { type BanChatMember struct {
ChatID int64 `json:"chat_id"` ChatID int64 `json:"chat_id"`
UserID int64 `json:"user_id"` UserID int64 `json:"user_id"`
UntilDate int `json:"until_date,omitempty"` UntilDate int `json:"until_date,omitempty"`
@@ -14,7 +14,7 @@ type BanChatMemberP struct {
// BanChatMember bans a user in a chat. // BanChatMember bans a user in a chat.
// Returns True on success. // Returns True on success.
// See https://core.telegram.org/bots/api#banchatmember // See https://core.telegram.org/bots/api#banchatmember
func (api *API) BanChatMember(params BanChatMemberP) (bool, error) { func (api *API) BanChatMember(params BanChatMember) (bool, error) {
req := NewRequestWithChatID[bool]("banChatMember", params, params.ChatID) req := NewRequestWithChatID[bool]("banChatMember", params, params.ChatID)
return req.Do(api) return req.Do(api)
} }
@@ -22,14 +22,14 @@ func (api *API) BanChatMember(params BanChatMemberP) (bool, error) {
// BanChatMemberWithContext is the context-aware variant of BanChatMember. // BanChatMemberWithContext is the context-aware variant of BanChatMember.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#banchatmember // See https://core.telegram.org/bots/api#banchatmember
func (api *API) BanChatMemberWithContext(ctx context.Context, params BanChatMemberP) (bool, error) { func (api *API) BanChatMemberWithContext(ctx context.Context, params BanChatMember) (bool, error) {
req := NewRequestWithChatID[bool]("banChatMember", params, params.ChatID) req := NewRequestWithChatID[bool]("banChatMember", params, params.ChatID)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// UnbanChatMemberP holds parameters for the unbanChatMember method. // UnbanChatMember holds parameters for the unbanChatMember method.
// See https://core.telegram.org/bots/api#unbanchatmember // See https://core.telegram.org/bots/api#unbanchatmember
type UnbanChatMemberP struct { type UnbanChatMember struct {
ChatID int64 `json:"chat_id"` ChatID int64 `json:"chat_id"`
UserID int64 `json:"user_id"` UserID int64 `json:"user_id"`
OnlyIfBanned bool `json:"only_if_banned"` OnlyIfBanned bool `json:"only_if_banned"`
@@ -38,7 +38,7 @@ type UnbanChatMemberP struct {
// UnbanChatMember unbans a previously banned user in a chat. // UnbanChatMember unbans a previously banned user in a chat.
// Returns True on success. // Returns True on success.
// See https://core.telegram.org/bots/api#unbanchatmember // See https://core.telegram.org/bots/api#unbanchatmember
func (api *API) UnbanChatMember(params UnbanChatMemberP) (bool, error) { func (api *API) UnbanChatMember(params UnbanChatMember) (bool, error) {
req := NewRequestWithChatID[bool]("unbanChatMember", params, params.ChatID) req := NewRequestWithChatID[bool]("unbanChatMember", params, params.ChatID)
return req.Do(api) return req.Do(api)
} }
@@ -46,14 +46,14 @@ func (api *API) UnbanChatMember(params UnbanChatMemberP) (bool, error) {
// UnbanChatMemberWithContext is the context-aware variant of UnbanChatMember. // UnbanChatMemberWithContext is the context-aware variant of UnbanChatMember.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#unbanchatmember // See https://core.telegram.org/bots/api#unbanchatmember
func (api *API) UnbanChatMemberWithContext(ctx context.Context, params UnbanChatMemberP) (bool, error) { func (api *API) UnbanChatMemberWithContext(ctx context.Context, params UnbanChatMember) (bool, error) {
req := NewRequestWithChatID[bool]("unbanChatMember", params, params.ChatID) req := NewRequestWithChatID[bool]("unbanChatMember", params, params.ChatID)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// RestrictChatMemberP holds parameters for the restrictChatMember method. // RestrictChatMember holds parameters for the restrictChatMember method.
// See https://core.telegram.org/bots/api#restrictchatmember // See https://core.telegram.org/bots/api#restrictchatmember
type RestrictChatMemberP struct { type RestrictChatMember struct {
ChatID int64 `json:"chat_id"` ChatID int64 `json:"chat_id"`
UserID int64 `json:"user_id"` UserID int64 `json:"user_id"`
Permissions ChatPermissions `json:"permissions"` Permissions ChatPermissions `json:"permissions"`
@@ -64,7 +64,7 @@ type RestrictChatMemberP struct {
// RestrictChatMember restricts a user in a chat. // RestrictChatMember restricts a user in a chat.
// Returns True on success. // Returns True on success.
// See https://core.telegram.org/bots/api#restrictchatmember // See https://core.telegram.org/bots/api#restrictchatmember
func (api *API) RestrictChatMember(params RestrictChatMemberP) (bool, error) { func (api *API) RestrictChatMember(params RestrictChatMember) (bool, error) {
req := NewRequestWithChatID[bool]("restrictChatMember", params, params.ChatID) req := NewRequestWithChatID[bool]("restrictChatMember", params, params.ChatID)
return req.Do(api) return req.Do(api)
} }
@@ -72,7 +72,7 @@ func (api *API) RestrictChatMember(params RestrictChatMemberP) (bool, error) {
// RestrictChatMemberWithContext is the context-aware variant of RestrictChatMember. // RestrictChatMemberWithContext is the context-aware variant of RestrictChatMember.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#restrictchatmember // See https://core.telegram.org/bots/api#restrictchatmember
func (api *API) RestrictChatMemberWithContext(ctx context.Context, params RestrictChatMemberP) (bool, error) { func (api *API) RestrictChatMemberWithContext(ctx context.Context, params RestrictChatMember) (bool, error) {
req := NewRequestWithChatID[bool]("restrictChatMember", params, params.ChatID) req := NewRequestWithChatID[bool]("restrictChatMember", params, params.ChatID)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
@@ -118,9 +118,9 @@ func (api *API) PromoteChatMemberWithContext(ctx context.Context, params Promote
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// SetChatAdministratorCustomTitleP holds parameters for the setChatAdministratorCustomTitle method. // SetChatAdministratorCustomTitle holds parameters for the setChatAdministratorCustomTitle method.
// See https://core.telegram.org/bots/api#setchatadministratorcustomtitle // See https://core.telegram.org/bots/api#setchatadministratorcustomtitle
type SetChatAdministratorCustomTitleP struct { type SetChatAdministratorCustomTitle struct {
ChatID int64 `json:"chat_id"` ChatID int64 `json:"chat_id"`
UserID int64 `json:"user_id"` UserID int64 `json:"user_id"`
CustomTitle string `json:"custom_title"` CustomTitle string `json:"custom_title"`
@@ -129,7 +129,7 @@ type SetChatAdministratorCustomTitleP struct {
// SetChatAdministratorCustomTitle sets a custom title for an administrator. // SetChatAdministratorCustomTitle sets a custom title for an administrator.
// Returns True on success. // Returns True on success.
// See https://core.telegram.org/bots/api#setchatadministratorcustomtitle // See https://core.telegram.org/bots/api#setchatadministratorcustomtitle
func (api *API) SetChatAdministratorCustomTitle(params SetChatAdministratorCustomTitleP) (bool, error) { func (api *API) SetChatAdministratorCustomTitle(params SetChatAdministratorCustomTitle) (bool, error) {
req := NewRequestWithChatID[bool]("setChatAdministratorCustomTitle", params, params.ChatID) req := NewRequestWithChatID[bool]("setChatAdministratorCustomTitle", params, params.ChatID)
return req.Do(api) return req.Do(api)
} }
@@ -137,14 +137,14 @@ func (api *API) SetChatAdministratorCustomTitle(params SetChatAdministratorCusto
// SetChatAdministratorCustomTitleWithContext is the context-aware variant of SetChatAdministratorCustomTitle. // SetChatAdministratorCustomTitleWithContext is the context-aware variant of SetChatAdministratorCustomTitle.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#setchatadministratorcustomtitle // See https://core.telegram.org/bots/api#setchatadministratorcustomtitle
func (api *API) SetChatAdministratorCustomTitleWithContext(ctx context.Context, params SetChatAdministratorCustomTitleP) (bool, error) { func (api *API) SetChatAdministratorCustomTitleWithContext(ctx context.Context, params SetChatAdministratorCustomTitle) (bool, error) {
req := NewRequestWithChatID[bool]("setChatAdministratorCustomTitle", params, params.ChatID) req := NewRequestWithChatID[bool]("setChatAdministratorCustomTitle", params, params.ChatID)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// SetChatMemberTagP holds parameters for the setChatMemberTag method. // SetChatMemberTag holds parameters for the setChatMemberTag method.
// See https://core.telegram.org/bots/api#setchatmembertag // See https://core.telegram.org/bots/api#setchatmembertag
type SetChatMemberTagP struct { type SetChatMemberTag struct {
ChatID int64 `json:"chat_id"` ChatID int64 `json:"chat_id"`
UserID int64 `json:"user_id"` UserID int64 `json:"user_id"`
Tag string `json:"tag,omitempty"` Tag string `json:"tag,omitempty"`
@@ -153,7 +153,7 @@ type SetChatMemberTagP struct {
// SetChatMemberTag sets a tag for a chat member. // SetChatMemberTag sets a tag for a chat member.
// Returns True on success. // Returns True on success.
// See https://core.telegram.org/bots/api#setchatmembertag // See https://core.telegram.org/bots/api#setchatmembertag
func (api *API) SetChatMemberTag(params SetChatMemberTagP) (bool, error) { func (api *API) SetChatMemberTag(params SetChatMemberTag) (bool, error) {
req := NewRequestWithChatID[bool]("setChatMemberTag", params, params.ChatID) req := NewRequestWithChatID[bool]("setChatMemberTag", params, params.ChatID)
return req.Do(api) return req.Do(api)
} }
@@ -161,14 +161,14 @@ func (api *API) SetChatMemberTag(params SetChatMemberTagP) (bool, error) {
// SetChatMemberTagWithContext is the context-aware variant of SetChatMemberTag. // SetChatMemberTagWithContext is the context-aware variant of SetChatMemberTag.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#setchatmembertag // See https://core.telegram.org/bots/api#setchatmembertag
func (api *API) SetChatMemberTagWithContext(ctx context.Context, params SetChatMemberTagP) (bool, error) { func (api *API) SetChatMemberTagWithContext(ctx context.Context, params SetChatMemberTag) (bool, error) {
req := NewRequestWithChatID[bool]("setChatMemberTag", params, params.ChatID) req := NewRequestWithChatID[bool]("setChatMemberTag", params, params.ChatID)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// BanChatSenderChatP holds parameters for the banChatSenderChat method. // BanChatSenderChat holds parameters for the banChatSenderChat method.
// See https://core.telegram.org/bots/api#banchatsenderchat // See https://core.telegram.org/bots/api#banchatsenderchat
type BanChatSenderChatP struct { type BanChatSenderChat struct {
ChatID int64 `json:"chat_id"` ChatID int64 `json:"chat_id"`
SenderChatID int64 `json:"sender_chat_id"` SenderChatID int64 `json:"sender_chat_id"`
} }
@@ -176,7 +176,7 @@ type BanChatSenderChatP struct {
// BanChatSenderChat bans a channel chat in a supergroup or channel. // BanChatSenderChat bans a channel chat in a supergroup or channel.
// Returns True on success. // Returns True on success.
// See https://core.telegram.org/bots/api#banchatsenderchat // See https://core.telegram.org/bots/api#banchatsenderchat
func (api *API) BanChatSenderChat(params BanChatSenderChatP) (bool, error) { func (api *API) BanChatSenderChat(params BanChatSenderChat) (bool, error) {
req := NewRequestWithChatID[bool]("banChatSenderChat", params, params.ChatID) req := NewRequestWithChatID[bool]("banChatSenderChat", params, params.ChatID)
return req.Do(api) return req.Do(api)
} }
@@ -184,14 +184,14 @@ func (api *API) BanChatSenderChat(params BanChatSenderChatP) (bool, error) {
// BanChatSenderChatWithContext is the context-aware variant of BanChatSenderChat. // BanChatSenderChatWithContext is the context-aware variant of BanChatSenderChat.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#banchatsenderchat // See https://core.telegram.org/bots/api#banchatsenderchat
func (api *API) BanChatSenderChatWithContext(ctx context.Context, params BanChatSenderChatP) (bool, error) { func (api *API) BanChatSenderChatWithContext(ctx context.Context, params BanChatSenderChat) (bool, error) {
req := NewRequestWithChatID[bool]("banChatSenderChat", params, params.ChatID) req := NewRequestWithChatID[bool]("banChatSenderChat", params, params.ChatID)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// UnbanChatSenderChatP holds parameters for the unbanChatSenderChat method. // UnbanChatSenderChat holds parameters for the unbanChatSenderChat method.
// See https://core.telegram.org/bots/api#unbanchatsenderchat // See https://core.telegram.org/bots/api#unbanchatsenderchat
type UnbanChatSenderChatP struct { type UnbanChatSenderChat struct {
ChatID int64 `json:"chat_id"` ChatID int64 `json:"chat_id"`
SenderChatID int64 `json:"sender_chat_id"` SenderChatID int64 `json:"sender_chat_id"`
} }
@@ -199,7 +199,7 @@ type UnbanChatSenderChatP struct {
// UnbanChatSenderChat unbans a previously banned channel chat. // UnbanChatSenderChat unbans a previously banned channel chat.
// Returns True on success. // Returns True on success.
// See https://core.telegram.org/bots/api#unbanchatsenderchat // See https://core.telegram.org/bots/api#unbanchatsenderchat
func (api *API) UnbanChatSenderChat(params UnbanChatSenderChatP) (bool, error) { func (api *API) UnbanChatSenderChat(params UnbanChatSenderChat) (bool, error) {
req := NewRequestWithChatID[bool]("unbanChatSenderChat", params, params.ChatID) req := NewRequestWithChatID[bool]("unbanChatSenderChat", params, params.ChatID)
return req.Do(api) return req.Do(api)
} }
@@ -207,14 +207,14 @@ func (api *API) UnbanChatSenderChat(params UnbanChatSenderChatP) (bool, error) {
// UnbanChatSenderChatWithContext is the context-aware variant of UnbanChatSenderChat. // UnbanChatSenderChatWithContext is the context-aware variant of UnbanChatSenderChat.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#unbanchatsenderchat // See https://core.telegram.org/bots/api#unbanchatsenderchat
func (api *API) UnbanChatSenderChatWithContext(ctx context.Context, params UnbanChatSenderChatP) (bool, error) { func (api *API) UnbanChatSenderChatWithContext(ctx context.Context, params UnbanChatSenderChat) (bool, error) {
req := NewRequestWithChatID[bool]("unbanChatSenderChat", params, params.ChatID) req := NewRequestWithChatID[bool]("unbanChatSenderChat", params, params.ChatID)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// SetChatPermissionsP holds parameters for the setChatPermissions method. // SetChatPermissions holds parameters for the setChatPermissions method.
// See https://core.telegram.org/bots/api#setchatpermissions // See https://core.telegram.org/bots/api#setchatpermissions
type SetChatPermissionsP struct { type SetChatPermissions struct {
ChatID int64 `json:"chat_id"` ChatID int64 `json:"chat_id"`
Permissions ChatPermissions `json:"permissions"` Permissions ChatPermissions `json:"permissions"`
UseIndependentChatPermissions bool `json:"use_independent_chat_permissions,omitempty"` UseIndependentChatPermissions bool `json:"use_independent_chat_permissions,omitempty"`
@@ -223,7 +223,7 @@ type SetChatPermissionsP struct {
// SetChatPermissions sets default chat permissions for all members. // SetChatPermissions sets default chat permissions for all members.
// Returns True on success. // Returns True on success.
// See https://core.telegram.org/bots/api#setchatpermissions // See https://core.telegram.org/bots/api#setchatpermissions
func (api *API) SetChatPermissions(params SetChatPermissionsP) (bool, error) { func (api *API) SetChatPermissions(params SetChatPermissions) (bool, error) {
req := NewRequestWithChatID[bool]("setChatPermissions", params, params.ChatID) req := NewRequestWithChatID[bool]("setChatPermissions", params, params.ChatID)
return req.Do(api) return req.Do(api)
} }
@@ -231,21 +231,21 @@ func (api *API) SetChatPermissions(params SetChatPermissionsP) (bool, error) {
// SetChatPermissionsWithContext is the context-aware variant of SetChatPermissions. // SetChatPermissionsWithContext is the context-aware variant of SetChatPermissions.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#setchatpermissions // See https://core.telegram.org/bots/api#setchatpermissions
func (api *API) SetChatPermissionsWithContext(ctx context.Context, params SetChatPermissionsP) (bool, error) { func (api *API) SetChatPermissionsWithContext(ctx context.Context, params SetChatPermissions) (bool, error) {
req := NewRequestWithChatID[bool]("setChatPermissions", params, params.ChatID) req := NewRequestWithChatID[bool]("setChatPermissions", params, params.ChatID)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// ExportChatInviteLinkP holds parameters for the exportChatInviteLink method. // ExportChatInviteLink holds parameters for the exportChatInviteLink method.
// See https://core.telegram.org/bots/api#exportchatinvitelink // See https://core.telegram.org/bots/api#exportchatinvitelink
type ExportChatInviteLinkP struct { type ExportChatInviteLink struct {
ChatID int64 `json:"chat_id"` ChatID int64 `json:"chat_id"`
} }
// ExportChatInviteLink generates a new primary invite link for a chat. // ExportChatInviteLink generates a new primary invite link for a chat.
// Returns the new invite link as string. // Returns the new invite link as string.
// See https://core.telegram.org/bots/api#exportchatinvitelink // See https://core.telegram.org/bots/api#exportchatinvitelink
func (api *API) ExportChatInviteLink(params ExportChatInviteLinkP) (string, error) { func (api *API) ExportChatInviteLink(params ExportChatInviteLink) (string, error) {
req := NewRequestWithChatID[string]("exportChatInviteLink", params, params.ChatID) req := NewRequestWithChatID[string]("exportChatInviteLink", params, params.ChatID)
return req.Do(api) return req.Do(api)
} }
@@ -253,14 +253,14 @@ func (api *API) ExportChatInviteLink(params ExportChatInviteLinkP) (string, erro
// ExportChatInviteLinkWithContext is the context-aware variant of ExportChatInviteLink. // ExportChatInviteLinkWithContext is the context-aware variant of ExportChatInviteLink.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#exportchatinvitelink // See https://core.telegram.org/bots/api#exportchatinvitelink
func (api *API) ExportChatInviteLinkWithContext(ctx context.Context, params ExportChatInviteLinkP) (string, error) { func (api *API) ExportChatInviteLinkWithContext(ctx context.Context, params ExportChatInviteLink) (string, error) {
req := NewRequestWithChatID[string]("exportChatInviteLink", params, params.ChatID) req := NewRequestWithChatID[string]("exportChatInviteLink", params, params.ChatID)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// CreateChatInviteLinkP holds parameters for the createChatInviteLink method. // CreateChatInviteLink holds parameters for the createChatInviteLink method.
// See https://core.telegram.org/bots/api#createchatinvitelink // See https://core.telegram.org/bots/api#createchatinvitelink
type CreateChatInviteLinkP struct { type CreateChatInviteLink struct {
ChatID int64 `json:"chat_id"` ChatID int64 `json:"chat_id"`
Name *string `json:"name,omitempty"` Name *string `json:"name,omitempty"`
ExpireDate int `json:"expire_date,omitempty"` ExpireDate int `json:"expire_date,omitempty"`
@@ -271,7 +271,7 @@ type CreateChatInviteLinkP struct {
// CreateChatInviteLink creates an additional invite link for a chat. // CreateChatInviteLink creates an additional invite link for a chat.
// Returns the created invite link. // Returns the created invite link.
// See https://core.telegram.org/bots/api#createchatinvitelink // See https://core.telegram.org/bots/api#createchatinvitelink
func (api *API) CreateChatInviteLink(params CreateChatInviteLinkP) (ChatInviteLink, error) { func (api *API) CreateChatInviteLink(params CreateChatInviteLink) (ChatInviteLink, error) {
req := NewRequestWithChatID[ChatInviteLink]("createChatInviteLink", params, params.ChatID) req := NewRequestWithChatID[ChatInviteLink]("createChatInviteLink", params, params.ChatID)
return req.Do(api) return req.Do(api)
} }
@@ -279,14 +279,14 @@ func (api *API) CreateChatInviteLink(params CreateChatInviteLinkP) (ChatInviteLi
// CreateChatInviteLinkWithContext is the context-aware variant of CreateChatInviteLink. // CreateChatInviteLinkWithContext is the context-aware variant of CreateChatInviteLink.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#createchatinvitelink // See https://core.telegram.org/bots/api#createchatinvitelink
func (api *API) CreateChatInviteLinkWithContext(ctx context.Context, params CreateChatInviteLinkP) (ChatInviteLink, error) { func (api *API) CreateChatInviteLinkWithContext(ctx context.Context, params CreateChatInviteLink) (ChatInviteLink, error) {
req := NewRequestWithChatID[ChatInviteLink]("createChatInviteLink", params, params.ChatID) req := NewRequestWithChatID[ChatInviteLink]("createChatInviteLink", params, params.ChatID)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// EditChatInviteLinkP holds parameters for the editChatInviteLink method. // EditChatInviteLink holds parameters for the editChatInviteLink method.
// See https://core.telegram.org/bots/api#editchatinvitelink // See https://core.telegram.org/bots/api#editchatinvitelink
type EditChatInviteLinkP struct { type EditChatInviteLink struct {
ChatID int64 `json:"chat_id"` ChatID int64 `json:"chat_id"`
InviteLink string `json:"invite_link"` InviteLink string `json:"invite_link"`
@@ -299,7 +299,7 @@ type EditChatInviteLinkP struct {
// EditChatInviteLink edits a nonprimary invite link. // EditChatInviteLink edits a nonprimary invite link.
// Returns the edited invite link. // Returns the edited invite link.
// See https://core.telegram.org/bots/api#editchatinvitelink // See https://core.telegram.org/bots/api#editchatinvitelink
func (api *API) EditChatInviteLink(params EditChatInviteLinkP) (ChatInviteLink, error) { func (api *API) EditChatInviteLink(params EditChatInviteLink) (ChatInviteLink, error) {
req := NewRequestWithChatID[ChatInviteLink]("editChatInviteLink", params, params.ChatID) req := NewRequestWithChatID[ChatInviteLink]("editChatInviteLink", params, params.ChatID)
return req.Do(api) return req.Do(api)
} }
@@ -307,14 +307,14 @@ func (api *API) EditChatInviteLink(params EditChatInviteLinkP) (ChatInviteLink,
// EditChatInviteLinkWithContext is the context-aware variant of EditChatInviteLink. // EditChatInviteLinkWithContext is the context-aware variant of EditChatInviteLink.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#editchatinvitelink // See https://core.telegram.org/bots/api#editchatinvitelink
func (api *API) EditChatInviteLinkWithContext(ctx context.Context, params EditChatInviteLinkP) (ChatInviteLink, error) { func (api *API) EditChatInviteLinkWithContext(ctx context.Context, params EditChatInviteLink) (ChatInviteLink, error) {
req := NewRequestWithChatID[ChatInviteLink]("editChatInviteLink", params, params.ChatID) req := NewRequestWithChatID[ChatInviteLink]("editChatInviteLink", params, params.ChatID)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// CreateChatSubscriptionInviteLinkP holds parameters for the createChatSubscriptionInviteLink method. // CreateChatSubscriptionInviteLink holds parameters for the createChatSubscriptionInviteLink method.
// See https://core.telegram.org/bots/api#createchatsubscriptioninvitelink // See https://core.telegram.org/bots/api#createchatsubscriptioninvitelink
type CreateChatSubscriptionInviteLinkP struct { type CreateChatSubscriptionInviteLink struct {
ChatID int64 `json:"chat_id"` ChatID int64 `json:"chat_id"`
Name string `json:"name,omitempty"` Name string `json:"name,omitempty"`
SubscriptionPeriod int `json:"subscription_period,omitempty"` SubscriptionPeriod int `json:"subscription_period,omitempty"`
@@ -324,7 +324,7 @@ type CreateChatSubscriptionInviteLinkP struct {
// CreateChatSubscriptionInviteLink creates a subscription invite link for a channel chat. // CreateChatSubscriptionInviteLink creates a subscription invite link for a channel chat.
// Returns the created invite link. // Returns the created invite link.
// See https://core.telegram.org/bots/api#createchatsubscriptioninvitelink // See https://core.telegram.org/bots/api#createchatsubscriptioninvitelink
func (api *API) CreateChatSubscriptionInviteLink(params CreateChatSubscriptionInviteLinkP) (ChatInviteLink, error) { func (api *API) CreateChatSubscriptionInviteLink(params CreateChatSubscriptionInviteLink) (ChatInviteLink, error) {
req := NewRequestWithChatID[ChatInviteLink]("createChatSubscriptionInviteLink", params, params.ChatID) req := NewRequestWithChatID[ChatInviteLink]("createChatSubscriptionInviteLink", params, params.ChatID)
return req.Do(api) return req.Do(api)
} }
@@ -332,14 +332,14 @@ func (api *API) CreateChatSubscriptionInviteLink(params CreateChatSubscriptionIn
// CreateChatSubscriptionInviteLinkWithContext is the context-aware variant of CreateChatSubscriptionInviteLink. // CreateChatSubscriptionInviteLinkWithContext is the context-aware variant of CreateChatSubscriptionInviteLink.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#createchatsubscriptioninvitelink // See https://core.telegram.org/bots/api#createchatsubscriptioninvitelink
func (api *API) CreateChatSubscriptionInviteLinkWithContext(ctx context.Context, params CreateChatSubscriptionInviteLinkP) (ChatInviteLink, error) { func (api *API) CreateChatSubscriptionInviteLinkWithContext(ctx context.Context, params CreateChatSubscriptionInviteLink) (ChatInviteLink, error) {
req := NewRequestWithChatID[ChatInviteLink]("createChatSubscriptionInviteLink", params, params.ChatID) req := NewRequestWithChatID[ChatInviteLink]("createChatSubscriptionInviteLink", params, params.ChatID)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// EditChatSubscriptionInviteLinkP holds parameters for the editChatSubscriptionInviteLink method. // EditChatSubscriptionInviteLink holds parameters for the editChatSubscriptionInviteLink method.
// See https://core.telegram.org/bots/api#editchatsubscriptioninvitelink // See https://core.telegram.org/bots/api#editchatsubscriptioninvitelink
type EditChatSubscriptionInviteLinkP struct { type EditChatSubscriptionInviteLink struct {
ChatID int64 `json:"chat_id"` ChatID int64 `json:"chat_id"`
InviteLink string `json:"invite_link"` InviteLink string `json:"invite_link"`
Name string `json:"name,omitempty"` Name string `json:"name,omitempty"`
@@ -348,7 +348,7 @@ type EditChatSubscriptionInviteLinkP struct {
// EditChatSubscriptionInviteLink edits a subscription invite link. // EditChatSubscriptionInviteLink edits a subscription invite link.
// Returns the edited invite link. // Returns the edited invite link.
// See https://core.telegram.org/bots/api#editchatsubscriptioninvitelink // See https://core.telegram.org/bots/api#editchatsubscriptioninvitelink
func (api *API) EditChatSubscriptionInviteLink(params EditChatSubscriptionInviteLinkP) (ChatInviteLink, error) { func (api *API) EditChatSubscriptionInviteLink(params EditChatSubscriptionInviteLink) (ChatInviteLink, error) {
req := NewRequestWithChatID[ChatInviteLink]("editChatSubscriptionInviteLink", params, params.ChatID) req := NewRequestWithChatID[ChatInviteLink]("editChatSubscriptionInviteLink", params, params.ChatID)
return req.Do(api) return req.Do(api)
} }
@@ -356,14 +356,14 @@ func (api *API) EditChatSubscriptionInviteLink(params EditChatSubscriptionInvite
// EditChatSubscriptionInviteLinkWithContext is the context-aware variant of EditChatSubscriptionInviteLink. // EditChatSubscriptionInviteLinkWithContext is the context-aware variant of EditChatSubscriptionInviteLink.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#editchatsubscriptioninvitelink // See https://core.telegram.org/bots/api#editchatsubscriptioninvitelink
func (api *API) EditChatSubscriptionInviteLinkWithContext(ctx context.Context, params EditChatSubscriptionInviteLinkP) (ChatInviteLink, error) { func (api *API) EditChatSubscriptionInviteLinkWithContext(ctx context.Context, params EditChatSubscriptionInviteLink) (ChatInviteLink, error) {
req := NewRequestWithChatID[ChatInviteLink]("editChatSubscriptionInviteLink", params, params.ChatID) req := NewRequestWithChatID[ChatInviteLink]("editChatSubscriptionInviteLink", params, params.ChatID)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// RevokeChatInviteLinkP holds parameters for the revokeChatInviteLink method. // RevokeChatInviteLink holds parameters for the revokeChatInviteLink method.
// See https://core.telegram.org/bots/api#revokechatinvitelink // See https://core.telegram.org/bots/api#revokechatinvitelink
type RevokeChatInviteLinkP struct { type RevokeChatInviteLink struct {
ChatID int64 `json:"chat_id"` ChatID int64 `json:"chat_id"`
InviteLink string `json:"invite_link"` InviteLink string `json:"invite_link"`
} }
@@ -371,7 +371,7 @@ type RevokeChatInviteLinkP struct {
// RevokeChatInviteLink revokes an invite link. // RevokeChatInviteLink revokes an invite link.
// Returns the revoked invite link object. // Returns the revoked invite link object.
// See https://core.telegram.org/bots/api#revokechatinvitelink // See https://core.telegram.org/bots/api#revokechatinvitelink
func (api *API) RevokeChatInviteLink(params RevokeChatInviteLinkP) (ChatInviteLink, error) { func (api *API) RevokeChatInviteLink(params RevokeChatInviteLink) (ChatInviteLink, error) {
req := NewRequestWithChatID[ChatInviteLink]("revokeChatInviteLink", params, params.ChatID) req := NewRequestWithChatID[ChatInviteLink]("revokeChatInviteLink", params, params.ChatID)
return req.Do(api) return req.Do(api)
} }
@@ -379,14 +379,14 @@ func (api *API) RevokeChatInviteLink(params RevokeChatInviteLinkP) (ChatInviteLi
// RevokeChatInviteLinkWithContext is the context-aware variant of RevokeChatInviteLink. // RevokeChatInviteLinkWithContext is the context-aware variant of RevokeChatInviteLink.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#revokechatinvitelink // See https://core.telegram.org/bots/api#revokechatinvitelink
func (api *API) RevokeChatInviteLinkWithContext(ctx context.Context, params RevokeChatInviteLinkP) (ChatInviteLink, error) { func (api *API) RevokeChatInviteLinkWithContext(ctx context.Context, params RevokeChatInviteLink) (ChatInviteLink, error) {
req := NewRequestWithChatID[ChatInviteLink]("revokeChatInviteLink", params, params.ChatID) req := NewRequestWithChatID[ChatInviteLink]("revokeChatInviteLink", params, params.ChatID)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// ApproveChatJoinRequestP holds parameters for the approveChatJoinRequest method. // ApproveChatJoinRequest holds parameters for the approveChatJoinRequest method.
// See https://core.telegram.org/bots/api#approvechatjoinrequest // See https://core.telegram.org/bots/api#approvechatjoinrequest
type ApproveChatJoinRequestP struct { type ApproveChatJoinRequest struct {
ChatID int64 `json:"chat_id"` ChatID int64 `json:"chat_id"`
UserID int64 `json:"user_id"` UserID int64 `json:"user_id"`
} }
@@ -394,7 +394,7 @@ type ApproveChatJoinRequestP struct {
// ApproveChatJoinRequest approves a chat join request. // ApproveChatJoinRequest approves a chat join request.
// Returns True on success. // Returns True on success.
// See https://core.telegram.org/bots/api#approvechatjoinrequest // See https://core.telegram.org/bots/api#approvechatjoinrequest
func (api *API) ApproveChatJoinRequest(params ApproveChatJoinRequestP) (bool, error) { func (api *API) ApproveChatJoinRequest(params ApproveChatJoinRequest) (bool, error) {
req := NewRequestWithChatID[bool]("approveChatJoinRequest", params, params.ChatID) req := NewRequestWithChatID[bool]("approveChatJoinRequest", params, params.ChatID)
return req.Do(api) return req.Do(api)
} }
@@ -402,14 +402,14 @@ func (api *API) ApproveChatJoinRequest(params ApproveChatJoinRequestP) (bool, er
// ApproveChatJoinRequestWithContext is the context-aware variant of ApproveChatJoinRequest. // ApproveChatJoinRequestWithContext is the context-aware variant of ApproveChatJoinRequest.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#approvechatjoinrequest // See https://core.telegram.org/bots/api#approvechatjoinrequest
func (api *API) ApproveChatJoinRequestWithContext(ctx context.Context, params ApproveChatJoinRequestP) (bool, error) { func (api *API) ApproveChatJoinRequestWithContext(ctx context.Context, params ApproveChatJoinRequest) (bool, error) {
req := NewRequestWithChatID[bool]("approveChatJoinRequest", params, params.ChatID) req := NewRequestWithChatID[bool]("approveChatJoinRequest", params, params.ChatID)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// DeclineChatJoinRequestP holds parameters for the declineChatJoinRequest method. // DeclineChatJoinRequest holds parameters for the declineChatJoinRequest method.
// See https://core.telegram.org/bots/api#declinechatjoinrequest // See https://core.telegram.org/bots/api#declinechatjoinrequest
type DeclineChatJoinRequestP struct { type DeclineChatJoinRequest struct {
ChatID int64 `json:"chat_id"` ChatID int64 `json:"chat_id"`
UserID int64 `json:"user_id"` UserID int64 `json:"user_id"`
} }
@@ -417,7 +417,7 @@ type DeclineChatJoinRequestP struct {
// DeclineChatJoinRequest declines a chat join request. // DeclineChatJoinRequest declines a chat join request.
// Returns True on success. // Returns True on success.
// See https://core.telegram.org/bots/api#declinechatjoinrequest // See https://core.telegram.org/bots/api#declinechatjoinrequest
func (api *API) DeclineChatJoinRequest(params DeclineChatJoinRequestP) (bool, error) { func (api *API) DeclineChatJoinRequest(params DeclineChatJoinRequest) (bool, error) {
req := NewRequestWithChatID[bool]("declineChatJoinRequest", params, params.ChatID) req := NewRequestWithChatID[bool]("declineChatJoinRequest", params, params.ChatID)
return req.Do(api) return req.Do(api)
} }
@@ -425,14 +425,14 @@ func (api *API) DeclineChatJoinRequest(params DeclineChatJoinRequestP) (bool, er
// DeclineChatJoinRequestWithContext is the context-aware variant of DeclineChatJoinRequest. // DeclineChatJoinRequestWithContext is the context-aware variant of DeclineChatJoinRequest.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#declinechatjoinrequest // See https://core.telegram.org/bots/api#declinechatjoinrequest
func (api *API) DeclineChatJoinRequestWithContext(ctx context.Context, params DeclineChatJoinRequestP) (bool, error) { func (api *API) DeclineChatJoinRequestWithContext(ctx context.Context, params DeclineChatJoinRequest) (bool, error) {
req := NewRequestWithChatID[bool]("declineChatJoinRequest", params, params.ChatID) req := NewRequestWithChatID[bool]("declineChatJoinRequest", params, params.ChatID)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// SetChatPhotoP holds parameters for the setChatPhoto method. // SetChatPhoto holds parameters for the setChatPhoto method.
// See https://core.telegram.org/bots/api#setchatphoto // See https://core.telegram.org/bots/api#setchatphoto
type SetChatPhotoP struct { type SetChatPhoto struct {
ChatID int64 `json:"chat_id"` ChatID int64 `json:"chat_id"`
} }
@@ -440,7 +440,7 @@ type SetChatPhotoP struct {
// photo is the file to upload as the new photo. // photo is the file to upload as the new photo.
// Returns True on success. // Returns True on success.
// See https://core.telegram.org/bots/api#setchatphoto // See https://core.telegram.org/bots/api#setchatphoto
func (api *API) SetChatPhoto(params SetChatPhotoP, photo UploaderFile) (bool, error) { func (api *API) SetChatPhoto(params SetChatPhoto, photo UploaderFile) (bool, error) {
uploader := NewUploader(api) uploader := NewUploader(api)
defer func() { defer func() {
_ = uploader.Close() _ = uploader.Close()
@@ -449,16 +449,16 @@ func (api *API) SetChatPhoto(params SetChatPhotoP, photo UploaderFile) (bool, er
return req.Do(uploader) return req.Do(uploader)
} }
// DeleteChatPhotoP holds parameters for the deleteChatPhoto method. // DeleteChatPhoto holds parameters for the deleteChatPhoto method.
// See https://core.telegram.org/bots/api#deletechatphoto // See https://core.telegram.org/bots/api#deletechatphoto
type DeleteChatPhotoP struct { type DeleteChatPhoto struct {
ChatID int64 `json:"chat_id"` ChatID int64 `json:"chat_id"`
} }
// DeleteChatPhoto deletes a chat photo. // DeleteChatPhoto deletes a chat photo.
// Returns True on success. // Returns True on success.
// See https://core.telegram.org/bots/api#deletechatphoto // See https://core.telegram.org/bots/api#deletechatphoto
func (api *API) DeleteChatPhoto(params DeleteChatPhotoP) (bool, error) { func (api *API) DeleteChatPhoto(params DeleteChatPhoto) (bool, error) {
req := NewRequestWithChatID[bool]("deleteChatPhoto", params, params.ChatID) req := NewRequestWithChatID[bool]("deleteChatPhoto", params, params.ChatID)
return req.Do(api) return req.Do(api)
} }
@@ -466,14 +466,14 @@ func (api *API) DeleteChatPhoto(params DeleteChatPhotoP) (bool, error) {
// DeleteChatPhotoWithContext is the context-aware variant of DeleteChatPhoto. // DeleteChatPhotoWithContext is the context-aware variant of DeleteChatPhoto.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#deletechatphoto // See https://core.telegram.org/bots/api#deletechatphoto
func (api *API) DeleteChatPhotoWithContext(ctx context.Context, params DeleteChatPhotoP) (bool, error) { func (api *API) DeleteChatPhotoWithContext(ctx context.Context, params DeleteChatPhoto) (bool, error) {
req := NewRequestWithChatID[bool]("deleteChatPhoto", params, params.ChatID) req := NewRequestWithChatID[bool]("deleteChatPhoto", params, params.ChatID)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// SetChatTitleP holds parameters for the setChatTitle method. // SetChatTitle holds parameters for the setChatTitle method.
// See https://core.telegram.org/bots/api#setchattitle // See https://core.telegram.org/bots/api#setchattitle
type SetChatTitleP struct { type SetChatTitle struct {
ChatID int64 `json:"chat_id"` ChatID int64 `json:"chat_id"`
Title string `json:"title"` Title string `json:"title"`
} }
@@ -481,7 +481,7 @@ type SetChatTitleP struct {
// SetChatTitle changes the chat title. // SetChatTitle changes the chat title.
// Returns True on success. // Returns True on success.
// See https://core.telegram.org/bots/api#setchattitle // See https://core.telegram.org/bots/api#setchattitle
func (api *API) SetChatTitle(params SetChatTitleP) (bool, error) { func (api *API) SetChatTitle(params SetChatTitle) (bool, error) {
req := NewRequestWithChatID[bool]("setChatTitle", params, params.ChatID) req := NewRequestWithChatID[bool]("setChatTitle", params, params.ChatID)
return req.Do(api) return req.Do(api)
} }
@@ -489,14 +489,14 @@ func (api *API) SetChatTitle(params SetChatTitleP) (bool, error) {
// SetChatTitleWithContext is the context-aware variant of SetChatTitle. // SetChatTitleWithContext is the context-aware variant of SetChatTitle.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#setchattitle // See https://core.telegram.org/bots/api#setchattitle
func (api *API) SetChatTitleWithContext(ctx context.Context, params SetChatTitleP) (bool, error) { func (api *API) SetChatTitleWithContext(ctx context.Context, params SetChatTitle) (bool, error) {
req := NewRequestWithChatID[bool]("setChatTitle", params, params.ChatID) req := NewRequestWithChatID[bool]("setChatTitle", params, params.ChatID)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// SetChatDescriptionP holds parameters for the setChatDescription method. // SetChatDescription holds parameters for the setChatDescription method.
// See https://core.telegram.org/bots/api#setchatdescription // See https://core.telegram.org/bots/api#setchatdescription
type SetChatDescriptionP struct { type SetChatDescription struct {
ChatID int64 `json:"chat_id"` ChatID int64 `json:"chat_id"`
Description string `json:"description"` Description string `json:"description"`
} }
@@ -504,7 +504,7 @@ type SetChatDescriptionP struct {
// SetChatDescription changes the chat description. // SetChatDescription changes the chat description.
// Returns True on success. // Returns True on success.
// See https://core.telegram.org/bots/api#setchatdescription // See https://core.telegram.org/bots/api#setchatdescription
func (api *API) SetChatDescription(params SetChatDescriptionP) (bool, error) { func (api *API) SetChatDescription(params SetChatDescription) (bool, error) {
req := NewRequestWithChatID[bool]("setChatDescription", params, params.ChatID) req := NewRequestWithChatID[bool]("setChatDescription", params, params.ChatID)
return req.Do(api) return req.Do(api)
} }
@@ -512,14 +512,14 @@ func (api *API) SetChatDescription(params SetChatDescriptionP) (bool, error) {
// SetChatDescriptionWithContext is the context-aware variant of SetChatDescription. // SetChatDescriptionWithContext is the context-aware variant of SetChatDescription.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#setchatdescription // See https://core.telegram.org/bots/api#setchatdescription
func (api *API) SetChatDescriptionWithContext(ctx context.Context, params SetChatDescriptionP) (bool, error) { func (api *API) SetChatDescriptionWithContext(ctx context.Context, params SetChatDescription) (bool, error) {
req := NewRequestWithChatID[bool]("setChatDescription", params, params.ChatID) req := NewRequestWithChatID[bool]("setChatDescription", params, params.ChatID)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// PinChatMessageP holds parameters for the pinChatMessage method. // PinChatMessage holds parameters for the pinChatMessage method.
// See https://core.telegram.org/bots/api#pinchatmessage // See https://core.telegram.org/bots/api#pinchatmessage
type PinChatMessageP struct { type PinChatMessage struct {
BusinessConnectionID *string `json:"business_connection_id,omitempty"` BusinessConnectionID *string `json:"business_connection_id,omitempty"`
ChatID int64 `json:"chat_id"` ChatID int64 `json:"chat_id"`
MessageID int `json:"message_id"` MessageID int `json:"message_id"`
@@ -529,7 +529,7 @@ type PinChatMessageP struct {
// PinChatMessage pins a message in a chat. // PinChatMessage pins a message in a chat.
// Returns True on success. // Returns True on success.
// See https://core.telegram.org/bots/api#pinchatmessage // See https://core.telegram.org/bots/api#pinchatmessage
func (api *API) PinChatMessage(params PinChatMessageP) (bool, error) { func (api *API) PinChatMessage(params PinChatMessage) (bool, error) {
req := NewRequestWithChatID[bool]("pinChatMessage", params, params.ChatID) req := NewRequestWithChatID[bool]("pinChatMessage", params, params.ChatID)
return req.Do(api) return req.Do(api)
} }
@@ -537,14 +537,14 @@ func (api *API) PinChatMessage(params PinChatMessageP) (bool, error) {
// PinChatMessageWithContext is the context-aware variant of PinChatMessage. // PinChatMessageWithContext is the context-aware variant of PinChatMessage.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#pinchatmessage // See https://core.telegram.org/bots/api#pinchatmessage
func (api *API) PinChatMessageWithContext(ctx context.Context, params PinChatMessageP) (bool, error) { func (api *API) PinChatMessageWithContext(ctx context.Context, params PinChatMessage) (bool, error) {
req := NewRequestWithChatID[bool]("pinChatMessage", params, params.ChatID) req := NewRequestWithChatID[bool]("pinChatMessage", params, params.ChatID)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// UnpinChatMessageP holds parameters for the unpinChatMessage method. // UnpinChatMessage holds parameters for the unpinChatMessage method.
// See https://core.telegram.org/bots/api#unpinchatmessage // See https://core.telegram.org/bots/api#unpinchatmessage
type UnpinChatMessageP struct { type UnpinChatMessage struct {
BusinessConnectionID *string `json:"business_connection_id,omitempty"` BusinessConnectionID *string `json:"business_connection_id,omitempty"`
ChatID int64 `json:"chat_id"` ChatID int64 `json:"chat_id"`
MessageID int `json:"message_id"` MessageID int `json:"message_id"`
@@ -553,7 +553,7 @@ type UnpinChatMessageP struct {
// UnpinChatMessage unpins a message in a chat. // UnpinChatMessage unpins a message in a chat.
// Returns True on success. // Returns True on success.
// See https://core.telegram.org/bots/api#unpinchatmessage // See https://core.telegram.org/bots/api#unpinchatmessage
func (api *API) UnpinChatMessage(params UnpinChatMessageP) (bool, error) { func (api *API) UnpinChatMessage(params UnpinChatMessage) (bool, error) {
req := NewRequestWithChatID[bool]("unpinChatMessage", params, params.ChatID) req := NewRequestWithChatID[bool]("unpinChatMessage", params, params.ChatID)
return req.Do(api) return req.Do(api)
} }
@@ -561,21 +561,21 @@ func (api *API) UnpinChatMessage(params UnpinChatMessageP) (bool, error) {
// UnpinChatMessageWithContext is the context-aware variant of UnpinChatMessage. // UnpinChatMessageWithContext is the context-aware variant of UnpinChatMessage.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#unpinchatmessage // See https://core.telegram.org/bots/api#unpinchatmessage
func (api *API) UnpinChatMessageWithContext(ctx context.Context, params UnpinChatMessageP) (bool, error) { func (api *API) UnpinChatMessageWithContext(ctx context.Context, params UnpinChatMessage) (bool, error) {
req := NewRequestWithChatID[bool]("unpinChatMessage", params, params.ChatID) req := NewRequestWithChatID[bool]("unpinChatMessage", params, params.ChatID)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// UnpinAllChatMessagesP holds parameters for the unpinAllChatMessages method. // UnpinAllChatMessages holds parameters for the unpinAllChatMessages method.
// See https://core.telegram.org/bots/api#unpinallchatmessages // See https://core.telegram.org/bots/api#unpinallchatmessages
type UnpinAllChatMessagesP struct { type UnpinAllChatMessages struct {
ChatID int64 `json:"chat_id"` ChatID int64 `json:"chat_id"`
} }
// UnpinAllChatMessages unpins all pinned messages in a chat. // UnpinAllChatMessages unpins all pinned messages in a chat.
// Returns True on success. // Returns True on success.
// See https://core.telegram.org/bots/api#unpinallchatmessages // See https://core.telegram.org/bots/api#unpinallchatmessages
func (api *API) UnpinAllChatMessages(params UnpinAllChatMessagesP) (bool, error) { func (api *API) UnpinAllChatMessages(params UnpinAllChatMessages) (bool, error) {
req := NewRequestWithChatID[bool]("unpinAllChatMessages", params, params.ChatID) req := NewRequestWithChatID[bool]("unpinAllChatMessages", params, params.ChatID)
return req.Do(api) return req.Do(api)
} }
@@ -583,21 +583,21 @@ func (api *API) UnpinAllChatMessages(params UnpinAllChatMessagesP) (bool, error)
// UnpinAllChatMessagesWithContext is the context-aware variant of UnpinAllChatMessages. // UnpinAllChatMessagesWithContext is the context-aware variant of UnpinAllChatMessages.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#unpinallchatmessages // See https://core.telegram.org/bots/api#unpinallchatmessages
func (api *API) UnpinAllChatMessagesWithContext(ctx context.Context, params UnpinAllChatMessagesP) (bool, error) { func (api *API) UnpinAllChatMessagesWithContext(ctx context.Context, params UnpinAllChatMessages) (bool, error) {
req := NewRequestWithChatID[bool]("unpinAllChatMessages", params, params.ChatID) req := NewRequestWithChatID[bool]("unpinAllChatMessages", params, params.ChatID)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// LeaveChatP holds parameters for the leaveChat method. // LeaveChat holds parameters for the leaveChat method.
// See https://core.telegram.org/bots/api#leavechat // See https://core.telegram.org/bots/api#leavechat
type LeaveChatP struct { type LeaveChat struct {
ChatID int64 `json:"chat_id"` ChatID int64 `json:"chat_id"`
} }
// LeaveChat makes the bot leave a chat. // LeaveChat makes the bot leave a chat.
// Returns True on success. // Returns True on success.
// See https://core.telegram.org/bots/api#leavechat // See https://core.telegram.org/bots/api#leavechat
func (api *API) LeaveChat(params LeaveChatP) (bool, error) { 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) // fixed method name
return req.Do(api) return req.Do(api)
} }
@@ -605,20 +605,20 @@ func (api *API) LeaveChat(params LeaveChatP) (bool, error) {
// LeaveChatWithContext is the context-aware variant of LeaveChat. // LeaveChatWithContext is the context-aware variant of LeaveChat.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#leavechat // See https://core.telegram.org/bots/api#leavechat
func (api *API) LeaveChatWithContext(ctx context.Context, params LeaveChatP) (bool, error) { 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) // fixed method name
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// GetChatP holds parameters for the getChat method. // GetChat holds parameters for the getChat method.
// See https://core.telegram.org/bots/api#getchat // See https://core.telegram.org/bots/api#getchat
type GetChatP struct { type GetChat struct {
ChatID int64 `json:"chat_id"` ChatID int64 `json:"chat_id"`
} }
// GetChat gets uptodate information about a chat. // GetChat gets uptodate information about a chat.
// See https://core.telegram.org/bots/api#getchat // See https://core.telegram.org/bots/api#getchat
func (api *API) GetChat(params GetChatP) (ChatFullInfo, error) { 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) // fixed method name
return req.Do(api) return req.Do(api)
} }
@@ -626,20 +626,20 @@ func (api *API) GetChat(params GetChatP) (ChatFullInfo, error) {
// GetChatWithContext is the context-aware variant of GetChat. // GetChatWithContext is the context-aware variant of GetChat.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#getchat // See https://core.telegram.org/bots/api#getchat
func (api *API) GetChatWithContext(ctx context.Context, params GetChatP) (ChatFullInfo, error) { 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) // fixed method name
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// GetChatAdministratorsP holds parameters for the getChatAdministrators method. // GetChatAdministrators holds parameters for the getChatAdministrators method.
// See https://core.telegram.org/bots/api#getchatadministrators // See https://core.telegram.org/bots/api#getchatadministrators
type GetChatAdministratorsP struct { type GetChatAdministrators struct {
ChatID int64 `json:"chat_id"` ChatID int64 `json:"chat_id"`
} }
// GetChatAdministrators returns a list of administrators in a chat. // GetChatAdministrators returns a list of administrators in a chat.
// See https://core.telegram.org/bots/api#getchatadministrators // See https://core.telegram.org/bots/api#getchatadministrators
func (api *API) GetChatAdministrators(params GetChatAdministratorsP) ([]ChatMember, error) { func (api *API) GetChatAdministrators(params GetChatAdministrators) ([]ChatMember, error) {
req := NewRequestWithChatID[[]ChatMember]("getChatAdministrators", params, params.ChatID) req := NewRequestWithChatID[[]ChatMember]("getChatAdministrators", params, params.ChatID)
return req.Do(api) return req.Do(api)
} }
@@ -647,20 +647,20 @@ func (api *API) GetChatAdministrators(params GetChatAdministratorsP) ([]ChatMemb
// GetChatAdministratorsWithContext is the context-aware variant of GetChatAdministrators. // GetChatAdministratorsWithContext is the context-aware variant of GetChatAdministrators.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#getchatadministrators // See https://core.telegram.org/bots/api#getchatadministrators
func (api *API) GetChatAdministratorsWithContext(ctx context.Context, params GetChatAdministratorsP) ([]ChatMember, error) { func (api *API) GetChatAdministratorsWithContext(ctx context.Context, params GetChatAdministrators) ([]ChatMember, error) {
req := NewRequestWithChatID[[]ChatMember]("getChatAdministrators", params, params.ChatID) req := NewRequestWithChatID[[]ChatMember]("getChatAdministrators", params, params.ChatID)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// GetChatMembersCountP holds parameters for the getChatMemberCount method. // GetChatMembersCount holds parameters for the getChatMemberCount method.
// See https://core.telegram.org/bots/api#getchatmembercount // See https://core.telegram.org/bots/api#getchatmembercount
type GetChatMembersCountP struct { type GetChatMembersCount struct {
ChatID int64 `json:"chat_id"` ChatID int64 `json:"chat_id"`
} }
// GetChatMemberCount returns the number of members in a chat. // GetChatMemberCount returns the number of members in a chat.
// See https://core.telegram.org/bots/api#getchatmembercount // See https://core.telegram.org/bots/api#getchatmembercount
func (api *API) GetChatMemberCount(params GetChatMembersCountP) (int, error) { func (api *API) GetChatMemberCount(params GetChatMembersCount) (int, error) {
req := NewRequestWithChatID[int]("getChatMemberCount", params, params.ChatID) req := NewRequestWithChatID[int]("getChatMemberCount", params, params.ChatID)
return req.Do(api) return req.Do(api)
} }
@@ -668,21 +668,21 @@ func (api *API) GetChatMemberCount(params GetChatMembersCountP) (int, error) {
// GetChatMemberCountWithContext is the context-aware variant of GetChatMemberCount. // GetChatMemberCountWithContext is the context-aware variant of GetChatMemberCount.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#getchatmembercount // See https://core.telegram.org/bots/api#getchatmembercount
func (api *API) GetChatMemberCountWithContext(ctx context.Context, params GetChatMembersCountP) (int, error) { func (api *API) GetChatMemberCountWithContext(ctx context.Context, params GetChatMembersCount) (int, error) {
req := NewRequestWithChatID[int]("getChatMemberCount", params, params.ChatID) req := NewRequestWithChatID[int]("getChatMemberCount", params, params.ChatID)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// GetChatMemberP holds parameters for the getChatMember method. // GetChatMember holds parameters for the getChatMember method.
// See https://core.telegram.org/bots/api#getchatmember // See https://core.telegram.org/bots/api#getchatmember
type GetChatMemberP struct { type GetChatMember struct {
ChatID int64 `json:"chat_id"` ChatID int64 `json:"chat_id"`
UserID int64 `json:"user_id"` UserID int64 `json:"user_id"`
} }
// GetChatMember returns information about a member of a chat. // GetChatMember returns information about a member of a chat.
// See https://core.telegram.org/bots/api#getchatmember // See https://core.telegram.org/bots/api#getchatmember
func (api *API) GetChatMember(params GetChatMemberP) (ChatMember, error) { func (api *API) GetChatMember(params GetChatMember) (ChatMember, error) {
req := NewRequestWithChatID[ChatMember]("getChatMember", params, params.ChatID) req := NewRequestWithChatID[ChatMember]("getChatMember", params, params.ChatID)
return req.Do(api) return req.Do(api)
} }
@@ -690,14 +690,14 @@ func (api *API) GetChatMember(params GetChatMemberP) (ChatMember, error) {
// GetChatMemberWithContext is the context-aware variant of GetChatMember. // GetChatMemberWithContext is the context-aware variant of GetChatMember.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#getchatmember // See https://core.telegram.org/bots/api#getchatmember
func (api *API) GetChatMemberWithContext(ctx context.Context, params GetChatMemberP) (ChatMember, error) { func (api *API) GetChatMemberWithContext(ctx context.Context, params GetChatMember) (ChatMember, error) {
req := NewRequestWithChatID[ChatMember]("getChatMember", params, params.ChatID) req := NewRequestWithChatID[ChatMember]("getChatMember", params, params.ChatID)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// SetChatStickerSetP holds parameters for the setChatStickerSet method. // SetChatStickerSet holds parameters for the setChatStickerSet method.
// See https://core.telegram.org/bots/api#setchatstickerset // See https://core.telegram.org/bots/api#setchatstickerset
type SetChatStickerSetP struct { type SetChatStickerSet struct {
ChatID int64 `json:"chat_id"` ChatID int64 `json:"chat_id"`
StickerSetName string `json:"sticker_set_name"` StickerSetName string `json:"sticker_set_name"`
} }
@@ -705,7 +705,7 @@ type SetChatStickerSetP struct {
// SetChatStickerSet associates a sticker set with a supergroup. // SetChatStickerSet associates a sticker set with a supergroup.
// Returns True on success. // Returns True on success.
// See https://core.telegram.org/bots/api#setchatstickerset // See https://core.telegram.org/bots/api#setchatstickerset
func (api *API) SetChatStickerSet(params SetChatStickerSetP) (bool, error) { func (api *API) SetChatStickerSet(params SetChatStickerSet) (bool, error) {
req := NewRequestWithChatID[bool]("setChatStickerSet", params, params.ChatID) req := NewRequestWithChatID[bool]("setChatStickerSet", params, params.ChatID)
return req.Do(api) return req.Do(api)
} }
@@ -713,21 +713,21 @@ func (api *API) SetChatStickerSet(params SetChatStickerSetP) (bool, error) {
// SetChatStickerSetWithContext is the context-aware variant of SetChatStickerSet. // SetChatStickerSetWithContext is the context-aware variant of SetChatStickerSet.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#setchatstickerset // See https://core.telegram.org/bots/api#setchatstickerset
func (api *API) SetChatStickerSetWithContext(ctx context.Context, params SetChatStickerSetP) (bool, error) { func (api *API) SetChatStickerSetWithContext(ctx context.Context, params SetChatStickerSet) (bool, error) {
req := NewRequestWithChatID[bool]("setChatStickerSet", params, params.ChatID) req := NewRequestWithChatID[bool]("setChatStickerSet", params, params.ChatID)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// DeleteChatStickerSetP holds parameters for the deleteChatStickerSet method. // DeleteChatStickerSet holds parameters for the deleteChatStickerSet method.
// See https://core.telegram.org/bots/api#deletechatstickerset // See https://core.telegram.org/bots/api#deletechatstickerset
type DeleteChatStickerSetP struct { type DeleteChatStickerSet struct {
ChatID int64 `json:"chat_id"` ChatID int64 `json:"chat_id"`
} }
// DeleteChatStickerSet deletes a sticker set from a supergroup. // DeleteChatStickerSet deletes a sticker set from a supergroup.
// Returns True on success. // Returns True on success.
// See https://core.telegram.org/bots/api#deletechatstickerset // See https://core.telegram.org/bots/api#deletechatstickerset
func (api *API) DeleteChatStickerSet(params DeleteChatStickerSetP) (bool, error) { func (api *API) DeleteChatStickerSet(params DeleteChatStickerSet) (bool, error) {
req := NewRequestWithChatID[bool]("deleteChatStickerSet", params, params.ChatID) req := NewRequestWithChatID[bool]("deleteChatStickerSet", params, params.ChatID)
return req.Do(api) return req.Do(api)
} }
@@ -735,21 +735,21 @@ func (api *API) DeleteChatStickerSet(params DeleteChatStickerSetP) (bool, error)
// DeleteChatStickerSetWithContext is the context-aware variant of DeleteChatStickerSet. // DeleteChatStickerSetWithContext is the context-aware variant of DeleteChatStickerSet.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#deletechatstickerset // See https://core.telegram.org/bots/api#deletechatstickerset
func (api *API) DeleteChatStickerSetWithContext(ctx context.Context, params DeleteChatStickerSetP) (bool, error) { func (api *API) DeleteChatStickerSetWithContext(ctx context.Context, params DeleteChatStickerSet) (bool, error) {
req := NewRequestWithChatID[bool]("deleteChatStickerSet", params, params.ChatID) req := NewRequestWithChatID[bool]("deleteChatStickerSet", params, params.ChatID)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// GetUserChatBoostsP holds parameters for the getUserChatBoosts method. // GetUserChatBoosts holds parameters for the getUserChatBoosts method.
// See https://core.telegram.org/bots/api#getuserchatboosts // See https://core.telegram.org/bots/api#getuserchatboosts
type GetUserChatBoostsP struct { type GetUserChatBoosts struct {
ChatID int64 `json:"chat_id"` ChatID int64 `json:"chat_id"`
UserID int64 `json:"user_id"` UserID int64 `json:"user_id"`
} }
// GetUserChatBoosts returns the list of boosts a user has given to a chat. // GetUserChatBoosts returns the list of boosts a user has given to a chat.
// See https://core.telegram.org/bots/api#getuserchatboosts // See https://core.telegram.org/bots/api#getuserchatboosts
func (api *API) GetUserChatBoosts(params GetUserChatBoostsP) (UserChatBoosts, error) { func (api *API) GetUserChatBoosts(params GetUserChatBoosts) (UserChatBoosts, error) {
req := NewRequestWithChatID[UserChatBoosts]("getUserChatBoosts", params, params.ChatID) req := NewRequestWithChatID[UserChatBoosts]("getUserChatBoosts", params, params.ChatID)
return req.Do(api) return req.Do(api)
} }
@@ -757,14 +757,14 @@ func (api *API) GetUserChatBoosts(params GetUserChatBoostsP) (UserChatBoosts, er
// GetUserChatBoostsWithContext is the context-aware variant of GetUserChatBoosts. // GetUserChatBoostsWithContext is the context-aware variant of GetUserChatBoosts.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#getuserchatboosts // See https://core.telegram.org/bots/api#getuserchatboosts
func (api *API) GetUserChatBoostsWithContext(ctx context.Context, params GetUserChatBoostsP) (UserChatBoosts, error) { func (api *API) GetUserChatBoostsWithContext(ctx context.Context, params GetUserChatBoosts) (UserChatBoosts, error) {
req := NewRequestWithChatID[UserChatBoosts]("getUserChatBoosts", params, params.ChatID) req := NewRequestWithChatID[UserChatBoosts]("getUserChatBoosts", params, params.ChatID)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// GetChatGiftsP holds parameters for the getChatGifts method. // GetChatGifts holds parameters for the getChatGifts method.
// See https://core.telegram.org/bots/api#getchatgifts // See https://core.telegram.org/bots/api#getchatgifts
type GetChatGiftsP struct { type GetChatGifts struct {
ChatID int64 `json:"chat_id"` ChatID int64 `json:"chat_id"`
ExcludeUnsaved bool `json:"exclude_unsaved,omitempty"` ExcludeUnsaved bool `json:"exclude_unsaved,omitempty"`
ExcludeSaved bool `json:"exclude_saved,omitempty"` ExcludeSaved bool `json:"exclude_saved,omitempty"`
@@ -780,7 +780,7 @@ type GetChatGiftsP struct {
// GetChatGifts returns gifts owned by a chat. // GetChatGifts returns gifts owned by a chat.
// See https://core.telegram.org/bots/api#getchatgifts // See https://core.telegram.org/bots/api#getchatgifts
func (api *API) GetChatGifts(params GetChatGiftsP) (OwnedGifts, error) { func (api *API) GetChatGifts(params GetChatGifts) (OwnedGifts, error) {
req := NewRequestWithChatID[OwnedGifts]("getChatGifts", params, params.ChatID) req := NewRequestWithChatID[OwnedGifts]("getChatGifts", params, params.ChatID)
return req.Do(api) return req.Do(api)
} }
@@ -788,7 +788,7 @@ func (api *API) GetChatGifts(params GetChatGiftsP) (OwnedGifts, error) {
// GetChatGiftsWithContext is the context-aware variant of GetChatGifts. // GetChatGiftsWithContext is the context-aware variant of GetChatGifts.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#getchatgifts // See https://core.telegram.org/bots/api#getchatgifts
func (api *API) GetChatGiftsWithContext(ctx context.Context, params GetChatGiftsP) (OwnedGifts, error) { func (api *API) GetChatGiftsWithContext(ctx context.Context, params GetChatGifts) (OwnedGifts, error) {
req := NewRequestWithChatID[OwnedGifts]("getChatGifts", params, params.ChatID) req := NewRequestWithChatID[OwnedGifts]("getChatGifts", params, params.ChatID)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
+50 -19
View File
@@ -3,24 +3,28 @@ package tgapi
// Chat represents a chat (private, group, supergroup, channel). // Chat represents a chat (private, group, supergroup, channel).
// See https://core.telegram.org/bots/api#chat // See https://core.telegram.org/bots/api#chat
type Chat struct { type Chat struct {
ID int64 `json:"id"` ID int64 `json:"id"`
Type string `json:"type"` Type ChatType `json:"type"`
Title *string `json:"title,omitempty"` Title *string `json:"title,omitempty"`
Username *string `json:"username,omitempty"` Username *string `json:"username,omitempty"`
FirstName *string `json:"first_name,omitempty"` FirstName *string `json:"first_name,omitempty"`
LastName *string `json:"last_name,omitempty"` LastName *string `json:"last_name,omitempty"`
IsForum *bool `json:"is_forum,omitempty"` IsForum *bool `json:"is_forum,omitempty"`
IsDirectMessages *bool `json:"is_direct_messages,omitempty"` IsDirectMessages *bool `json:"is_direct_messages,omitempty"`
} }
// ChatType represents the type of a chat. // ChatType represents the type of a chat.
type ChatType string type ChatType string
const ( const (
ChatTypePrivate ChatType = "private" // ChatTypePrivate identifies a private chat.
ChatTypeGroup ChatType = "group" ChatTypePrivate ChatType = "private"
// ChatTypeGroup identifies a basic group chat.
ChatTypeGroup ChatType = "group"
// ChatTypeSupergroup identifies a supergroup chat.
ChatTypeSupergroup ChatType = "supergroup" ChatTypeSupergroup ChatType = "supergroup"
ChatTypeChannel ChatType = "channel" // ChatTypeChannel identifies a channel chat.
ChatTypeChannel ChatType = "channel"
) )
// ChatFullInfo contains full information about a chat. // ChatFullInfo contains full information about a chat.
@@ -103,11 +107,12 @@ type ChatPermissions struct {
CanSendAudios bool `json:"can_send_audios"` CanSendAudios bool `json:"can_send_audios"`
CanSendDocuments bool `json:"can_send_documents"` CanSendDocuments bool `json:"can_send_documents"`
CanSendPhotos bool `json:"can_send_photos"` CanSendPhotos bool `json:"can_send_photos"`
CanSendVideos bool `json:"can_send_videos"`
CanSendVideoNotes bool `json:"can_send_video_notes"` CanSendVideoNotes bool `json:"can_send_video_notes"`
CanSendVoiceNotes bool `json:"can_send_voice_notes"` CanSendVoiceNotes bool `json:"can_send_voice_notes"`
CanSendPolls bool `json:"can_send_polls"` CanSendPolls bool `json:"can_send_polls"`
CanSendOtherMessages bool `json:"can_send_other_messages"` CanSendOtherMessages bool `json:"can_send_other_messages"`
CanAddWebPagePreview bool `json:"can_add_web_page_preview"` CanAddWebPagePreview bool `json:"can_add_web_page_previews"`
CanEditTag bool `json:"can_edit_tag"` CanEditTag bool `json:"can_edit_tag"`
CanChangeInfo bool `json:"can_change_info"` CanChangeInfo bool `json:"can_change_info"`
CanInviteUsers bool `json:"can_invite_users"` CanInviteUsers bool `json:"can_invite_users"`
@@ -143,12 +148,18 @@ type ChatInviteLink struct {
type ChatMemberStatusType string type ChatMemberStatusType string
const ( const (
ChatMemberStatusOwner ChatMemberStatusType = "owner" // ChatMemberStatusOwner identifies a chat owner.
ChatMemberStatusOwner ChatMemberStatusType = "owner"
// ChatMemberStatusAdministrator identifies a chat administrator.
ChatMemberStatusAdministrator ChatMemberStatusType = "administrator" ChatMemberStatusAdministrator ChatMemberStatusType = "administrator"
ChatMemberStatusMember ChatMemberStatusType = "member" // ChatMemberStatusMember identifies a regular member.
ChatMemberStatusRestricted ChatMemberStatusType = "restricted" ChatMemberStatusMember ChatMemberStatusType = "member"
ChatMemberStatusLeft ChatMemberStatusType = "left" // ChatMemberStatusRestricted identifies a restricted member.
ChatMemberStatusBanned ChatMemberStatusType = "kicked" ChatMemberStatusRestricted ChatMemberStatusType = "restricted"
// ChatMemberStatusLeft identifies a user who left the chat.
ChatMemberStatusLeft ChatMemberStatusType = "left"
// ChatMemberStatusBanned identifies a banned user.
ChatMemberStatusBanned ChatMemberStatusType = "kicked"
) )
// ChatMember contains information about one member of a chat. // ChatMember contains information about one member of a chat.
@@ -190,12 +201,13 @@ type ChatMember struct {
CanSendMessages *bool `json:"can_send_messages,omitempty"` CanSendMessages *bool `json:"can_send_messages,omitempty"`
CanSendAudios *bool `json:"can_send_audios,omitempty"` CanSendAudios *bool `json:"can_send_audios,omitempty"`
CanSendDocuments *bool `json:"can_send_documents,omitempty"` CanSendDocuments *bool `json:"can_send_documents,omitempty"`
CanSendPhotos *bool `json:"can_send_photos,omitempty"`
CanSendVideos *bool `json:"can_send_videos,omitempty"` CanSendVideos *bool `json:"can_send_videos,omitempty"`
CanSendVideoNotes *bool `json:"can_send_video_notes,omitempty"` CanSendVideoNotes *bool `json:"can_send_video_notes,omitempty"`
CanSendVoiceNotes *bool `json:"can_send_voice_notes,omitempty"` CanSendVoiceNotes *bool `json:"can_send_voice_notes,omitempty"`
CanSendPolls *bool `json:"can_send_polls,omitempty"` CanSendPolls *bool `json:"can_send_polls,omitempty"`
CanSendOtherMessages *bool `json:"can_send_other_messages,omitempty"` CanSendOtherMessages *bool `json:"can_send_other_messages,omitempty"`
CanAddWebPagePreview *bool `json:"can_add_web_page_preview,omitempty"` CanAddWebPagePreview *bool `json:"can_add_web_page_previews,omitempty"`
CanEditTag *bool `json:"can_edit_tag,omitempty"` CanEditTag *bool `json:"can_edit_tag,omitempty"`
} }
@@ -214,7 +226,7 @@ type ChatBoostSource struct {
// ChatBoost represents a boost added to a chat. // ChatBoost represents a boost added to a chat.
// See https://core.telegram.org/bots/api#chatboost // See https://core.telegram.org/bots/api#chatboost
type ChatBoost struct { type ChatBoost struct {
BoostID int `json:"boost_id"` BoostID string `json:"boost_id"`
AddDate int `json:"add_date"` AddDate int `json:"add_date"`
ExpirationDate int `json:"expiration_date"` ExpirationDate int `json:"expiration_date"`
Source ChatBoostSource `json:"source"` Source ChatBoostSource `json:"source"`
@@ -225,6 +237,25 @@ type ChatBoost struct {
type UserChatBoosts struct { type UserChatBoosts struct {
Boosts []ChatBoost `json:"boosts"` Boosts []ChatBoost `json:"boosts"`
} }
type ChatBoostAdded struct {
BoostCount int `json:"boost_count"`
}
type ChatBackground struct {
Type BackgroundType `json:"type"`
}
// ChatOwnerLeft describes a service message about a chat owner leaving.
// 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.
// 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. // ChatAdministratorRights represents the rights of an administrator in a chat.
// See https://core.telegram.org/bots/api#chatadministratorrights // See https://core.telegram.org/bots/api#chatadministratorrights
+7
View File
@@ -2,7 +2,14 @@ package tgapi
import "errors" import "errors"
// ErrRateLimit reports that a request exceeded the configured rate limiter.
var ErrRateLimit = errors.New("rate limit exceeded") var ErrRateLimit = errors.New("rate limit exceeded")
// ErrPoolUnexpected reports an unexpected result type returned from the worker pool.
var ErrPoolUnexpected = errors.New("unexpected response from pool") var ErrPoolUnexpected = errors.New("unexpected response from pool")
// ErrPoolQueueFull reports that the internal request queue is full.
var ErrPoolQueueFull = errors.New("worker pool queue full") 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") var ErrPoolStopped = errors.New("worker pool stopped")
+35 -35
View File
@@ -2,8 +2,8 @@ package tgapi
import "context" import "context"
// BaseForumTopicP contains common fields for forum topic operations that require a chat ID and a message thread ID. // BaseForumTopic contains common fields for forum topic operations that require a chat ID and a message thread ID.
type BaseForumTopicP struct { type BaseForumTopic struct {
ChatID int64 `json:"chat_id"` ChatID int64 `json:"chat_id"`
MessageThreadID int `json:"message_thread_id"` MessageThreadID int `json:"message_thread_id"`
} }
@@ -23,9 +23,9 @@ func (api *API) GetForumTopicIconStickersWithContext(ctx context.Context) ([]Sti
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// CreateForumTopicP holds parameters for the createForumTopic method. // CreateForumTopic holds parameters for the createForumTopic method.
// See https://core.telegram.org/bots/api#createforumtopic // See https://core.telegram.org/bots/api#createforumtopic
type CreateForumTopicP struct { type CreateForumTopic struct {
ChatID int64 `json:"chat_id"` ChatID int64 `json:"chat_id"`
Name string `json:"name"` Name string `json:"name"`
IconColor ForumTopicIconColor `json:"icon_color"` IconColor ForumTopicIconColor `json:"icon_color"`
@@ -35,7 +35,7 @@ type CreateForumTopicP struct {
// CreateForumTopic creates a topic in a forum supergroup. // CreateForumTopic creates a topic in a forum supergroup.
// Returns the created ForumTopic on success. // Returns the created ForumTopic on success.
// See https://core.telegram.org/bots/api#createforumtopic // See https://core.telegram.org/bots/api#createforumtopic
func (api *API) CreateForumTopic(params CreateForumTopicP) (ForumTopic, error) { func (api *API) CreateForumTopic(params CreateForumTopic) (ForumTopic, error) {
req := NewRequestWithChatID[ForumTopic]("createForumTopic", params, params.ChatID) req := NewRequestWithChatID[ForumTopic]("createForumTopic", params, params.ChatID)
return req.Do(api) return req.Do(api)
} }
@@ -43,15 +43,15 @@ func (api *API) CreateForumTopic(params CreateForumTopicP) (ForumTopic, error) {
// CreateForumTopicWithContext is the context-aware variant of CreateForumTopic. // CreateForumTopicWithContext is the context-aware variant of CreateForumTopic.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#createforumtopic // See https://core.telegram.org/bots/api#createforumtopic
func (api *API) CreateForumTopicWithContext(ctx context.Context, params CreateForumTopicP) (ForumTopic, error) { func (api *API) CreateForumTopicWithContext(ctx context.Context, params CreateForumTopic) (ForumTopic, error) {
req := NewRequestWithChatID[ForumTopic]("createForumTopic", params, params.ChatID) req := NewRequestWithChatID[ForumTopic]("createForumTopic", params, params.ChatID)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// EditForumTopicP holds parameters for the editForumTopic method. // EditForumTopic holds parameters for the editForumTopic method.
// See https://core.telegram.org/bots/api#editforumtopic // See https://core.telegram.org/bots/api#editforumtopic
type EditForumTopicP struct { type EditForumTopic struct {
BaseForumTopicP BaseForumTopic
Name string `json:"name"` Name string `json:"name"`
IconCustomEmojiID string `json:"icon_custom_emoji_id"` IconCustomEmojiID string `json:"icon_custom_emoji_id"`
} }
@@ -59,7 +59,7 @@ type EditForumTopicP struct {
// EditForumTopic edits name and icon of a forum topic. // EditForumTopic edits name and icon of a forum topic.
// Returns True on success. // Returns True on success.
// See https://core.telegram.org/bots/api#editforumtopic // See https://core.telegram.org/bots/api#editforumtopic
func (api *API) EditForumTopic(params EditForumTopicP) (bool, error) { func (api *API) EditForumTopic(params EditForumTopic) (bool, error) {
req := NewRequestWithChatID[bool]("editForumTopic", params, params.ChatID) req := NewRequestWithChatID[bool]("editForumTopic", params, params.ChatID)
return req.Do(api) return req.Do(api)
} }
@@ -67,7 +67,7 @@ func (api *API) EditForumTopic(params EditForumTopicP) (bool, error) {
// EditForumTopicWithContext is the context-aware variant of EditForumTopic. // EditForumTopicWithContext is the context-aware variant of EditForumTopic.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#editforumtopic // See https://core.telegram.org/bots/api#editforumtopic
func (api *API) EditForumTopicWithContext(ctx context.Context, params EditForumTopicP) (bool, error) { func (api *API) EditForumTopicWithContext(ctx context.Context, params EditForumTopic) (bool, error) {
req := NewRequestWithChatID[bool]("editForumTopic", params, params.ChatID) req := NewRequestWithChatID[bool]("editForumTopic", params, params.ChatID)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
@@ -75,7 +75,7 @@ func (api *API) EditForumTopicWithContext(ctx context.Context, params EditForumT
// CloseForumTopic closes an open forum topic. // CloseForumTopic closes an open forum topic.
// Returns True on success. // Returns True on success.
// See https://core.telegram.org/bots/api#closeforumtopic // See https://core.telegram.org/bots/api#closeforumtopic
func (api *API) CloseForumTopic(params BaseForumTopicP) (bool, error) { func (api *API) CloseForumTopic(params BaseForumTopic) (bool, error) {
req := NewRequestWithChatID[bool]("closeForumTopic", params, params.ChatID) req := NewRequestWithChatID[bool]("closeForumTopic", params, params.ChatID)
return req.Do(api) return req.Do(api)
} }
@@ -83,7 +83,7 @@ func (api *API) CloseForumTopic(params BaseForumTopicP) (bool, error) {
// CloseForumTopicWithContext is the context-aware variant of CloseForumTopic. // CloseForumTopicWithContext is the context-aware variant of CloseForumTopic.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#closeforumtopic // See https://core.telegram.org/bots/api#closeforumtopic
func (api *API) CloseForumTopicWithContext(ctx context.Context, params BaseForumTopicP) (bool, error) { func (api *API) CloseForumTopicWithContext(ctx context.Context, params BaseForumTopic) (bool, error) {
req := NewRequestWithChatID[bool]("closeForumTopic", params, params.ChatID) req := NewRequestWithChatID[bool]("closeForumTopic", params, params.ChatID)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
@@ -91,7 +91,7 @@ func (api *API) CloseForumTopicWithContext(ctx context.Context, params BaseForum
// ReopenForumTopic reopens a closed forum topic. // ReopenForumTopic reopens a closed forum topic.
// Returns True on success. // Returns True on success.
// See https://core.telegram.org/bots/api#reopenforumtopic // See https://core.telegram.org/bots/api#reopenforumtopic
func (api *API) ReopenForumTopic(params BaseForumTopicP) (bool, error) { func (api *API) ReopenForumTopic(params BaseForumTopic) (bool, error) {
req := NewRequestWithChatID[bool]("reopenForumTopic", params, params.ChatID) req := NewRequestWithChatID[bool]("reopenForumTopic", params, params.ChatID)
return req.Do(api) return req.Do(api)
} }
@@ -99,7 +99,7 @@ func (api *API) ReopenForumTopic(params BaseForumTopicP) (bool, error) {
// ReopenForumTopicWithContext is the context-aware variant of ReopenForumTopic. // ReopenForumTopicWithContext is the context-aware variant of ReopenForumTopic.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#reopenforumtopic // See https://core.telegram.org/bots/api#reopenforumtopic
func (api *API) ReopenForumTopicWithContext(ctx context.Context, params BaseForumTopicP) (bool, error) { func (api *API) ReopenForumTopicWithContext(ctx context.Context, params BaseForumTopic) (bool, error) {
req := NewRequestWithChatID[bool]("reopenForumTopic", params, params.ChatID) req := NewRequestWithChatID[bool]("reopenForumTopic", params, params.ChatID)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
@@ -107,7 +107,7 @@ func (api *API) ReopenForumTopicWithContext(ctx context.Context, params BaseForu
// DeleteForumTopic deletes a forum topic. // DeleteForumTopic deletes a forum topic.
// Returns True on success. // Returns True on success.
// See https://core.telegram.org/bots/api#deleteforumtopic // See https://core.telegram.org/bots/api#deleteforumtopic
func (api *API) DeleteForumTopic(params BaseForumTopicP) (bool, error) { func (api *API) DeleteForumTopic(params BaseForumTopic) (bool, error) {
req := NewRequestWithChatID[bool]("deleteForumTopic", params, params.ChatID) req := NewRequestWithChatID[bool]("deleteForumTopic", params, params.ChatID)
return req.Do(api) return req.Do(api)
} }
@@ -115,7 +115,7 @@ func (api *API) DeleteForumTopic(params BaseForumTopicP) (bool, error) {
// DeleteForumTopicWithContext is the context-aware variant of DeleteForumTopic. // DeleteForumTopicWithContext is the context-aware variant of DeleteForumTopic.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#deleteforumtopic // See https://core.telegram.org/bots/api#deleteforumtopic
func (api *API) DeleteForumTopicWithContext(ctx context.Context, params BaseForumTopicP) (bool, error) { func (api *API) DeleteForumTopicWithContext(ctx context.Context, params BaseForumTopic) (bool, error) {
req := NewRequestWithChatID[bool]("deleteForumTopic", params, params.ChatID) req := NewRequestWithChatID[bool]("deleteForumTopic", params, params.ChatID)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
@@ -123,7 +123,7 @@ func (api *API) DeleteForumTopicWithContext(ctx context.Context, params BaseForu
// UnpinAllForumTopicMessages clears the list of pinned messages in a forum topic. // UnpinAllForumTopicMessages clears the list of pinned messages in a forum topic.
// Returns True on success. // Returns True on success.
// See https://core.telegram.org/bots/api#unpinallforumtopicmessages // See https://core.telegram.org/bots/api#unpinallforumtopicmessages
func (api *API) UnpinAllForumTopicMessages(params BaseForumTopicP) (bool, error) { func (api *API) UnpinAllForumTopicMessages(params BaseForumTopic) (bool, error) {
req := NewRequestWithChatID[bool]("unpinAllForumTopicMessages", params, params.ChatID) req := NewRequestWithChatID[bool]("unpinAllForumTopicMessages", params, params.ChatID)
return req.Do(api) return req.Do(api)
} }
@@ -131,19 +131,19 @@ func (api *API) UnpinAllForumTopicMessages(params BaseForumTopicP) (bool, error)
// UnpinAllForumTopicMessagesWithContext is the context-aware variant of UnpinAllForumTopicMessages. // UnpinAllForumTopicMessagesWithContext is the context-aware variant of UnpinAllForumTopicMessages.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#unpinallforumtopicmessages // See https://core.telegram.org/bots/api#unpinallforumtopicmessages
func (api *API) UnpinAllForumTopicMessagesWithContext(ctx context.Context, params BaseForumTopicP) (bool, error) { func (api *API) UnpinAllForumTopicMessagesWithContext(ctx context.Context, params BaseForumTopic) (bool, error) {
req := NewRequestWithChatID[bool]("unpinAllForumTopicMessages", params, params.ChatID) req := NewRequestWithChatID[bool]("unpinAllForumTopicMessages", params, params.ChatID)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// BaseGeneralForumTopicP contains common fields for general forum topic operations that require a chat ID. // BaseGeneralForumTopic contains common fields for general forum topic operations that require a chat ID.
type BaseGeneralForumTopicP struct { type BaseGeneralForumTopic struct {
ChatID int64 `json:"chat_id"` ChatID int64 `json:"chat_id"`
} }
// EditGeneralForumTopicP holds parameters for the editGeneralForumTopic method. // EditGeneralForumTopic holds parameters for the editGeneralForumTopic method.
// See https://core.telegram.org/bots/api#editgeneralforumtopic // See https://core.telegram.org/bots/api#editgeneralforumtopic
type EditGeneralForumTopicP struct { type EditGeneralForumTopic struct {
ChatID int64 `json:"chat_id"` ChatID int64 `json:"chat_id"`
Name string `json:"name"` Name string `json:"name"`
} }
@@ -151,7 +151,7 @@ type EditGeneralForumTopicP struct {
// EditGeneralForumTopic edits the name of the 'General' topic in a forum supergroup. // EditGeneralForumTopic edits the name of the 'General' topic in a forum supergroup.
// Returns True on success. // Returns True on success.
// See https://core.telegram.org/bots/api#editgeneralforumtopic // See https://core.telegram.org/bots/api#editgeneralforumtopic
func (api *API) EditGeneralForumTopic(params EditGeneralForumTopicP) (bool, error) { func (api *API) EditGeneralForumTopic(params EditGeneralForumTopic) (bool, error) {
req := NewRequestWithChatID[bool]("editGeneralForumTopic", params, params.ChatID) req := NewRequestWithChatID[bool]("editGeneralForumTopic", params, params.ChatID)
return req.Do(api) return req.Do(api)
} }
@@ -159,7 +159,7 @@ func (api *API) EditGeneralForumTopic(params EditGeneralForumTopicP) (bool, erro
// EditGeneralForumTopicWithContext is the context-aware variant of EditGeneralForumTopic. // EditGeneralForumTopicWithContext is the context-aware variant of EditGeneralForumTopic.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#editgeneralforumtopic // See https://core.telegram.org/bots/api#editgeneralforumtopic
func (api *API) EditGeneralForumTopicWithContext(ctx context.Context, params EditGeneralForumTopicP) (bool, error) { func (api *API) EditGeneralForumTopicWithContext(ctx context.Context, params EditGeneralForumTopic) (bool, error) {
req := NewRequestWithChatID[bool]("editGeneralForumTopic", params, params.ChatID) req := NewRequestWithChatID[bool]("editGeneralForumTopic", params, params.ChatID)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
@@ -167,7 +167,7 @@ func (api *API) EditGeneralForumTopicWithContext(ctx context.Context, params Edi
// CloseGeneralForumTopic closes the 'General' topic in a forum supergroup. // CloseGeneralForumTopic closes the 'General' topic in a forum supergroup.
// Returns True on success. // Returns True on success.
// See https://core.telegram.org/bots/api#closegeneralforumtopic // See https://core.telegram.org/bots/api#closegeneralforumtopic
func (api *API) CloseGeneralForumTopic(params BaseGeneralForumTopicP) (bool, error) { func (api *API) CloseGeneralForumTopic(params BaseGeneralForumTopic) (bool, error) {
req := NewRequestWithChatID[bool]("closeGeneralForumTopic", params, params.ChatID) req := NewRequestWithChatID[bool]("closeGeneralForumTopic", params, params.ChatID)
return req.Do(api) return req.Do(api)
} }
@@ -175,7 +175,7 @@ func (api *API) CloseGeneralForumTopic(params BaseGeneralForumTopicP) (bool, err
// CloseGeneralForumTopicWithContext is the context-aware variant of CloseGeneralForumTopic. // CloseGeneralForumTopicWithContext is the context-aware variant of CloseGeneralForumTopic.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#closegeneralforumtopic // See https://core.telegram.org/bots/api#closegeneralforumtopic
func (api *API) CloseGeneralForumTopicWithContext(ctx context.Context, params BaseGeneralForumTopicP) (bool, error) { func (api *API) CloseGeneralForumTopicWithContext(ctx context.Context, params BaseGeneralForumTopic) (bool, error) {
req := NewRequestWithChatID[bool]("closeGeneralForumTopic", params, params.ChatID) req := NewRequestWithChatID[bool]("closeGeneralForumTopic", params, params.ChatID)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
@@ -183,7 +183,7 @@ func (api *API) CloseGeneralForumTopicWithContext(ctx context.Context, params Ba
// ReopenGeneralForumTopic reopens the 'General' topic in a forum supergroup. // ReopenGeneralForumTopic reopens the 'General' topic in a forum supergroup.
// Returns True on success. // Returns True on success.
// See https://core.telegram.org/bots/api#reopengeneralforumtopic // See https://core.telegram.org/bots/api#reopengeneralforumtopic
func (api *API) ReopenGeneralForumTopic(params BaseGeneralForumTopicP) (bool, error) { func (api *API) ReopenGeneralForumTopic(params BaseGeneralForumTopic) (bool, error) {
req := NewRequestWithChatID[bool]("reopenGeneralForumTopic", params, params.ChatID) req := NewRequestWithChatID[bool]("reopenGeneralForumTopic", params, params.ChatID)
return req.Do(api) return req.Do(api)
} }
@@ -191,7 +191,7 @@ func (api *API) ReopenGeneralForumTopic(params BaseGeneralForumTopicP) (bool, er
// ReopenGeneralForumTopicWithContext is the context-aware variant of ReopenGeneralForumTopic. // ReopenGeneralForumTopicWithContext is the context-aware variant of ReopenGeneralForumTopic.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#reopengeneralforumtopic // See https://core.telegram.org/bots/api#reopengeneralforumtopic
func (api *API) ReopenGeneralForumTopicWithContext(ctx context.Context, params BaseGeneralForumTopicP) (bool, error) { func (api *API) ReopenGeneralForumTopicWithContext(ctx context.Context, params BaseGeneralForumTopic) (bool, error) {
req := NewRequestWithChatID[bool]("reopenGeneralForumTopic", params, params.ChatID) req := NewRequestWithChatID[bool]("reopenGeneralForumTopic", params, params.ChatID)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
@@ -199,7 +199,7 @@ func (api *API) ReopenGeneralForumTopicWithContext(ctx context.Context, params B
// HideGeneralForumTopic hides the 'General' topic in a forum supergroup. // HideGeneralForumTopic hides the 'General' topic in a forum supergroup.
// Returns True on success. // Returns True on success.
// See https://core.telegram.org/bots/api#hidegeneralforumtopic // See https://core.telegram.org/bots/api#hidegeneralforumtopic
func (api *API) HideGeneralForumTopic(params BaseGeneralForumTopicP) (bool, error) { func (api *API) HideGeneralForumTopic(params BaseGeneralForumTopic) (bool, error) {
req := NewRequestWithChatID[bool]("hideGeneralForumTopic", params, params.ChatID) req := NewRequestWithChatID[bool]("hideGeneralForumTopic", params, params.ChatID)
return req.Do(api) return req.Do(api)
} }
@@ -207,7 +207,7 @@ func (api *API) HideGeneralForumTopic(params BaseGeneralForumTopicP) (bool, erro
// HideGeneralForumTopicWithContext is the context-aware variant of HideGeneralForumTopic. // HideGeneralForumTopicWithContext is the context-aware variant of HideGeneralForumTopic.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#hidegeneralforumtopic // See https://core.telegram.org/bots/api#hidegeneralforumtopic
func (api *API) HideGeneralForumTopicWithContext(ctx context.Context, params BaseGeneralForumTopicP) (bool, error) { func (api *API) HideGeneralForumTopicWithContext(ctx context.Context, params BaseGeneralForumTopic) (bool, error) {
req := NewRequestWithChatID[bool]("hideGeneralForumTopic", params, params.ChatID) req := NewRequestWithChatID[bool]("hideGeneralForumTopic", params, params.ChatID)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
@@ -215,7 +215,7 @@ func (api *API) HideGeneralForumTopicWithContext(ctx context.Context, params Bas
// UnhideGeneralForumTopic unhides the 'General' topic in a forum supergroup. // UnhideGeneralForumTopic unhides the 'General' topic in a forum supergroup.
// Returns True on success. // Returns True on success.
// See https://core.telegram.org/bots/api#unhidegeneralforumtopic // See https://core.telegram.org/bots/api#unhidegeneralforumtopic
func (api *API) UnhideGeneralForumTopic(params BaseGeneralForumTopicP) (bool, error) { func (api *API) UnhideGeneralForumTopic(params BaseGeneralForumTopic) (bool, error) {
req := NewRequestWithChatID[bool]("unhideGeneralForumTopic", params, params.ChatID) req := NewRequestWithChatID[bool]("unhideGeneralForumTopic", params, params.ChatID)
return req.Do(api) return req.Do(api)
} }
@@ -223,7 +223,7 @@ func (api *API) UnhideGeneralForumTopic(params BaseGeneralForumTopicP) (bool, er
// UnhideGeneralForumTopicWithContext is the context-aware variant of UnhideGeneralForumTopic. // UnhideGeneralForumTopicWithContext is the context-aware variant of UnhideGeneralForumTopic.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#unhidegeneralforumtopic // See https://core.telegram.org/bots/api#unhidegeneralforumtopic
func (api *API) UnhideGeneralForumTopicWithContext(ctx context.Context, params BaseGeneralForumTopicP) (bool, error) { func (api *API) UnhideGeneralForumTopicWithContext(ctx context.Context, params BaseGeneralForumTopic) (bool, error) {
req := NewRequestWithChatID[bool]("unhideGeneralForumTopic", params, params.ChatID) req := NewRequestWithChatID[bool]("unhideGeneralForumTopic", params, params.ChatID)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
@@ -231,7 +231,7 @@ func (api *API) UnhideGeneralForumTopicWithContext(ctx context.Context, params B
// UnpinAllGeneralForumTopicMessages clears the list of pinned messages in the 'General' topic. // UnpinAllGeneralForumTopicMessages clears the list of pinned messages in the 'General' topic.
// Returns True on success. // Returns True on success.
// See https://core.telegram.org/bots/api#unpinallgeneralforumtopicmessages // See https://core.telegram.org/bots/api#unpinallgeneralforumtopicmessages
func (api *API) UnpinAllGeneralForumTopicMessages(params BaseGeneralForumTopicP) (bool, error) { func (api *API) UnpinAllGeneralForumTopicMessages(params BaseGeneralForumTopic) (bool, error) {
req := NewRequestWithChatID[bool]("unpinAllGeneralForumTopicMessages", params, params.ChatID) req := NewRequestWithChatID[bool]("unpinAllGeneralForumTopicMessages", params, params.ChatID)
return req.Do(api) return req.Do(api)
} }
@@ -239,7 +239,7 @@ func (api *API) UnpinAllGeneralForumTopicMessages(params BaseGeneralForumTopicP)
// UnpinAllGeneralForumTopicMessagesWithContext is the context-aware variant of UnpinAllGeneralForumTopicMessages. // UnpinAllGeneralForumTopicMessagesWithContext is the context-aware variant of UnpinAllGeneralForumTopicMessages.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#unpinallgeneralforumtopicmessages // See https://core.telegram.org/bots/api#unpinallgeneralforumtopicmessages
func (api *API) UnpinAllGeneralForumTopicMessagesWithContext(ctx context.Context, params BaseGeneralForumTopicP) (bool, error) { func (api *API) UnpinAllGeneralForumTopicMessagesWithContext(ctx context.Context, params BaseGeneralForumTopic) (bool, error) {
req := NewRequestWithChatID[bool]("unpinAllGeneralForumTopicMessages", params, params.ChatID) req := NewRequestWithChatID[bool]("unpinAllGeneralForumTopicMessages", params, params.ChatID)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
+16
View File
@@ -19,3 +19,19 @@ const (
// ForumTopicIconColorBlue is the blue color for forum topic icons (value 7322096). // ForumTopicIconColorBlue is the blue color for forum topic icons (value 7322096).
ForumTopicIconColorBlue ForumTopicIconColor = 7322096 ForumTopicIconColorBlue ForumTopicIconColor = 7322096
) )
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"`
}
type ForumTopicEdited struct {
Name string `json:"name,omitempty"`
IconCustomEmojiID string `json:"icon_custom_emoji_id,omitempty"`
}
type ForumTopicClosed struct{}
type ForumTopicReopened struct{}
type GeneralForumTopicHidden struct{}
type GeneralForumTopicUnhidden struct {
}
+12 -12
View File
@@ -2,9 +2,9 @@ package tgapi
import "context" import "context"
// SendGameP holds parameters for the sendGame method. // SendGame holds parameters for the sendGame method.
// See https://core.telegram.org/bots/api#sendgame // See https://core.telegram.org/bots/api#sendgame
type SendGameP struct { type SendGame struct {
BusinessConnectionID string `json:"business_connection_id,omitempty"` BusinessConnectionID string `json:"business_connection_id,omitempty"`
ChatID int64 `json:"chat_id"` ChatID int64 `json:"chat_id"`
MessageThreadID int `json:"message_thread_id,omitempty"` MessageThreadID int `json:"message_thread_id,omitempty"`
@@ -21,7 +21,7 @@ type SendGameP struct {
// SendGame sends a game message. // SendGame sends a game message.
// See https://core.telegram.org/bots/api#sendgame // See https://core.telegram.org/bots/api#sendgame
func (api *API) SendGame(params SendGameP) (Message, error) { func (api *API) SendGame(params SendGame) (Message, error) {
req := NewRequestWithChatID[Message]("sendGame", params, params.ChatID) req := NewRequestWithChatID[Message]("sendGame", params, params.ChatID)
return req.Do(api) return req.Do(api)
} }
@@ -29,14 +29,14 @@ func (api *API) SendGame(params SendGameP) (Message, error) {
// SendGameWithContext is the context-aware variant of SendGame. // SendGameWithContext is the context-aware variant of SendGame.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#sendgame // See https://core.telegram.org/bots/api#sendgame
func (api *API) SendGameWithContext(ctx context.Context, params SendGameP) (Message, error) { func (api *API) SendGameWithContext(ctx context.Context, params SendGame) (Message, error) {
req := NewRequestWithChatID[Message]("sendGame", params, params.ChatID) req := NewRequestWithChatID[Message]("sendGame", params, params.ChatID)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// SetGameScoreP holds parameters for the setGameScore method. // SetGameScore holds parameters for the setGameScore method.
// See https://core.telegram.org/bots/api#setgamescore // See https://core.telegram.org/bots/api#setgamescore
type SetGameScoreP struct { type SetGameScore struct {
UserID int64 `json:"user_id"` UserID int64 `json:"user_id"`
Score int `json:"score"` Score int `json:"score"`
Force bool `json:"force,omitempty"` Force bool `json:"force,omitempty"`
@@ -50,7 +50,7 @@ type SetGameScoreP struct {
// If inline_message_id is provided, returns a boolean success flag. // If inline_message_id is provided, returns a boolean success flag.
// Otherwise returns the edited Message. // Otherwise returns the edited Message.
// See https://core.telegram.org/bots/api#setgamescore // See https://core.telegram.org/bots/api#setgamescore
func (api *API) SetGameScore(params SetGameScoreP) (Message, bool, error) { func (api *API) SetGameScore(params SetGameScore) (Message, bool, error) {
var zero Message var zero Message
if params.InlineMessageID != "" { if params.InlineMessageID != "" {
req := NewRequestWithChatID[bool]("setGameScore", params, params.ChatID) req := NewRequestWithChatID[bool]("setGameScore", params, params.ChatID)
@@ -65,7 +65,7 @@ func (api *API) SetGameScore(params SetGameScoreP) (Message, bool, error) {
// SetGameScoreWithContext is the context-aware variant of SetGameScore. // SetGameScoreWithContext is the context-aware variant of SetGameScore.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#setgamescore // See https://core.telegram.org/bots/api#setgamescore
func (api *API) SetGameScoreWithContext(ctx context.Context, params SetGameScoreP) (Message, bool, error) { func (api *API) SetGameScoreWithContext(ctx context.Context, params SetGameScore) (Message, bool, error) {
var zero Message var zero Message
if params.InlineMessageID != "" { if params.InlineMessageID != "" {
req := NewRequestWithChatID[bool]("setGameScore", params, params.ChatID) req := NewRequestWithChatID[bool]("setGameScore", params, params.ChatID)
@@ -77,9 +77,9 @@ func (api *API) SetGameScoreWithContext(ctx context.Context, params SetGameScore
return res, false, err return res, false, err
} }
// GetGameHighScoresP holds parameters for the getGameHighScores method. // GetGameHighScores holds parameters for the getGameHighScores method.
// See https://core.telegram.org/bots/api#getgamehighscores // See https://core.telegram.org/bots/api#getgamehighscores
type GetGameHighScoresP struct { type GetGameHighScores struct {
UserID int64 `json:"user_id"` UserID int64 `json:"user_id"`
ChatID int64 `json:"chat_id,omitempty"` ChatID int64 `json:"chat_id,omitempty"`
MessageID int `json:"message_id,omitempty"` MessageID int `json:"message_id,omitempty"`
@@ -88,7 +88,7 @@ type GetGameHighScoresP struct {
// GetGameHighScores returns game high score data for a user. // GetGameHighScores returns game high score data for a user.
// See https://core.telegram.org/bots/api#getgamehighscores // See https://core.telegram.org/bots/api#getgamehighscores
func (api *API) GetGameHighScores(params GetGameHighScoresP) ([]GameHighScore, error) { func (api *API) GetGameHighScores(params GetGameHighScores) ([]GameHighScore, error) {
req := NewRequestWithChatID[[]GameHighScore]("getGameHighScores", params, params.ChatID) req := NewRequestWithChatID[[]GameHighScore]("getGameHighScores", params, params.ChatID)
return req.Do(api) return req.Do(api)
} }
@@ -96,7 +96,7 @@ func (api *API) GetGameHighScores(params GetGameHighScoresP) ([]GameHighScore, e
// GetGameHighScoresWithContext is the context-aware variant of GetGameHighScores. // GetGameHighScoresWithContext is the context-aware variant of GetGameHighScores.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#getgamehighscores // See https://core.telegram.org/bots/api#getgamehighscores
func (api *API) GetGameHighScoresWithContext(ctx context.Context, params GetGameHighScoresP) ([]GameHighScore, error) { func (api *API) GetGameHighScoresWithContext(ctx context.Context, params GetGameHighScores) ([]GameHighScore, error) {
req := NewRequestWithChatID[[]GameHighScore]("getGameHighScores", params, params.ChatID) req := NewRequestWithChatID[[]GameHighScore]("getGameHighScores", params, params.ChatID)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
+10
View File
@@ -1,5 +1,15 @@
package tgapi package tgapi
type Game struct {
Title string `json:"title"`
Description string `json:"description"`
Photo []PhotoSize `json:"photo"`
Text string `json:"text,omitempty"`
TextEntities []MessageEntity `json:"text_entities,omitempty"`
Animation *Animation `json:"animation,omitempty"`
}
type CallbackGame struct{}
// GameHighScore represents one row in a game high score table. // GameHighScore represents one row in a game high score table.
// See https://core.telegram.org/bots/api#gamehighscore // See https://core.telegram.org/bots/api#gamehighscore
type GameHighScore struct { type GameHighScore struct {
+34 -12
View File
@@ -2,9 +2,9 @@ package tgapi
import "context" import "context"
// AnswerInlineQueryP holds parameters for the answerInlineQuery method. // AnswerInlineQuery holds parameters for the answerInlineQuery method.
// See https://core.telegram.org/bots/api#answerinlinequery // See https://core.telegram.org/bots/api#answerinlinequery
type AnswerInlineQueryP struct { type AnswerInlineQuery struct {
InlineQueryID string `json:"inline_query_id"` InlineQueryID string `json:"inline_query_id"`
Results []InlineQueryResult `json:"results"` Results []InlineQueryResult `json:"results"`
CacheTime int `json:"cache_time,omitempty"` CacheTime int `json:"cache_time,omitempty"`
@@ -16,7 +16,7 @@ type AnswerInlineQueryP struct {
// AnswerInlineQuery sends answers to an inline query. // AnswerInlineQuery sends answers to an inline query.
// Returns true on success. // Returns true on success.
// See https://core.telegram.org/bots/api#answerinlinequery // See https://core.telegram.org/bots/api#answerinlinequery
func (api *API) AnswerInlineQuery(params AnswerInlineQueryP) (bool, error) { func (api *API) AnswerInlineQuery(params AnswerInlineQuery) (bool, error) {
req := NewRequest[bool]("answerInlineQuery", params) req := NewRequest[bool]("answerInlineQuery", params)
return req.Do(api) return req.Do(api)
} }
@@ -24,21 +24,21 @@ func (api *API) AnswerInlineQuery(params AnswerInlineQueryP) (bool, error) {
// AnswerInlineQueryWithContext is the context-aware variant of AnswerInlineQuery. // AnswerInlineQueryWithContext is the context-aware variant of AnswerInlineQuery.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#answerinlinequery // See https://core.telegram.org/bots/api#answerinlinequery
func (api *API) AnswerInlineQueryWithContext(ctx context.Context, params AnswerInlineQueryP) (bool, error) { func (api *API) AnswerInlineQueryWithContext(ctx context.Context, params AnswerInlineQuery) (bool, error) {
req := NewRequest[bool]("answerInlineQuery", params) req := NewRequest[bool]("answerInlineQuery", params)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// AnswerWebAppQueryP holds parameters for the answerWebAppQuery method. // AnswerWebAppQuery holds parameters for the answerWebAppQuery method.
// See https://core.telegram.org/bots/api#answerwebappquery // See https://core.telegram.org/bots/api#answerwebappquery
type AnswerWebAppQueryP struct { type AnswerWebAppQuery struct {
WebAppQueryID string `json:"web_app_query_id"` WebAppQueryID string `json:"web_app_query_id"`
Result InlineQueryResult `json:"result"` Result InlineQueryResult `json:"result"`
} }
// AnswerWebAppQuery sets the result of a Web App interaction. // AnswerWebAppQuery sets the result of a Web App interaction.
// See https://core.telegram.org/bots/api#answerwebappquery // See https://core.telegram.org/bots/api#answerwebappquery
func (api *API) AnswerWebAppQuery(params AnswerWebAppQueryP) (SentWebAppMessage, error) { func (api *API) AnswerWebAppQuery(params AnswerWebAppQuery) (SentWebAppMessage, error) {
req := NewRequest[SentWebAppMessage]("answerWebAppQuery", params) req := NewRequest[SentWebAppMessage]("answerWebAppQuery", params)
return req.Do(api) return req.Do(api)
} }
@@ -46,14 +46,14 @@ func (api *API) AnswerWebAppQuery(params AnswerWebAppQueryP) (SentWebAppMessage,
// AnswerWebAppQueryWithContext is the context-aware variant of AnswerWebAppQuery. // AnswerWebAppQueryWithContext is the context-aware variant of AnswerWebAppQuery.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#answerwebappquery // See https://core.telegram.org/bots/api#answerwebappquery
func (api *API) AnswerWebAppQueryWithContext(ctx context.Context, params AnswerWebAppQueryP) (SentWebAppMessage, error) { func (api *API) AnswerWebAppQueryWithContext(ctx context.Context, params AnswerWebAppQuery) (SentWebAppMessage, error) {
req := NewRequest[SentWebAppMessage]("answerWebAppQuery", params) req := NewRequest[SentWebAppMessage]("answerWebAppQuery", params)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// SavePreparedInlineMessageP holds parameters for the savePreparedInlineMessage method. // SavePreparedInlineMessage holds parameters for the savePreparedInlineMessage method.
// See https://core.telegram.org/bots/api#savepreparedinlinemessage // See https://core.telegram.org/bots/api#savepreparedinlinemessage
type SavePreparedInlineMessageP struct { type SavePreparedInlineMessage struct {
UserID int64 `json:"user_id"` UserID int64 `json:"user_id"`
Result InlineQueryResult `json:"result"` Result InlineQueryResult `json:"result"`
AllowUserChats bool `json:"allow_user_chats,omitempty"` AllowUserChats bool `json:"allow_user_chats,omitempty"`
@@ -64,7 +64,7 @@ type SavePreparedInlineMessageP struct {
// SavePreparedInlineMessage stores a prepared message for Mini App users. // SavePreparedInlineMessage stores a prepared message for Mini App users.
// See https://core.telegram.org/bots/api#savepreparedinlinemessage // See https://core.telegram.org/bots/api#savepreparedinlinemessage
func (api *API) SavePreparedInlineMessage(params SavePreparedInlineMessageP) (PreparedInlineMessage, error) { func (api *API) SavePreparedInlineMessage(params SavePreparedInlineMessage) (PreparedInlineMessage, error) {
req := NewRequest[PreparedInlineMessage]("savePreparedInlineMessage", params) req := NewRequest[PreparedInlineMessage]("savePreparedInlineMessage", params)
return req.Do(api) return req.Do(api)
} }
@@ -72,7 +72,29 @@ func (api *API) SavePreparedInlineMessage(params SavePreparedInlineMessageP) (Pr
// SavePreparedInlineMessageWithContext is the context-aware variant of SavePreparedInlineMessage. // SavePreparedInlineMessageWithContext is the context-aware variant of SavePreparedInlineMessage.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#savepreparedinlinemessage // See https://core.telegram.org/bots/api#savepreparedinlinemessage
func (api *API) SavePreparedInlineMessageWithContext(ctx context.Context, params SavePreparedInlineMessageP) (PreparedInlineMessage, error) { func (api *API) SavePreparedInlineMessageWithContext(ctx context.Context, params SavePreparedInlineMessage) (PreparedInlineMessage, error) {
req := NewRequest[PreparedInlineMessage]("savePreparedInlineMessage", params) req := NewRequest[PreparedInlineMessage]("savePreparedInlineMessage", params)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// SavePreparedKeyboardButton holds parameters for the savePreparedKeyboardButton method.
// See https://core.telegram.org/bots/api#savepreparedkeyboardbutton
type SavePreparedKeyboardButton struct {
UserID int64 `json:"user_id"`
Button KeyboardButton `json:"button"`
}
// SavePreparedKeyboardButton stores a prepared keyboard button for Mini App users.
// See https://core.telegram.org/bots/api#savepreparedkeyboardbutton
func (api *API) SavePreparedKeyboardButton(params SavePreparedKeyboardButton) (PreparedKeyboardButton, error) {
req := NewRequest[PreparedKeyboardButton]("savePreparedKeyboardButton", params)
return req.Do(api)
}
// SavePreparedKeyboardButtonWithContext is the context-aware variant of SavePreparedKeyboardButton.
// 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) {
req := NewRequest[PreparedKeyboardButton]("savePreparedKeyboardButton", params)
return req.DoWithContext(ctx, api)
}
+6
View File
@@ -24,3 +24,9 @@ type PreparedInlineMessage struct {
ID string `json:"id"` ID string `json:"id"`
ExpirationDate int `json:"expiration_date"` ExpirationDate int `json:"expiration_date"`
} }
// PreparedKeyboardButton describes a prepared keyboard button.
// See https://core.telegram.org/bots/api#preparedkeyboardbutton
type PreparedKeyboardButton struct {
ID string `json:"id"`
}
+132 -124
View File
@@ -2,9 +2,9 @@ package tgapi
import "context" import "context"
// SendMessageP holds parameters for the sendMessage method. // SendMessage holds parameters for the sendMessage method.
// See https://core.telegram.org/bots/api#sendmessage // See https://core.telegram.org/bots/api#sendmessage
type SendMessageP struct { type SendMessage struct {
BusinessConnectionID string `json:"business_connection_id,omitempty"` BusinessConnectionID string `json:"business_connection_id,omitempty"`
ChatID int64 `json:"chat_id"` ChatID int64 `json:"chat_id"`
MessageThreadID int `json:"message_thread_id,omitempty"` MessageThreadID int `json:"message_thread_id,omitempty"`
@@ -26,22 +26,22 @@ type SendMessageP struct {
// SendMessage sends a text message. // SendMessage sends a text message.
// See https://core.telegram.org/bots/api#sendmessage // See https://core.telegram.org/bots/api#sendmessage
func (api *API) SendMessage(params SendMessageP) (Message, error) { func (api *API) SendMessage(params SendMessage) (Message, error) {
req := NewRequestWithChatID[Message, SendMessageP]("sendMessage", params, params.ChatID) req := NewRequestWithChatID[Message, SendMessage]("sendMessage", params, params.ChatID)
return req.Do(api) return req.Do(api)
} }
// SendMessageWithContext is the context-aware variant of SendMessage. // SendMessageWithContext is the context-aware variant of SendMessage.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#sendmessage // See https://core.telegram.org/bots/api#sendmessage
func (api *API) SendMessageWithContext(ctx context.Context, params SendMessageP) (Message, error) { func (api *API) SendMessageWithContext(ctx context.Context, params SendMessage) (Message, error) {
req := NewRequestWithChatID[Message, SendMessageP]("sendMessage", params, params.ChatID) req := NewRequestWithChatID[Message, SendMessage]("sendMessage", params, params.ChatID)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// ForwardMessageP holds parameters for the forwardMessage method. // ForwardMessage holds parameters for the forwardMessage method.
// See https://core.telegram.org/bots/api#forwardmessage // See https://core.telegram.org/bots/api#forwardmessage
type ForwardMessageP struct { type ForwardMessage struct {
ChatID int64 `json:"chat_id"` ChatID int64 `json:"chat_id"`
MessageThreadID int `json:"message_thread_id,omitempty"` MessageThreadID int `json:"message_thread_id,omitempty"`
DirectMessagesTopicID int `json:"direct_messages_topic_id,omitempty"` DirectMessagesTopicID int `json:"direct_messages_topic_id,omitempty"`
@@ -58,7 +58,7 @@ type ForwardMessageP struct {
// ForwardMessage forwards a message. // ForwardMessage forwards a message.
// See https://core.telegram.org/bots/api#forwardmessage // See https://core.telegram.org/bots/api#forwardmessage
func (api *API) ForwardMessage(params ForwardMessageP) (Message, error) { func (api *API) ForwardMessage(params ForwardMessage) (Message, error) {
req := NewRequestWithChatID[Message]("forwardMessage", params, params.ChatID) req := NewRequestWithChatID[Message]("forwardMessage", params, params.ChatID)
return req.Do(api) return req.Do(api)
} }
@@ -66,14 +66,14 @@ func (api *API) ForwardMessage(params ForwardMessageP) (Message, error) {
// ForwardMessageWithContext is the context-aware variant of ForwardMessage. // ForwardMessageWithContext is the context-aware variant of ForwardMessage.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#forwardmessage // See https://core.telegram.org/bots/api#forwardmessage
func (api *API) ForwardMessageWithContext(ctx context.Context, params ForwardMessageP) (Message, error) { func (api *API) ForwardMessageWithContext(ctx context.Context, params ForwardMessage) (Message, error) {
req := NewRequestWithChatID[Message]("forwardMessage", params, params.ChatID) req := NewRequestWithChatID[Message]("forwardMessage", params, params.ChatID)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// ForwardMessagesP holds parameters for the forwardMessages method. // ForwardMessages holds parameters for the forwardMessages method.
// See https://core.telegram.org/bots/api#forwardmessages // See https://core.telegram.org/bots/api#forwardmessages
type ForwardMessagesP struct { type ForwardMessages struct {
ChatID int64 `json:"chat_id"` ChatID int64 `json:"chat_id"`
MessageThreadID int `json:"message_thread_id,omitempty"` MessageThreadID int `json:"message_thread_id,omitempty"`
DirectMessagesTopicID int `json:"direct_messages_topic_id,omitempty"` DirectMessagesTopicID int `json:"direct_messages_topic_id,omitempty"`
@@ -87,7 +87,7 @@ type ForwardMessagesP struct {
// ForwardMessages forwards multiple messages. // ForwardMessages forwards multiple messages.
// Returns an array of message IDs of the sent messages. // Returns an array of message IDs of the sent messages.
// See https://core.telegram.org/bots/api#forwardmessages // See https://core.telegram.org/bots/api#forwardmessages
func (api *API) ForwardMessages(params ForwardMessagesP) ([]MessageID, error) { func (api *API) ForwardMessages(params ForwardMessages) ([]MessageID, error) {
req := NewRequestWithChatID[[]MessageID]("forwardMessages", params, params.ChatID) req := NewRequestWithChatID[[]MessageID]("forwardMessages", params, params.ChatID)
return req.Do(api) return req.Do(api)
} }
@@ -95,14 +95,14 @@ func (api *API) ForwardMessages(params ForwardMessagesP) ([]MessageID, error) {
// ForwardMessagesWithContext is the context-aware variant of ForwardMessages. // ForwardMessagesWithContext is the context-aware variant of ForwardMessages.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#forwardmessages // See https://core.telegram.org/bots/api#forwardmessages
func (api *API) ForwardMessagesWithContext(ctx context.Context, params ForwardMessagesP) ([]MessageID, error) { func (api *API) ForwardMessagesWithContext(ctx context.Context, params ForwardMessages) ([]MessageID, error) {
req := NewRequestWithChatID[[]MessageID]("forwardMessages", params, params.ChatID) req := NewRequestWithChatID[[]MessageID]("forwardMessages", params, params.ChatID)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// CopyMessageP holds parameters for the copyMessage method. // CopyMessage holds parameters for the copyMessage method.
// See https://core.telegram.org/bots/api#copymessage // See https://core.telegram.org/bots/api#copymessage
type CopyMessageP struct { type CopyMessage struct {
ChatID int64 `json:"chat_id"` ChatID int64 `json:"chat_id"`
MessageThreadID int `json:"message_thread_id,omitempty"` MessageThreadID int `json:"message_thread_id,omitempty"`
DirectMessagesTopicID int `json:"direct_messages_topic_id,omitempty"` DirectMessagesTopicID int `json:"direct_messages_topic_id,omitempty"`
@@ -128,7 +128,7 @@ type CopyMessageP struct {
// CopyMessage copies a message. // CopyMessage copies a message.
// Returns the MessageID of the sent copy. // Returns the MessageID of the sent copy.
// See https://core.telegram.org/bots/api#copymessage // See https://core.telegram.org/bots/api#copymessage
func (api *API) CopyMessage(params CopyMessageP) (int, error) { func (api *API) CopyMessage(params CopyMessage) (int, error) {
msgID, err := NewRequestWithChatID[MessageID]("copyMessage", params, params.ChatID).Do(api) msgID, err := NewRequestWithChatID[MessageID]("copyMessage", params, params.ChatID).Do(api)
if err != nil { if err != nil {
return 0, err return 0, err
@@ -139,7 +139,7 @@ func (api *API) CopyMessage(params CopyMessageP) (int, error) {
// CopyMessageWithContext is the context-aware variant of CopyMessage. // CopyMessageWithContext is the context-aware variant of CopyMessage.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#copymessage // See https://core.telegram.org/bots/api#copymessage
func (api *API) CopyMessageWithContext(ctx context.Context, params CopyMessageP) (int, error) { func (api *API) CopyMessageWithContext(ctx context.Context, params CopyMessage) (int, error) {
msgID, err := NewRequestWithChatID[MessageID]("copyMessage", params, params.ChatID).DoWithContext(ctx, api) msgID, err := NewRequestWithChatID[MessageID]("copyMessage", params, params.ChatID).DoWithContext(ctx, api)
if err != nil { if err != nil {
return 0, err return 0, err
@@ -147,9 +147,9 @@ func (api *API) CopyMessageWithContext(ctx context.Context, params CopyMessageP)
return msgID.MessageID, nil return msgID.MessageID, nil
} }
// CopyMessagesP holds parameters for the copyMessages method. // CopyMessages holds parameters for the copyMessages method.
// See https://core.telegram.org/bots/api#copymessages // See https://core.telegram.org/bots/api#copymessages
type CopyMessagesP struct { type CopyMessages struct {
ChatID int64 `json:"chat_id"` ChatID int64 `json:"chat_id"`
MessageThreadID int `json:"message_thread_id,omitempty"` MessageThreadID int `json:"message_thread_id,omitempty"`
DirectMessagesTopicID int `json:"direct_messages_topic_id,omitempty"` DirectMessagesTopicID int `json:"direct_messages_topic_id,omitempty"`
@@ -164,7 +164,7 @@ type CopyMessagesP struct {
// CopyMessages copies multiple messages. // CopyMessages copies multiple messages.
// Returns an array of message IDs of the sent copies. // Returns an array of message IDs of the sent copies.
// See https://core.telegram.org/bots/api#copymessages // See https://core.telegram.org/bots/api#copymessages
func (api *API) CopyMessages(params CopyMessagesP) ([]MessageID, error) { func (api *API) CopyMessages(params CopyMessages) ([]MessageID, error) {
req := NewRequestWithChatID[[]MessageID]("copyMessages", params, params.ChatID) req := NewRequestWithChatID[[]MessageID]("copyMessages", params, params.ChatID)
return req.Do(api) return req.Do(api)
} }
@@ -172,14 +172,14 @@ func (api *API) CopyMessages(params CopyMessagesP) ([]MessageID, error) {
// CopyMessagesWithContext is the context-aware variant of CopyMessages. // CopyMessagesWithContext is the context-aware variant of CopyMessages.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#copymessages // See https://core.telegram.org/bots/api#copymessages
func (api *API) CopyMessagesWithContext(ctx context.Context, params CopyMessagesP) ([]MessageID, error) { func (api *API) CopyMessagesWithContext(ctx context.Context, params CopyMessages) ([]MessageID, error) {
req := NewRequestWithChatID[[]MessageID]("copyMessages", params, params.ChatID) req := NewRequestWithChatID[[]MessageID]("copyMessages", params, params.ChatID)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// SendLocationP holds parameters for the sendLocation method. // SendLocation holds parameters for the sendLocation method.
// See https://core.telegram.org/bots/api#sendlocation // See https://core.telegram.org/bots/api#sendlocation
type SendLocationP struct { type SendLocation struct {
BusinessConnectionID string `json:"business_connection_id,omitempty"` BusinessConnectionID string `json:"business_connection_id,omitempty"`
ChatID int64 `json:"chat_id"` ChatID int64 `json:"chat_id"`
MessageThreadID int `json:"message_thread_id,omitempty"` MessageThreadID int `json:"message_thread_id,omitempty"`
@@ -204,7 +204,7 @@ type SendLocationP struct {
// SendLocation sends a point on the map. // SendLocation sends a point on the map.
// See https://core.telegram.org/bots/api#sendlocation // See https://core.telegram.org/bots/api#sendlocation
func (api *API) SendLocation(params SendLocationP) (Message, error) { func (api *API) SendLocation(params SendLocation) (Message, error) {
req := NewRequestWithChatID[Message]("sendLocation", params, params.ChatID) req := NewRequestWithChatID[Message]("sendLocation", params, params.ChatID)
return req.Do(api) return req.Do(api)
} }
@@ -212,14 +212,14 @@ func (api *API) SendLocation(params SendLocationP) (Message, error) {
// SendLocationWithContext is the context-aware variant of SendLocation. // SendLocationWithContext is the context-aware variant of SendLocation.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#sendlocation // See https://core.telegram.org/bots/api#sendlocation
func (api *API) SendLocationWithContext(ctx context.Context, params SendLocationP) (Message, error) { func (api *API) SendLocationWithContext(ctx context.Context, params SendLocation) (Message, error) {
req := NewRequestWithChatID[Message]("sendLocation", params, params.ChatID) req := NewRequestWithChatID[Message]("sendLocation", params, params.ChatID)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// SendVenueP holds parameters for the sendVenue method. // SendVenue holds parameters for the sendVenue method.
// See https://core.telegram.org/bots/api#sendvenue // See https://core.telegram.org/bots/api#sendvenue
type SendVenueP struct { type SendVenue struct {
BusinessConnectionID string `json:"business_connection_id,omitempty"` BusinessConnectionID string `json:"business_connection_id,omitempty"`
ChatID int64 `json:"chat_id"` ChatID int64 `json:"chat_id"`
MessageThreadID int `json:"message_thread_id,omitempty"` MessageThreadID int `json:"message_thread_id,omitempty"`
@@ -246,7 +246,7 @@ type SendVenueP struct {
// SendVenue sends information about a venue. // SendVenue sends information about a venue.
// See https://core.telegram.org/bots/api#sendvenue // See https://core.telegram.org/bots/api#sendvenue
func (api *API) SendVenue(params SendVenueP) (Message, error) { func (api *API) SendVenue(params SendVenue) (Message, error) {
req := NewRequestWithChatID[Message]("sendVenue", params, params.ChatID) req := NewRequestWithChatID[Message]("sendVenue", params, params.ChatID)
return req.Do(api) return req.Do(api)
} }
@@ -254,14 +254,14 @@ func (api *API) SendVenue(params SendVenueP) (Message, error) {
// SendVenueWithContext is the context-aware variant of SendVenue. // SendVenueWithContext is the context-aware variant of SendVenue.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#sendvenue // See https://core.telegram.org/bots/api#sendvenue
func (api *API) SendVenueWithContext(ctx context.Context, params SendVenueP) (Message, error) { func (api *API) SendVenueWithContext(ctx context.Context, params SendVenue) (Message, error) {
req := NewRequestWithChatID[Message]("sendVenue", params, params.ChatID) req := NewRequestWithChatID[Message]("sendVenue", params, params.ChatID)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// SendContactP holds parameters for the sendContact method. // SendContact holds parameters for the sendContact method.
// See https://core.telegram.org/bots/api#sendcontact // See https://core.telegram.org/bots/api#sendcontact
type SendContactP struct { type SendContact struct {
BusinessConnectionID string `json:"business_connection_id,omitempty"` BusinessConnectionID string `json:"business_connection_id,omitempty"`
ChatID int64 `json:"chat_id"` ChatID int64 `json:"chat_id"`
MessageThreadID int `json:"message_thread_id,omitempty"` MessageThreadID int `json:"message_thread_id,omitempty"`
@@ -284,7 +284,7 @@ type SendContactP struct {
// SendContact sends a phone contact. // SendContact sends a phone contact.
// See https://core.telegram.org/bots/api#sendcontact // See https://core.telegram.org/bots/api#sendcontact
func (api *API) SendContact(params SendContactP) (Message, error) { func (api *API) SendContact(params SendContact) (Message, error) {
req := NewRequestWithChatID[Message]("sendContact", params, params.ChatID) req := NewRequestWithChatID[Message]("sendContact", params, params.ChatID)
return req.Do(api) return req.Do(api)
} }
@@ -292,32 +292,40 @@ func (api *API) SendContact(params SendContactP) (Message, error) {
// SendContactWithContext is the context-aware variant of SendContact. // SendContactWithContext is the context-aware variant of SendContact.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#sendcontact // See https://core.telegram.org/bots/api#sendcontact
func (api *API) SendContactWithContext(ctx context.Context, params SendContactP) (Message, error) { func (api *API) SendContactWithContext(ctx context.Context, params SendContact) (Message, error) {
req := NewRequestWithChatID[Message]("sendContact", params, params.ChatID) req := NewRequestWithChatID[Message]("sendContact", params, params.ChatID)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// SendPollP holds parameters for the sendPoll method. // SendPoll holds parameters for the sendPoll method.
// See https://core.telegram.org/bots/api#sendpoll // See https://core.telegram.org/bots/api#sendpoll
type SendPollP struct { type SendPoll struct {
BusinessConnectionID string `json:"business_connection_id,omitempty"` BusinessConnectionID string `json:"business_connection_id,omitempty"`
ChatID int64 `json:"chat_id"` ChatID int64 `json:"chat_id"`
MessageThreadID int `json:"message_thread_id,omitempty"` MessageThreadID int `json:"message_thread_id,omitempty"`
Question string `json:"question"` Question string `json:"question"`
QuestionParseMode ParseMode `json:"question_parse_mode,omitempty"` QuestionParseMode ParseMode `json:"question_parse_mode,omitempty"`
QuestionEntities []MessageEntity `json:"question_entities,omitempty"` QuestionEntities []MessageEntity `json:"question_entities,omitempty"`
Options []InputPollOption `json:"options"` Options []InputPollOption `json:"options"`
IsAnonymous bool `json:"is_anonymous,omitempty"` IsAnonymous bool `json:"is_anonymous,omitempty"`
Type PollType `json:"type"` Type PollType `json:"type"`
AllowsMultipleAnswers bool `json:"allows_multiple_answers,omitempty"` AllowsMultipleAnswers bool `json:"allows_multiple_answers,omitempty"`
CorrectOptionID int `json:"correct_option_id,omitempty"` AllowsRevoting bool `json:"allows_revoting,omitempty"`
Explanation string `json:"explanation,omitempty"` ShuffleOptions bool `json:"shuffle_options,omitempty"`
ExplanationParseMode ParseMode `json:"explanation_parse_mode,omitempty"` AllowAddingOptions bool `json:"allow_adding_options,omitempty"`
ExplanationEntities []MessageEntity `json:"explanation_entities,omitempty"` HideResultsUntilCloses bool `json:"hide_results_until_closes,omitempty"`
OpenPeriod int `json:"open_period,omitempty"` CorrectOptionIDs []int `json:"correct_option_ids,omitempty"`
CloseDate int `json:"close_date"` Explanation string `json:"explanation,omitempty"`
IsClosed bool `json:"is_closed,omitempty"` ExplanationParseMode ParseMode `json:"explanation_parse_mode,omitempty"`
ExplanationEntities []MessageEntity `json:"explanation_entities,omitempty"`
OpenPeriod int `json:"open_period,omitempty"`
CloseDate int `json:"close_date"`
IsClosed bool `json:"is_closed,omitempty"`
Description string `json:"description"`
DescriptionParseMode ParseMode `json:"description_parse_mode,omitempty"`
DescriptionEntities []MessageEntity `json:"description_entities,omitempty"`
DisableNotification bool `json:"disable_notification,omitempty"` DisableNotification bool `json:"disable_notification,omitempty"`
ProtectContent bool `json:"protect_content,omitempty"` ProtectContent bool `json:"protect_content,omitempty"`
@@ -330,7 +338,7 @@ type SendPollP struct {
// SendPoll sends a native poll. // SendPoll sends a native poll.
// See https://core.telegram.org/bots/api#sendpoll // See https://core.telegram.org/bots/api#sendpoll
func (api *API) SendPoll(params SendPollP) (Message, error) { func (api *API) SendPoll(params SendPoll) (Message, error) {
req := NewRequestWithChatID[Message]("sendPoll", params, params.ChatID) req := NewRequestWithChatID[Message]("sendPoll", params, params.ChatID)
return req.Do(api) return req.Do(api)
} }
@@ -338,14 +346,14 @@ func (api *API) SendPoll(params SendPollP) (Message, error) {
// SendPollWithContext is the context-aware variant of SendPoll. // SendPollWithContext is the context-aware variant of SendPoll.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#sendpoll // See https://core.telegram.org/bots/api#sendpoll
func (api *API) SendPollWithContext(ctx context.Context, params SendPollP) (Message, error) { func (api *API) SendPollWithContext(ctx context.Context, params SendPoll) (Message, error) {
req := NewRequestWithChatID[Message]("sendPoll", params, params.ChatID) req := NewRequestWithChatID[Message]("sendPoll", params, params.ChatID)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// SendChecklistP holds parameters for the sendChecklist method. // SendChecklist holds parameters for the sendChecklist method.
// See https://core.telegram.org/bots/api#sendchecklist // See https://core.telegram.org/bots/api#sendchecklist
type SendChecklistP struct { type SendChecklist struct {
BusinessConnectionID string `json:"business_connection_id"` BusinessConnectionID string `json:"business_connection_id"`
ChatID int64 `json:"chat_id"` ChatID int64 `json:"chat_id"`
Checklist InputChecklist `json:"checklist"` Checklist InputChecklist `json:"checklist"`
@@ -360,7 +368,7 @@ type SendChecklistP struct {
// SendChecklist sends a checklist. // SendChecklist sends a checklist.
// See https://core.telegram.org/bots/api#sendchecklist // See https://core.telegram.org/bots/api#sendchecklist
func (api *API) SendChecklist(params SendChecklistP) (Message, error) { func (api *API) SendChecklist(params SendChecklist) (Message, error) {
req := NewRequestWithChatID[Message]("sendChecklist", params, params.ChatID) req := NewRequestWithChatID[Message]("sendChecklist", params, params.ChatID)
return req.Do(api) return req.Do(api)
} }
@@ -368,14 +376,14 @@ func (api *API) SendChecklist(params SendChecklistP) (Message, error) {
// SendChecklistWithContext is the context-aware variant of SendChecklist. // SendChecklistWithContext is the context-aware variant of SendChecklist.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#sendchecklist // See https://core.telegram.org/bots/api#sendchecklist
func (api *API) SendChecklistWithContext(ctx context.Context, params SendChecklistP) (Message, error) { func (api *API) SendChecklistWithContext(ctx context.Context, params SendChecklist) (Message, error) {
req := NewRequestWithChatID[Message]("sendChecklist", params, params.ChatID) req := NewRequestWithChatID[Message]("sendChecklist", params, params.ChatID)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// SendDiceP holds parameters for the sendDice method. // SendDice holds parameters for the sendDice method.
// See https://core.telegram.org/bots/api#senddice // See https://core.telegram.org/bots/api#senddice
type SendDiceP struct { type SendDice struct {
BusinessConnectionID string `json:"business_connection_id,omitempty"` BusinessConnectionID string `json:"business_connection_id,omitempty"`
ChatID int64 `json:"chat_id"` ChatID int64 `json:"chat_id"`
MessageThreadID int `json:"message_thread_id,omitempty"` MessageThreadID int `json:"message_thread_id,omitempty"`
@@ -395,7 +403,7 @@ type SendDiceP struct {
// SendDice sends a dice, which will have a random value. // SendDice sends a dice, which will have a random value.
// See https://core.telegram.org/bots/api#senddice // See https://core.telegram.org/bots/api#senddice
func (api *API) SendDice(params SendDiceP) (Message, error) { func (api *API) SendDice(params SendDice) (Message, error) {
req := NewRequestWithChatID[Message]("sendDice", params, params.ChatID) req := NewRequestWithChatID[Message]("sendDice", params, params.ChatID)
return req.Do(api) return req.Do(api)
} }
@@ -403,14 +411,14 @@ func (api *API) SendDice(params SendDiceP) (Message, error) {
// SendDiceWithContext is the context-aware variant of SendDice. // SendDiceWithContext is the context-aware variant of SendDice.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#senddice // See https://core.telegram.org/bots/api#senddice
func (api *API) SendDiceWithContext(ctx context.Context, params SendDiceP) (Message, error) { func (api *API) SendDiceWithContext(ctx context.Context, params SendDice) (Message, error) {
req := NewRequestWithChatID[Message]("sendDice", params, params.ChatID) req := NewRequestWithChatID[Message]("sendDice", params, params.ChatID)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// SendMessageDraftP holds parameters for the sendMessageDraft method. // SendMessageDraft holds parameters for the sendMessageDraft method.
// See https://core.telegram.org/bots/api#sendmessagedraft // See https://core.telegram.org/bots/api#sendmessagedraft
type SendMessageDraftP struct { type SendMessageDraft struct {
ChatID int64 `json:"chat_id"` ChatID int64 `json:"chat_id"`
MessageThreadID int `json:"message_thread_id,omitempty"` MessageThreadID int `json:"message_thread_id,omitempty"`
DraftID uint64 `json:"draft_id"` DraftID uint64 `json:"draft_id"`
@@ -422,7 +430,7 @@ type SendMessageDraftP struct {
// SendMessageDraft sends or updates a draft message in the target chat. // SendMessageDraft sends or updates a draft message in the target chat.
// Returns True on success. // Returns True on success.
// See https://core.telegram.org/bots/api#sendmessagedraft // See https://core.telegram.org/bots/api#sendmessagedraft
func (api *API) SendMessageDraft(params SendMessageDraftP) (bool, error) { func (api *API) SendMessageDraft(params SendMessageDraft) (bool, error) {
req := NewRequestWithChatID[bool]("sendMessageDraft", params, params.ChatID) req := NewRequestWithChatID[bool]("sendMessageDraft", params, params.ChatID)
return req.Do(api) return req.Do(api)
} }
@@ -430,14 +438,14 @@ func (api *API) SendMessageDraft(params SendMessageDraftP) (bool, error) {
// SendMessageDraftWithContext is the context-aware variant of SendMessageDraft. // SendMessageDraftWithContext is the context-aware variant of SendMessageDraft.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#sendmessagedraft // See https://core.telegram.org/bots/api#sendmessagedraft
func (api *API) SendMessageDraftWithContext(ctx context.Context, params SendMessageDraftP) (bool, error) { func (api *API) SendMessageDraftWithContext(ctx context.Context, params SendMessageDraft) (bool, error) {
req := NewRequestWithChatID[bool]("sendMessageDraft", params, params.ChatID) req := NewRequestWithChatID[bool]("sendMessageDraft", params, params.ChatID)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// SendChatActionP holds parameters for the sendChatAction method. // SendChatAction holds parameters for the sendChatAction method.
// See https://core.telegram.org/bots/api#sendchataction // See https://core.telegram.org/bots/api#sendchataction
type SendChatActionP struct { type SendChatAction struct {
BusinessConnectionID string `json:"business_connection_id,omitempty"` BusinessConnectionID string `json:"business_connection_id,omitempty"`
ChatID int64 `json:"chat_id"` ChatID int64 `json:"chat_id"`
MessageThreadID int `json:"message_thread_id,omitempty"` MessageThreadID int `json:"message_thread_id,omitempty"`
@@ -447,7 +455,7 @@ type SendChatActionP struct {
// SendChatAction sends a chat action (typing, uploading photo, etc.). // SendChatAction sends a chat action (typing, uploading photo, etc.).
// Returns True on success. // Returns True on success.
// See https://core.telegram.org/bots/api#sendchataction // See https://core.telegram.org/bots/api#sendchataction
func (api *API) SendChatAction(params SendChatActionP) (bool, error) { func (api *API) SendChatAction(params SendChatAction) (bool, error) {
req := NewRequestWithChatID[bool]("sendChatAction", params, params.ChatID) req := NewRequestWithChatID[bool]("sendChatAction", params, params.ChatID)
return req.Do(api) return req.Do(api)
} }
@@ -455,14 +463,14 @@ func (api *API) SendChatAction(params SendChatActionP) (bool, error) {
// SendChatActionWithContext is the context-aware variant of SendChatAction. // SendChatActionWithContext is the context-aware variant of SendChatAction.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#sendchataction // See https://core.telegram.org/bots/api#sendchataction
func (api *API) SendChatActionWithContext(ctx context.Context, params SendChatActionP) (bool, error) { func (api *API) SendChatActionWithContext(ctx context.Context, params SendChatAction) (bool, error) {
req := NewRequestWithChatID[bool]("sendChatAction", params, params.ChatID) req := NewRequestWithChatID[bool]("sendChatAction", params, params.ChatID)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// SetMessageReactionP holds parameters for the setMessageReaction method. // SetMessageReaction holds parameters for the setMessageReaction method.
// See https://core.telegram.org/bots/api#setmessagereaction // See https://core.telegram.org/bots/api#setmessagereaction
type SetMessageReactionP struct { type SetMessageReaction struct {
ChatID int64 `json:"chat_id"` ChatID int64 `json:"chat_id"`
MessageId int `json:"message_id"` MessageId int `json:"message_id"`
Reaction []ReactionType `json:"reaction"` Reaction []ReactionType `json:"reaction"`
@@ -472,7 +480,7 @@ type SetMessageReactionP struct {
// SetMessageReaction changes the chosen reaction on a message. // SetMessageReaction changes the chosen reaction on a message.
// Returns True on success. // Returns True on success.
// See https://core.telegram.org/bots/api#setmessagereaction // See https://core.telegram.org/bots/api#setmessagereaction
func (api *API) SetMessageReaction(params SetMessageReactionP) (bool, error) { func (api *API) SetMessageReaction(params SetMessageReaction) (bool, error) {
req := NewRequestWithChatID[bool]("setMessageReaction", params, params.ChatID) req := NewRequestWithChatID[bool]("setMessageReaction", params, params.ChatID)
return req.Do(api) return req.Do(api)
} }
@@ -480,14 +488,14 @@ func (api *API) SetMessageReaction(params SetMessageReactionP) (bool, error) {
// SetMessageReactionWithContext is the context-aware variant of SetMessageReaction. // SetMessageReactionWithContext is the context-aware variant of SetMessageReaction.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#setmessagereaction // See https://core.telegram.org/bots/api#setmessagereaction
func (api *API) SetMessageReactionWithContext(ctx context.Context, params SetMessageReactionP) (bool, error) { func (api *API) SetMessageReactionWithContext(ctx context.Context, params SetMessageReaction) (bool, error) {
req := NewRequestWithChatID[bool]("setMessageReaction", params, params.ChatID) req := NewRequestWithChatID[bool]("setMessageReaction", params, params.ChatID)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// EditMessageTextP holds parameters for the editMessageText method. // EditMessageText holds parameters for the editMessageText method.
// See https://core.telegram.org/bots/api#editmessagetext // See https://core.telegram.org/bots/api#editmessagetext
type EditMessageTextP struct { type EditMessageText struct {
BusinessConnectionID string `json:"business_connection_id,omitempty"` BusinessConnectionID string `json:"business_connection_id,omitempty"`
ChatID int64 `json:"chat_id,omitempty"` ChatID int64 `json:"chat_id,omitempty"`
MessageID int `json:"message_id,omitempty"` MessageID int `json:"message_id,omitempty"`
@@ -503,7 +511,7 @@ type EditMessageTextP struct {
// If inline_message_id is provided, returns a boolean success flag; // If inline_message_id is provided, returns a boolean success flag;
// otherwise returns the edited Message. // otherwise returns the edited Message.
// See https://core.telegram.org/bots/api#editmessagetext // See https://core.telegram.org/bots/api#editmessagetext
func (api *API) EditMessageText(params EditMessageTextP) (Message, bool, error) { func (api *API) EditMessageText(params EditMessageText) (Message, bool, error) {
var zero Message var zero Message
if params.InlineMessageID != "" { if params.InlineMessageID != "" {
req := NewRequestWithChatID[bool]("editMessageText", params, params.ChatID) req := NewRequestWithChatID[bool]("editMessageText", params, params.ChatID)
@@ -518,7 +526,7 @@ func (api *API) EditMessageText(params EditMessageTextP) (Message, bool, error)
// EditMessageTextWithContext is the context-aware variant of EditMessageText. // EditMessageTextWithContext is the context-aware variant of EditMessageText.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#editmessagetext // See https://core.telegram.org/bots/api#editmessagetext
func (api *API) EditMessageTextWithContext(ctx context.Context, params EditMessageTextP) (Message, bool, error) { func (api *API) EditMessageTextWithContext(ctx context.Context, params EditMessageText) (Message, bool, error) {
var zero Message var zero Message
if params.InlineMessageID != "" { if params.InlineMessageID != "" {
req := NewRequestWithChatID[bool]("editMessageText", params, params.ChatID) req := NewRequestWithChatID[bool]("editMessageText", params, params.ChatID)
@@ -530,9 +538,9 @@ func (api *API) EditMessageTextWithContext(ctx context.Context, params EditMessa
return res, false, err return res, false, err
} }
// EditMessageCaptionP holds parameters for the editMessageCaption method. // EditMessageCaption holds parameters for the editMessageCaption method.
// See https://core.telegram.org/bots/api#editmessagecaption // See https://core.telegram.org/bots/api#editmessagecaption
type EditMessageCaptionP struct { type EditMessageCaption struct {
BusinessConnectionID string `json:"business_connection_id,omitempty"` BusinessConnectionID string `json:"business_connection_id,omitempty"`
ChatID int64 `json:"chat_id,omitempty"` ChatID int64 `json:"chat_id,omitempty"`
MessageID int `json:"message_id,omitempty"` MessageID int `json:"message_id,omitempty"`
@@ -548,7 +556,7 @@ type EditMessageCaptionP struct {
// If inline_message_id is provided, returns a boolean success flag; // If inline_message_id is provided, returns a boolean success flag;
// otherwise returns the edited Message. // otherwise returns the edited Message.
// See https://core.telegram.org/bots/api#editmessagecaption // See https://core.telegram.org/bots/api#editmessagecaption
func (api *API) EditMessageCaption(params EditMessageCaptionP) (Message, bool, error) { func (api *API) EditMessageCaption(params EditMessageCaption) (Message, bool, error) {
var zero Message var zero Message
if params.InlineMessageID != "" { if params.InlineMessageID != "" {
req := NewRequestWithChatID[bool]("editMessageCaption", params, params.ChatID) req := NewRequestWithChatID[bool]("editMessageCaption", params, params.ChatID)
@@ -563,7 +571,7 @@ func (api *API) EditMessageCaption(params EditMessageCaptionP) (Message, bool, e
// EditMessageCaptionWithContext is the context-aware variant of EditMessageCaption. // EditMessageCaptionWithContext is the context-aware variant of EditMessageCaption.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#editmessagecaption // See https://core.telegram.org/bots/api#editmessagecaption
func (api *API) EditMessageCaptionWithContext(ctx context.Context, params EditMessageCaptionP) (Message, bool, error) { func (api *API) EditMessageCaptionWithContext(ctx context.Context, params EditMessageCaption) (Message, bool, error) {
var zero Message var zero Message
if params.InlineMessageID != "" { if params.InlineMessageID != "" {
req := NewRequestWithChatID[bool]("editMessageCaption", params, params.ChatID) req := NewRequestWithChatID[bool]("editMessageCaption", params, params.ChatID)
@@ -575,9 +583,9 @@ func (api *API) EditMessageCaptionWithContext(ctx context.Context, params EditMe
return res, false, err return res, false, err
} }
// EditMessageMediaP holds parameters for the editMessageMedia method. // EditMessageMedia holds parameters for the editMessageMedia method.
// See https://core.telegram.org/bots/api#editmessagemedia // See https://core.telegram.org/bots/api#editmessagemedia
type EditMessageMediaP struct { type EditMessageMedia struct {
BusinessConnectionID string `json:"business_connection_id,omitempty"` BusinessConnectionID string `json:"business_connection_id,omitempty"`
ChatID int64 `json:"chat_id,omitempty"` ChatID int64 `json:"chat_id,omitempty"`
MessageID int `json:"message_id,omitempty"` MessageID int `json:"message_id,omitempty"`
@@ -590,7 +598,7 @@ type EditMessageMediaP struct {
// If inline_message_id is provided, returns a boolean success flag; // If inline_message_id is provided, returns a boolean success flag;
// otherwise returns the edited Message. // otherwise returns the edited Message.
// See https://core.telegram.org/bots/api#editmessagemedia // See https://core.telegram.org/bots/api#editmessagemedia
func (api *API) EditMessageMedia(params EditMessageMediaP) (Message, bool, error) { func (api *API) EditMessageMedia(params EditMessageMedia) (Message, bool, error) {
var zero Message var zero Message
if params.InlineMessageID != "" { if params.InlineMessageID != "" {
req := NewRequestWithChatID[bool]("editMessageMedia", params, params.ChatID) req := NewRequestWithChatID[bool]("editMessageMedia", params, params.ChatID)
@@ -605,7 +613,7 @@ func (api *API) EditMessageMedia(params EditMessageMediaP) (Message, bool, error
// EditMessageMediaWithContext is the context-aware variant of EditMessageMedia. // EditMessageMediaWithContext is the context-aware variant of EditMessageMedia.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#editmessagemedia // See https://core.telegram.org/bots/api#editmessagemedia
func (api *API) EditMessageMediaWithContext(ctx context.Context, params EditMessageMediaP) (Message, bool, error) { func (api *API) EditMessageMediaWithContext(ctx context.Context, params EditMessageMedia) (Message, bool, error) {
var zero Message var zero Message
if params.InlineMessageID != "" { if params.InlineMessageID != "" {
req := NewRequestWithChatID[bool]("editMessageMedia", params, params.ChatID) req := NewRequestWithChatID[bool]("editMessageMedia", params, params.ChatID)
@@ -617,9 +625,9 @@ func (api *API) EditMessageMediaWithContext(ctx context.Context, params EditMess
return res, false, err return res, false, err
} }
// EditMessageLiveLocationP holds parameters for the editMessageLiveLocation method. // EditMessageLiveLocation holds parameters for the editMessageLiveLocation method.
// See https://core.telegram.org/bots/api#editmessagelivelocation // See https://core.telegram.org/bots/api#editmessagelivelocation
type EditMessageLiveLocationP struct { type EditMessageLiveLocation struct {
BusinessConnectionID string `json:"business_connection_id,omitempty"` BusinessConnectionID string `json:"business_connection_id,omitempty"`
ChatID int64 `json:"chat_id,omitempty"` ChatID int64 `json:"chat_id,omitempty"`
MessageID int `json:"message_id,omitempty"` MessageID int `json:"message_id,omitempty"`
@@ -638,7 +646,7 @@ type EditMessageLiveLocationP struct {
// If inline_message_id is provided, returns a boolean success flag; // If inline_message_id is provided, returns a boolean success flag;
// otherwise returns the edited Message. // otherwise returns the edited Message.
// See https://core.telegram.org/bots/api#editmessagelivelocation // See https://core.telegram.org/bots/api#editmessagelivelocation
func (api *API) EditMessageLiveLocation(params EditMessageLiveLocationP) (Message, bool, error) { func (api *API) EditMessageLiveLocation(params EditMessageLiveLocation) (Message, bool, error) {
var zero Message var zero Message
if params.InlineMessageID != "" { if params.InlineMessageID != "" {
req := NewRequestWithChatID[bool]("editMessageLiveLocation", params, params.ChatID) req := NewRequestWithChatID[bool]("editMessageLiveLocation", params, params.ChatID)
@@ -653,7 +661,7 @@ func (api *API) EditMessageLiveLocation(params EditMessageLiveLocationP) (Messag
// EditMessageLiveLocationWithContext is the context-aware variant of EditMessageLiveLocation. // EditMessageLiveLocationWithContext is the context-aware variant of EditMessageLiveLocation.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#editmessagelivelocation // See https://core.telegram.org/bots/api#editmessagelivelocation
func (api *API) EditMessageLiveLocationWithContext(ctx context.Context, params EditMessageLiveLocationP) (Message, bool, error) { func (api *API) EditMessageLiveLocationWithContext(ctx context.Context, params EditMessageLiveLocation) (Message, bool, error) {
var zero Message var zero Message
if params.InlineMessageID != "" { if params.InlineMessageID != "" {
req := NewRequestWithChatID[bool]("editMessageLiveLocation", params, params.ChatID) req := NewRequestWithChatID[bool]("editMessageLiveLocation", params, params.ChatID)
@@ -665,9 +673,9 @@ func (api *API) EditMessageLiveLocationWithContext(ctx context.Context, params E
return res, false, err return res, false, err
} }
// StopMessageLiveLocationP holds parameters for the stopMessageLiveLocation method. // StopMessageLiveLocation holds parameters for the stopMessageLiveLocation method.
// See https://core.telegram.org/bots/api#stopmessagelivelocation // See https://core.telegram.org/bots/api#stopmessagelivelocation
type StopMessageLiveLocationP struct { type StopMessageLiveLocation struct {
BusinessConnectionID string `json:"business_connection_id,omitempty"` BusinessConnectionID string `json:"business_connection_id,omitempty"`
ChatID int64 `json:"chat_id,omitempty"` ChatID int64 `json:"chat_id,omitempty"`
MessageID int `json:"message_id,omitempty"` MessageID int `json:"message_id,omitempty"`
@@ -679,7 +687,7 @@ type StopMessageLiveLocationP struct {
// If inline_message_id is provided, returns a boolean success flag; // If inline_message_id is provided, returns a boolean success flag;
// otherwise returns the edited Message. // otherwise returns the edited Message.
// See https://core.telegram.org/bots/api#stopmessagelivelocation // See https://core.telegram.org/bots/api#stopmessagelivelocation
func (api *API) StopMessageLiveLocation(params StopMessageLiveLocationP) (Message, bool, error) { func (api *API) StopMessageLiveLocation(params StopMessageLiveLocation) (Message, bool, error) {
var zero Message var zero Message
if params.InlineMessageID != "" { if params.InlineMessageID != "" {
req := NewRequestWithChatID[bool]("stopMessageLiveLocation", params, params.ChatID) req := NewRequestWithChatID[bool]("stopMessageLiveLocation", params, params.ChatID)
@@ -694,7 +702,7 @@ func (api *API) StopMessageLiveLocation(params StopMessageLiveLocationP) (Messag
// StopMessageLiveLocationWithContext is the context-aware variant of StopMessageLiveLocation. // StopMessageLiveLocationWithContext is the context-aware variant of StopMessageLiveLocation.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#stopmessagelivelocation // See https://core.telegram.org/bots/api#stopmessagelivelocation
func (api *API) StopMessageLiveLocationWithContext(ctx context.Context, params StopMessageLiveLocationP) (Message, bool, error) { func (api *API) StopMessageLiveLocationWithContext(ctx context.Context, params StopMessageLiveLocation) (Message, bool, error) {
var zero Message var zero Message
if params.InlineMessageID != "" { if params.InlineMessageID != "" {
req := NewRequestWithChatID[bool]("stopMessageLiveLocation", params, params.ChatID) req := NewRequestWithChatID[bool]("stopMessageLiveLocation", params, params.ChatID)
@@ -706,8 +714,8 @@ func (api *API) StopMessageLiveLocationWithContext(ctx context.Context, params S
return res, false, err return res, false, err
} }
// EditMessageChecklistP holds parameters for the editMessageChecklist method. // EditMessageChecklist holds parameters for the editMessageChecklist method.
type EditMessageChecklistP struct { type EditMessageChecklist struct {
BusinessConnectionID string `json:"business_connection_id"` BusinessConnectionID string `json:"business_connection_id"`
ChatID int64 `json:"chat_id"` ChatID int64 `json:"chat_id"`
MessageID int `json:"message_id"` MessageID int `json:"message_id"`
@@ -717,7 +725,7 @@ type EditMessageChecklistP struct {
// EditMessageChecklist edits a checklist message. // EditMessageChecklist edits a checklist message.
// See https://core.telegram.org/bots/api#editmessagechecklist // See https://core.telegram.org/bots/api#editmessagechecklist
func (api *API) EditMessageChecklist(params EditMessageChecklistP) (Message, error) { func (api *API) EditMessageChecklist(params EditMessageChecklist) (Message, error) {
req := NewRequestWithChatID[Message]("editMessageChecklist", params, params.ChatID) req := NewRequestWithChatID[Message]("editMessageChecklist", params, params.ChatID)
return req.Do(api) return req.Do(api)
} }
@@ -725,14 +733,14 @@ func (api *API) EditMessageChecklist(params EditMessageChecklistP) (Message, err
// EditMessageChecklistWithContext is the context-aware variant of EditMessageChecklist. // EditMessageChecklistWithContext is the context-aware variant of EditMessageChecklist.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#editmessagechecklist // See https://core.telegram.org/bots/api#editmessagechecklist
func (api *API) EditMessageChecklistWithContext(ctx context.Context, params EditMessageChecklistP) (Message, error) { func (api *API) EditMessageChecklistWithContext(ctx context.Context, params EditMessageChecklist) (Message, error) {
req := NewRequestWithChatID[Message]("editMessageChecklist", params, params.ChatID) req := NewRequestWithChatID[Message]("editMessageChecklist", params, params.ChatID)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// EditMessageReplyMarkupP holds parameters for the editMessageReplyMarkup method. // EditMessageReplyMarkup holds parameters for the editMessageReplyMarkup method.
// See https://core.telegram.org/bots/api#editmessagereplymarkup // See https://core.telegram.org/bots/api#editmessagereplymarkup
type EditMessageReplyMarkupP struct { type EditMessageReplyMarkup struct {
BusinessConnectionID string `json:"business_connection_id,omitempty"` BusinessConnectionID string `json:"business_connection_id,omitempty"`
ChatID int64 `json:"chat_id,omitempty"` ChatID int64 `json:"chat_id,omitempty"`
MessageID int `json:"message_id,omitempty"` MessageID int `json:"message_id,omitempty"`
@@ -744,7 +752,7 @@ type EditMessageReplyMarkupP struct {
// If inline_message_id is provided, returns a boolean success flag; // If inline_message_id is provided, returns a boolean success flag;
// otherwise returns the edited Message. // otherwise returns the edited Message.
// See https://core.telegram.org/bots/api#editmessagereplymarkup // See https://core.telegram.org/bots/api#editmessagereplymarkup
func (api *API) EditMessageReplyMarkup(params EditMessageReplyMarkupP) (Message, bool, error) { func (api *API) EditMessageReplyMarkup(params EditMessageReplyMarkup) (Message, bool, error) {
var zero Message var zero Message
if params.InlineMessageID != "" { if params.InlineMessageID != "" {
req := NewRequestWithChatID[bool]("editMessageReplyMarkup", params, params.ChatID) req := NewRequestWithChatID[bool]("editMessageReplyMarkup", params, params.ChatID)
@@ -759,7 +767,7 @@ func (api *API) EditMessageReplyMarkup(params EditMessageReplyMarkupP) (Message,
// EditMessageReplyMarkupWithContext is the context-aware variant of EditMessageReplyMarkup. // EditMessageReplyMarkupWithContext is the context-aware variant of EditMessageReplyMarkup.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#editmessagereplymarkup // See https://core.telegram.org/bots/api#editmessagereplymarkup
func (api *API) EditMessageReplyMarkupWithContext(ctx context.Context, params EditMessageReplyMarkupP) (Message, bool, error) { func (api *API) EditMessageReplyMarkupWithContext(ctx context.Context, params EditMessageReplyMarkup) (Message, bool, error) {
var zero Message var zero Message
if params.InlineMessageID != "" { if params.InlineMessageID != "" {
req := NewRequestWithChatID[bool]("editMessageReplyMarkup", params, params.ChatID) req := NewRequestWithChatID[bool]("editMessageReplyMarkup", params, params.ChatID)
@@ -771,9 +779,9 @@ func (api *API) EditMessageReplyMarkupWithContext(ctx context.Context, params Ed
return res, false, err return res, false, err
} }
// StopPollP holds parameters for the stopPoll method. // StopPoll holds parameters for the stopPoll method.
// See https://core.telegram.org/bots/api#stoppoll // See https://core.telegram.org/bots/api#stoppoll
type StopPollP struct { type StopPoll struct {
BusinessConnectionID string `json:"business_connection_id,omitempty"` BusinessConnectionID string `json:"business_connection_id,omitempty"`
ChatID int64 `json:"chat_id"` ChatID int64 `json:"chat_id"`
MessageID int `json:"message_id"` MessageID int `json:"message_id"`
@@ -783,7 +791,7 @@ type StopPollP struct {
// StopPoll stops a poll that was sent by the bot. // StopPoll stops a poll that was sent by the bot.
// Returns the stopped Poll. // Returns the stopped Poll.
// See https://core.telegram.org/bots/api#stoppoll // See https://core.telegram.org/bots/api#stoppoll
func (api *API) StopPoll(params StopPollP) (Poll, error) { func (api *API) StopPoll(params StopPoll) (Poll, error) {
req := NewRequestWithChatID[Poll]("stopPoll", params, params.ChatID) req := NewRequestWithChatID[Poll]("stopPoll", params, params.ChatID)
return req.Do(api) return req.Do(api)
} }
@@ -791,14 +799,14 @@ func (api *API) StopPoll(params StopPollP) (Poll, error) {
// StopPollWithContext is the context-aware variant of StopPoll. // StopPollWithContext is the context-aware variant of StopPoll.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#stoppoll // See https://core.telegram.org/bots/api#stoppoll
func (api *API) StopPollWithContext(ctx context.Context, params StopPollP) (Poll, error) { func (api *API) StopPollWithContext(ctx context.Context, params StopPoll) (Poll, error) {
req := NewRequestWithChatID[Poll]("stopPoll", params, params.ChatID) req := NewRequestWithChatID[Poll]("stopPoll", params, params.ChatID)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// ApproveSuggestedPostP holds parameters for the approveSuggestedPost method. // ApproveSuggestedPost holds parameters for the approveSuggestedPost method.
// See https://core.telegram.org/bots/api#approvesuggestedpost // See https://core.telegram.org/bots/api#approvesuggestedpost
type ApproveSuggestedPostP struct { type ApproveSuggestedPost struct {
ChatID int64 `json:"chat_id"` ChatID int64 `json:"chat_id"`
MessageID int `json:"message_id"` MessageID int `json:"message_id"`
SendDate int `json:"send_date,omitempty"` SendDate int `json:"send_date,omitempty"`
@@ -807,7 +815,7 @@ type ApproveSuggestedPostP struct {
// ApproveSuggestedPost approves a suggested channel post. // ApproveSuggestedPost approves a suggested channel post.
// Returns True on success. // Returns True on success.
// See https://core.telegram.org/bots/api#approvesuggestedpost // See https://core.telegram.org/bots/api#approvesuggestedpost
func (api *API) ApproveSuggestedPost(params ApproveSuggestedPostP) (bool, error) { func (api *API) ApproveSuggestedPost(params ApproveSuggestedPost) (bool, error) {
req := NewRequestWithChatID[bool]("approveSuggestedPost", params, params.ChatID) req := NewRequestWithChatID[bool]("approveSuggestedPost", params, params.ChatID)
return req.Do(api) return req.Do(api)
} }
@@ -815,14 +823,14 @@ func (api *API) ApproveSuggestedPost(params ApproveSuggestedPostP) (bool, error)
// ApproveSuggestedPostWithContext is the context-aware variant of ApproveSuggestedPost. // ApproveSuggestedPostWithContext is the context-aware variant of ApproveSuggestedPost.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#approvesuggestedpost // See https://core.telegram.org/bots/api#approvesuggestedpost
func (api *API) ApproveSuggestedPostWithContext(ctx context.Context, params ApproveSuggestedPostP) (bool, error) { func (api *API) ApproveSuggestedPostWithContext(ctx context.Context, params ApproveSuggestedPost) (bool, error) {
req := NewRequestWithChatID[bool]("approveSuggestedPost", params, params.ChatID) req := NewRequestWithChatID[bool]("approveSuggestedPost", params, params.ChatID)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// DeclineSuggestedPostP holds parameters for the declineSuggestedPost method. // DeclineSuggestedPost holds parameters for the declineSuggestedPost method.
// See https://core.telegram.org/bots/api#declinesuggestedpost // See https://core.telegram.org/bots/api#declinesuggestedpost
type DeclineSuggestedPostP struct { type DeclineSuggestedPost struct {
ChatID int64 `json:"chat_id"` ChatID int64 `json:"chat_id"`
MessageID int `json:"message_id"` MessageID int `json:"message_id"`
Comment string `json:"comment,omitempty"` Comment string `json:"comment,omitempty"`
@@ -831,7 +839,7 @@ type DeclineSuggestedPostP struct {
// DeclineSuggestedPost declines a suggested channel post. // DeclineSuggestedPost declines a suggested channel post.
// Returns True on success. // Returns True on success.
// See https://core.telegram.org/bots/api#declinesuggestedpost // See https://core.telegram.org/bots/api#declinesuggestedpost
func (api *API) DeclineSuggestedPost(params DeclineSuggestedPostP) (bool, error) { func (api *API) DeclineSuggestedPost(params DeclineSuggestedPost) (bool, error) {
req := NewRequestWithChatID[bool]("declineSuggestedPost", params, params.ChatID) req := NewRequestWithChatID[bool]("declineSuggestedPost", params, params.ChatID)
return req.Do(api) return req.Do(api)
} }
@@ -839,14 +847,14 @@ func (api *API) DeclineSuggestedPost(params DeclineSuggestedPostP) (bool, error)
// DeclineSuggestedPostWithContext is the context-aware variant of DeclineSuggestedPost. // DeclineSuggestedPostWithContext is the context-aware variant of DeclineSuggestedPost.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#declinesuggestedpost // See https://core.telegram.org/bots/api#declinesuggestedpost
func (api *API) DeclineSuggestedPostWithContext(ctx context.Context, params DeclineSuggestedPostP) (bool, error) { func (api *API) DeclineSuggestedPostWithContext(ctx context.Context, params DeclineSuggestedPost) (bool, error) {
req := NewRequestWithChatID[bool]("declineSuggestedPost", params, params.ChatID) req := NewRequestWithChatID[bool]("declineSuggestedPost", params, params.ChatID)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// DeleteMessageP holds parameters for the deleteMessage method. // DeleteMessage holds parameters for the deleteMessage method.
// See https://core.telegram.org/bots/api#deletemessage // See https://core.telegram.org/bots/api#deletemessage
type DeleteMessageP struct { type DeleteMessage struct {
ChatID int64 `json:"chat_id"` ChatID int64 `json:"chat_id"`
MessageID int `json:"message_id"` MessageID int `json:"message_id"`
} }
@@ -854,7 +862,7 @@ type DeleteMessageP struct {
// DeleteMessage deletes a message. // DeleteMessage deletes a message.
// Returns True on success. // Returns True on success.
// See https://core.telegram.org/bots/api#deletemessage // See https://core.telegram.org/bots/api#deletemessage
func (api *API) DeleteMessage(params DeleteMessageP) (bool, error) { func (api *API) DeleteMessage(params DeleteMessage) (bool, error) {
req := NewRequestWithChatID[bool]("deleteMessage", params, params.ChatID) req := NewRequestWithChatID[bool]("deleteMessage", params, params.ChatID)
return req.Do(api) return req.Do(api)
} }
@@ -862,14 +870,14 @@ func (api *API) DeleteMessage(params DeleteMessageP) (bool, error) {
// DeleteMessageWithContext is the context-aware variant of DeleteMessage. // DeleteMessageWithContext is the context-aware variant of DeleteMessage.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#deletemessage // See https://core.telegram.org/bots/api#deletemessage
func (api *API) DeleteMessageWithContext(ctx context.Context, params DeleteMessageP) (bool, error) { func (api *API) DeleteMessageWithContext(ctx context.Context, params DeleteMessage) (bool, error) {
req := NewRequestWithChatID[bool]("deleteMessage", params, params.ChatID) req := NewRequestWithChatID[bool]("deleteMessage", params, params.ChatID)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// DeleteMessagesP holds parameters for the deleteMessages method. // DeleteMessages holds parameters for the deleteMessages method.
// See https://core.telegram.org/bots/api#deletemessages // See https://core.telegram.org/bots/api#deletemessages
type DeleteMessagesP struct { type DeleteMessages struct {
ChatID int64 `json:"chat_id"` ChatID int64 `json:"chat_id"`
MessageIDs []int `json:"message_ids"` MessageIDs []int `json:"message_ids"`
} }
@@ -877,7 +885,7 @@ type DeleteMessagesP struct {
// DeleteMessages deletes multiple messages at once. // DeleteMessages deletes multiple messages at once.
// Returns True on success. // Returns True on success.
// See https://core.telegram.org/bots/api#deletemessages // See https://core.telegram.org/bots/api#deletemessages
func (api *API) DeleteMessages(params DeleteMessagesP) (bool, error) { func (api *API) DeleteMessages(params DeleteMessages) (bool, error) {
req := NewRequestWithChatID[bool]("deleteMessages", params, params.ChatID) req := NewRequestWithChatID[bool]("deleteMessages", params, params.ChatID)
return req.Do(api) return req.Do(api)
} }
@@ -885,14 +893,14 @@ func (api *API) DeleteMessages(params DeleteMessagesP) (bool, error) {
// DeleteMessagesWithContext is the context-aware variant of DeleteMessages. // DeleteMessagesWithContext is the context-aware variant of DeleteMessages.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#deletemessages // See https://core.telegram.org/bots/api#deletemessages
func (api *API) DeleteMessagesWithContext(ctx context.Context, params DeleteMessagesP) (bool, error) { func (api *API) DeleteMessagesWithContext(ctx context.Context, params DeleteMessages) (bool, error) {
req := NewRequestWithChatID[bool]("deleteMessages", params, params.ChatID) req := NewRequestWithChatID[bool]("deleteMessages", params, params.ChatID)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// AnswerCallbackQueryP holds parameters for the answerCallbackQuery method. // AnswerCallbackQuery holds parameters for the answerCallbackQuery method.
// See https://core.telegram.org/bots/api#answercallbackquery // See https://core.telegram.org/bots/api#answercallbackquery
type AnswerCallbackQueryP struct { type AnswerCallbackQuery struct {
CallbackQueryID string `json:"callback_query_id"` CallbackQueryID string `json:"callback_query_id"`
Text string `json:"text,omitempty"` Text string `json:"text,omitempty"`
ShowAlert bool `json:"show_alert,omitempty"` ShowAlert bool `json:"show_alert,omitempty"`
@@ -903,7 +911,7 @@ type AnswerCallbackQueryP struct {
// AnswerCallbackQuery sends answers to callback queries sent from inline keyboards. // AnswerCallbackQuery sends answers to callback queries sent from inline keyboards.
// Returns True on success. // Returns True on success.
// See https://core.telegram.org/bots/api#answercallbackquery // See https://core.telegram.org/bots/api#answercallbackquery
func (api *API) AnswerCallbackQuery(params AnswerCallbackQueryP) (bool, error) { func (api *API) AnswerCallbackQuery(params AnswerCallbackQuery) (bool, error) {
req := NewRequest[bool]("answerCallbackQuery", params) req := NewRequest[bool]("answerCallbackQuery", params)
return req.Do(api) return req.Do(api)
} }
@@ -911,7 +919,7 @@ func (api *API) AnswerCallbackQuery(params AnswerCallbackQueryP) (bool, error) {
// AnswerCallbackQueryWithContext is the context-aware variant of AnswerCallbackQuery. // AnswerCallbackQueryWithContext is the context-aware variant of AnswerCallbackQuery.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#answercallbackquery // See https://core.telegram.org/bots/api#answercallbackquery
func (api *API) AnswerCallbackQueryWithContext(ctx context.Context, params AnswerCallbackQueryP) (bool, error) { func (api *API) AnswerCallbackQueryWithContext(ctx context.Context, params AnswerCallbackQuery) (bool, error) {
req := NewRequest[bool]("answerCallbackQuery", params) req := NewRequest[bool]("answerCallbackQuery", params)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
+419 -113
View File
@@ -1,64 +1,222 @@
package tgapi package tgapi
import "git.nix13.pw/scuroneko/extypes" import (
"encoding/json"
"git.scuroneko.dev/scuroneko/extypes"
)
// MessageID represents a message identifier wrapper returned by some API methods. // MessageID represents a message identifier wrapper returned by some API methods.
type MessageID struct { type MessageID struct {
MessageID int `json:"message_id"` MessageID int `json:"message_id"`
} }
// MessageReplyMarkup represents an inline keyboard markup for a message.
// It is used in the Message type.
type MessageReplyMarkup struct {
InlineKeyboard [][]InlineKeyboardButton `json:"inline_keyboard"`
}
// DirectMessageTopic represents a forum topic in a direct message. // DirectMessageTopic represents a forum topic in a direct message.
type DirectMessageTopic struct { type DirectMessageTopic struct {
TopicID int64 `json:"topic_id"` TopicID int64 `json:"topic_id"`
User *User `json:"user,omitempty"` User *User `json:"user,omitempty"`
} }
type MessageOriginType string
const (
MessageOriginUserType = "user"
MessageOriginHiddenUserType = "hidden_user"
MessageOriginChatType = "chat"
MessageOriginChannel = "channel"
)
type MessageOrigin struct {
Type MessageOriginType `json:"type"`
Date int64 `json:"date"`
SenderUser *User `json:"sender_user,omitempty"`
SenderUserName string `json:"sender_user_name,omitempty"`
SenderChat *Chat `json:"sender_chat,omitempty"`
Chat *Chat `json:"chat,omitempty"`
MessageID int `json:"message_id"`
AuthorSignature string `json:"author_signature,omitempty"`
}
type ExternalReplyInfo struct {
Origin MessageOrigin `json:"origin"`
Chat *Chat `json:"chat,omitempty"`
MessageID int `json:"message_id,omitempty"`
LinkPreviewOptions *LinkPreviewOptions `json:"link_preview_options,omitempty"`
Animation *Animation `json:"animation,omitempty"`
Audio *Audio `json:"audio,omitempty"`
Document *Document `json:"document,omitempty"`
PaidMedia *PaidMediaInfo `json:"paid_media,omitempty"`
Photo []PhotoSize `json:"photo,omitempty"`
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"`
Contact *Contact `json:"contact,omitempty"`
Dice *Dice `json:"dice,omitempty"`
Game *Game `json:"game,omitempty"`
Giveaway *Giveaway `json:"giveaway,omitempty"`
GiveawayWinners *GiveawayWinners `json:"giveaway_winners,omitempty"`
Invoice *Invoice `json:"invoice,omitempty"`
Location *Location `json:"location,omitempty"`
Poll *Poll `json:"poll,omitempty"`
Venue *Venue `json:"venue,omitempty"`
}
type TextQuote struct {
Text string `json:"text"`
Entities []MessageEntity `json:"entities"`
Position int `json:"position"`
IsManual bool `json:"is_manual,omitempty"`
}
type MessageAutoDeleteTimerChanged struct {
MessageAutoDeleteTime int `json:"message_auto_delete_time"`
}
type DirectMessagePriceChanged struct {
AreDirectMessagesEnabled bool `json:"are_direct_messages_enabled"`
DirectMessageStarCount int `json:"direct_message_star_count,omitempty"`
}
type PaidMessagePriceChanged struct {
PaidMessageStarCount int `json:"paid_message_star_count"`
}
// Message represents a Telegram message. // Message represents a Telegram message.
// See https://core.telegram.org/bots/api#message // See https://core.telegram.org/bots/api#message
type Message struct { type Message struct {
MessageID int `json:"message_id"` MessageID int `json:"message_id"`
MessageThreadID int `json:"message_thread_id,omitempty"` MessageThreadID int `json:"message_thread_id,omitempty"`
DirectMessageTopic *DirectMessageTopic `json:"direct_message_topic,omitempty"` DirectMessageTopic *DirectMessageTopic `json:"direct_message_topic,omitempty"`
BusinessConnectionId string `json:"business_connection_id,omitempty"` From *User `json:"from,omitempty"`
From *User `json:"from,omitempty"`
SenderChat *Chat `json:"sender_chat,omitempty"` SenderChat *Chat `json:"sender_chat,omitempty"`
SenderBoostCount int `json:"sender_boost_count,omitempty"` SenderBoostCount int `json:"sender_boost_count,omitempty"`
SenderBusinessBot *User `json:"sender_business_bot,omitempty"` SenderBusinessBot *User `json:"sender_business_bot,omitempty"`
SenderTag string `json:"sender_tag,omitempty"` SenderTag string `json:"sender_tag,omitempty"`
Chat *Chat `json:"chat,omitempty"` Date int `json:"date"`
BusinessConnectionId string `json:"business_connection_id,omitempty"`
Chat *Chat `json:"chat,omitempty"`
ForwardOrigin *MessageOrigin `json:"forward_origin,omitempty"`
IsTopicMessage bool `json:"is_topic_message,omitempty"` IsTopicMessage bool `json:"is_topic_message,omitempty"`
IsAutomaticForward bool `json:"is_automatic_forward,omitempty"` IsAutomaticForward bool `json:"is_automatic_forward,omitempty"`
IsFromOffline bool `json:"is_from_offline,omitempty"` ReplyToMessage *Message `json:"reply_to_message,omitempty"`
IsPaidPost bool `json:"is_paid_post,omitempty"` ExternalReply *ExternalReplyInfo `json:"external_reply,omitempty"`
MediaGroupId string `json:"media_group_id,omitempty"` Quote *TextQuote `json:"quote,omitempty"`
AuthorSignature string `json:"author_signature,omitempty"`
PaidStarCount int `json:"paid_star_count,omitempty"`
ReplyToMessage *Message `json:"reply_to_message,omitempty"`
Text string `json:"text"` ReplyToStory *Story `json:"reply_to_story,omitempty"`
ReplyToChecklistTaskID int `json:"reply_to_checklist_task_id,omitempty"`
Photo extypes.Slice[*PhotoSize] `json:"photo,omitempty"` ReplyToPollOptionID string `json:"reply_to_poll_option_id,omitempty"`
Caption string `json:"caption,omitempty"` ViaBot *User `json:"via_bot,omitempty"`
CaptionEntities []MessageEntity `json:"caption_entities,omitempty"` EditDate int `json:"edit_date,omitempty"`
HasProtectedContent bool `json:"has_protected_content,omitempty"`
Date int `json:"date"` IsFromOffline bool `json:"is_from_offline,omitempty"`
EditDate int `json:"edit_date"` IsPaidPost bool `json:"is_paid_post,omitempty"`
MediaGroupId string `json:"media_group_id,omitempty"`
ReplyMarkup *MessageReplyMarkup `json:"reply_markup,omitempty"` AuthorSignature string `json:"author_signature,omitempty"`
PaidStarCount int `json:"paid_star_count,omitempty"`
Text string `json:"text"`
Entities []MessageEntity `json:"entities,omitempty"` Entities []MessageEntity `json:"entities,omitempty"`
LinkPreviewOptions *LinkPreviewOptions `json:"link_preview_options,omitempty"` LinkPreviewOptions *LinkPreviewOptions `json:"link_preview_options,omitempty"`
SuggestedPostInfo *SuggestedPostInfo `json:"suggested_post_info,omitempty"` SuggestedPostInfo *SuggestedPostInfo `json:"suggested_post_info,omitempty"`
EffectID string `json:"effect_id,omitempty"`
EffectID string `json:"effect_id,omitempty"` Animation *Animation `json:"animation,omitempty"`
Audio *Audio `json:"audio,omitempty"`
Document *Document `json:"document,omitempty"`
PaidMedia *PaidMediaInfo `json:"paid_media,omitempty"`
Photo extypes.Slice[PhotoSize] `json:"photo,omitempty"`
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"`
Contact *Contact `json:"contact,omitempty"`
Dice *Dice `json:"dice,omitempty"`
Game *Game `json:"game,omitempty"`
Poll *Poll `json:"poll,omitempty"`
Venue *Venue `json:"venue,omitempty"`
Location *Location `json:"location,omitempty"`
NewChatMembers []User `json:"new_chat_members,omitempty"`
LeftChatMember *User `json:"left_chat_member,omitempty"`
ChatOwnerLeft *ChatOwnerLeft `json:"chat_owner_left,omitempty"`
ChatOwnerChanged *ChatOwnerChanged `json:"chat_owner_changed,omitempty"`
NewChatTitle string `json:"new_chat_title,omitempty"`
NewChatPhoto []PhotoSize `json:"new_chat_photo,omitempty"`
DeleteChatPhoto bool `json:"delete_chat_photo,omitempty"`
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"`
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"`
ConnectedWebsite string `json:"connected_website,omitempty"`
WriteAccessAllowed *WriteAccessAllowed `json:"write_access_allowed,omitempty"`
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"`
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"`
GiveawayCreated *GiveawayCreated `json:"giveaway_created,omitempty"`
Giveaway *Giveaway `json:"giveaway,omitempty"`
GiveawayWinners *GiveawayWinners `json:"giveaway_winners,omitempty"`
GiveawayCompleted *GiveawayCompleted `json:"giveaway_completed,omitempty"`
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"`
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"`
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"`
WebAppData *WebAppData `json:"web_app_data,omitempty"`
ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"`
} }
// InaccessibleMessage describes a message that was deleted or is otherwise inaccessible. // InaccessibleMessage describes a message that was deleted or is otherwise inaccessible.
@@ -71,32 +229,125 @@ type InaccessibleMessage struct {
// MaybeInaccessibleMessage is a union type that can be either Message or InaccessibleMessage. // MaybeInaccessibleMessage is a union type that can be either Message or InaccessibleMessage.
// See https://core.telegram.org/bots/api#maybeinaccessiblemessage // See https://core.telegram.org/bots/api#maybeinaccessiblemessage
type MaybeInaccessibleMessage interface{ Message | InaccessibleMessage } type MaybeInaccessibleMessage struct {
msg *Message
ina *InaccessibleMessage
}
// UnmarshalJSON decodes either an accessible Message or an InaccessibleMessage.
func (m *MaybeInaccessibleMessage) UnmarshalJSON(data []byte) error {
tmp := struct {
Date int `json:"date"`
}{}
if err := json.Unmarshal(data, &tmp); err != nil {
return err
}
var err error
if tmp.Date > 0 {
err = json.Unmarshal(data, &m.msg)
} else {
err = json.Unmarshal(data, &m.ina)
}
if err != nil {
return err
}
return nil
}
// MarshalJSON encodes the populated accessible or inaccessible message payload.
func (m *MaybeInaccessibleMessage) MarshalJSON() ([]byte, error) {
if m.msg != nil {
return json.Marshal(m.msg)
} else if m.ina != nil {
return json.Marshal(m.ina)
}
return json.Marshal(nil)
}
// Message returns the accessible message payload when present.
func (m *MaybeInaccessibleMessage) Message() *Message {
return m.msg
}
// InaccessibleMessage returns the inaccessible message payload when present.
func (m *MaybeInaccessibleMessage) InaccessibleMessage() *InaccessibleMessage {
return m.ina
}
// IsAccessible reports whether the payload is an accessible message.
func (m *MaybeInaccessibleMessage) IsAccessible() bool {
return m.msg != nil
}
// IsInaccessible reports whether the payload is an inaccessible message.
func (m *MaybeInaccessibleMessage) IsInaccessible() bool {
return m.ina != nil
}
// MessageID returns the message identifier from either payload form.
func (m *MaybeInaccessibleMessage) MessageID() int {
if m.IsAccessible() {
return m.msg.MessageID
} else if m.IsInaccessible() {
return m.ina.MessageID
}
return 0
}
// Chat returns the chat from either payload form.
func (m *MaybeInaccessibleMessage) Chat() *Chat {
if m.IsAccessible() {
return m.msg.Chat
} else if m.IsInaccessible() {
return &m.ina.Chat
}
return nil
}
// MessageEntityType represents the type of a message entity. // MessageEntityType represents the type of a message entity.
type MessageEntityType string type MessageEntityType string
const ( const (
MessageEntityMention MessageEntityType = "mention" // MessageEntityMention identifies an @mention entity.
MessageEntityHashtag MessageEntityType = "hashtag" MessageEntityMention MessageEntityType = "mention"
MessageEntityCashtag MessageEntityType = "cashtag" // MessageEntityHashtag identifies a hashtag entity.
MessageEntityBotCommand MessageEntityType = "bot_command" MessageEntityHashtag MessageEntityType = "hashtag"
MessageEntityUrl MessageEntityType = "url" // MessageEntityCashtag identifies a cashtag entity.
MessageEntityEmail MessageEntityType = "email" MessageEntityCashtag MessageEntityType = "cashtag"
MessageEntityPhoneNumber MessageEntityType = "phone_number" // MessageEntityBotCommand identifies a bot command entity.
MessageEntityBold MessageEntityType = "bold" MessageEntityBotCommand MessageEntityType = "bot_command"
MessageEntityItalic MessageEntityType = "italic" // MessageEntityUrl identifies a URL entity.
MessageEntityUnderline MessageEntityType = "underline" MessageEntityUrl MessageEntityType = "url"
MessageEntityStrike MessageEntityType = "strikethrough" // MessageEntityEmail identifies an email entity.
MessageEntitySpoiler MessageEntityType = "spoiler" MessageEntityEmail MessageEntityType = "email"
MessageEntityBlockquote MessageEntityType = "blockquote" // MessageEntityPhoneNumber identifies a phone number entity.
MessageEntityPhoneNumber MessageEntityType = "phone_number"
// MessageEntityBold identifies bold text.
MessageEntityBold MessageEntityType = "bold"
// MessageEntityItalic identifies italic text.
MessageEntityItalic MessageEntityType = "italic"
// MessageEntityUnderline identifies underlined text.
MessageEntityUnderline MessageEntityType = "underline"
// MessageEntityStrike identifies strikethrough text.
MessageEntityStrike MessageEntityType = "strikethrough"
// MessageEntitySpoiler identifies spoiler text.
MessageEntitySpoiler MessageEntityType = "spoiler"
// MessageEntityBlockquote identifies a blockquote entity.
MessageEntityBlockquote MessageEntityType = "blockquote"
// MessageEntityExpandableBlockquote identifies an expandable blockquote entity.
MessageEntityExpandableBlockquote MessageEntityType = "expandable_blockquote" MessageEntityExpandableBlockquote MessageEntityType = "expandable_blockquote"
MessageEntityCode MessageEntityType = "code" // MessageEntityCode identifies inline code.
MessageEntityPre MessageEntityType = "pre" MessageEntityCode MessageEntityType = "code"
MessageEntityTextLink MessageEntityType = "text_link" // MessageEntityPre identifies a preformatted block.
MessageEntityTextMention MessageEntityType = "text_mention" MessageEntityPre MessageEntityType = "pre"
MessageEntityCustomEmoji MessageEntityType = "custom_emoji" // MessageEntityTextLink identifies linked text.
MessageEntityDateTime MessageEntityType = "date_time" MessageEntityTextLink MessageEntityType = "text_link"
// MessageEntityTextMention identifies a text mention.
MessageEntityTextMention MessageEntityType = "text_mention"
// MessageEntityCustomEmoji identifies a custom emoji entity.
MessageEntityCustomEmoji MessageEntityType = "custom_emoji"
// MessageEntityDateTime identifies a date-time entity.
MessageEntityDateTime MessageEntityType = "date_time"
) )
// MessageEntity represents one special entity in a text message. // MessageEntity represents one special entity in a text message.
@@ -121,12 +372,13 @@ type ReplyParameters struct {
MessageID int `json:"message_id"` MessageID int `json:"message_id"`
ChatID int64 `json:"chat_id,omitempty"` ChatID int64 `json:"chat_id,omitempty"`
AllowSendingWithoutReply bool `json:"allow_sending_without_reply,omitempty"` AllowSendingWithoutReply bool `json:"allow_sending_without_reply,omitempty"`
Quote string `json:"quote,omitempty"` Quote string `json:"quote,omitempty"`
QuoteParsingMode string `json:"quote_parsing_mode,omitempty"` QuoteParsingMode string `json:"quote_parsing_mode,omitempty"`
QuoteEntities []*MessageEntity `json:"quote_entities,omitempty"` QuoteEntities []MessageEntity `json:"quote_entities,omitempty"`
QuotePosition int `json:"quote_position,omitempty"` QuotePosition int `json:"quote_position,omitempty"`
ChecklistTaskID int `json:"checklist_task_id,omitempty"` ChecklistTaskID int `json:"checklist_task_id,omitempty"`
PollOptionID string `json:"poll_option_id,omitempty"`
} }
// LinkPreviewOptions describes the options used for link preview generation. // LinkPreviewOptions describes the options used for link preview generation.
@@ -166,23 +418,27 @@ type InlineKeyboardMarkup struct {
type KeyboardButtonStyle string type KeyboardButtonStyle string
const ( const (
KeyboardButtonStyleDanger KeyboardButtonStyle = "danger" // KeyboardButtonStyleDanger marks a destructive keyboard button.
KeyboardButtonStyleDanger KeyboardButtonStyle = "danger"
// KeyboardButtonStyleSuccess marks a confirmatory keyboard button.
KeyboardButtonStyleSuccess KeyboardButtonStyle = "success" KeyboardButtonStyleSuccess KeyboardButtonStyle = "success"
// KeyboardButtonStylePrimary marks a primary keyboard button.
KeyboardButtonStylePrimary KeyboardButtonStyle = "primary" KeyboardButtonStylePrimary KeyboardButtonStyle = "primary"
) )
// KeyboardButton represents one button of the reply keyboard. // KeyboardButton represents one button of the reply keyboard.
// See https://core.telegram.org/bots/api#keyboardbutton // See https://core.telegram.org/bots/api#keyboardbutton
type KeyboardButton struct { type KeyboardButton struct {
Text string `json:"text"` Text string `json:"text"`
IconCustomEmojiID string `json:"icon_custom_emoji_id,omitempty"` IconCustomEmojiID string `json:"icon_custom_emoji_id,omitempty"`
Style KeyboardButtonStyle `json:"style,omitempty"` Style KeyboardButtonStyle `json:"style,omitempty"`
RequestUsers *KeyboardButtonRequestUsers `json:"request_users,omitempty"` RequestUsers *KeyboardButtonRequestUsers `json:"request_users,omitempty"`
RequestChat *KeyboardButtonRequestChat `json:"request_chat,omitempty"` RequestChat *KeyboardButtonRequestChat `json:"request_chat,omitempty"`
RequestContact bool `json:"request_contact,omitempty"` RequestManagedBot *KeyboardButtonRequestManagedBot `json:"request_managed_bot,omitempty"`
RequestLocation bool `json:"request_location,omitempty"` RequestContact bool `json:"request_contact,omitempty"`
RequestPoll *KeyboardButtonPollType `json:"request_poll,omitempty"` RequestLocation bool `json:"request_location,omitempty"`
WebApp *WebAppInfo `json:"web_app,omitempty"` RequestPoll *KeyboardButtonPollType `json:"request_poll,omitempty"`
WebApp *WebAppInfo `json:"web_app,omitempty"`
} }
// KeyboardButtonRequestUsers defines criteria used to request suitable users. // KeyboardButtonRequestUsers defines criteria used to request suitable users.
@@ -213,6 +469,14 @@ type KeyboardButtonRequestChat struct {
RequestPhoto bool `json:"request_photo,omitempty"` RequestPhoto bool `json:"request_photo,omitempty"`
} }
// KeyboardButtonRequestManagedBot defines criteria used to request a managed bot.
// See https://core.telegram.org/bots/api#keyboardbuttonrequestmanagedbot
type KeyboardButtonRequestManagedBot struct {
RequestID int32 `json:"request_id"`
SuggestedName string `json:"suggested_name,omitempty"`
SuggestedUsername string `json:"suggested_username,omitempty"`
}
// KeyboardButtonPollType represents the type of a poll that may be created from a keyboard button. // KeyboardButtonPollType represents the type of a poll that may be created from a keyboard button.
// See https://core.telegram.org/bots/api#keyboardbuttonpolltype // See https://core.telegram.org/bots/api#keyboardbuttonpolltype
type KeyboardButtonPollType struct { type KeyboardButtonPollType struct {
@@ -252,52 +516,27 @@ type CallbackQuery struct {
GameShortName string `json:"game_short_name,omitempty"` GameShortName string `json:"game_short_name,omitempty"`
} }
// InputPollOption contains information about one answer option in a poll to be sent.
// 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"`
}
// PollType represents the type of a poll.
type PollType string
const (
PollTypeRegular PollType = "regular"
PollTypeQuiz PollType = "quiz"
)
// InputChecklistTask describes a task in a checklist.
type InputChecklistTask struct {
ID int `json:"id"`
Text string `json:"text"`
ParseMode ParseMode `json:"parse_mode,omitempty"`
TextEntities []*MessageEntity `json:"text_entities,omitempty"`
}
// InputChecklist represents a checklist to be sent.
type InputChecklist struct {
Title string `json:"title"`
ParseMode ParseMode `json:"parse_mode,omitempty"`
TitleEntities []*MessageEntity `json:"title_entities,omitempty"`
Tasks []InputChecklistTask `json:"tasks"`
OtherCanAddTasks bool `json:"other_can_add_tasks,omitempty"`
OtherCanMarkTasksAsDone bool `json:"other_can_mark_tasks_as_done,omitempty"`
}
// ChatActionType represents the type of chat action. // ChatActionType represents the type of chat action.
type ChatActionType string type ChatActionType string
const ( const (
ChatActionTyping ChatActionType = "typing" // ChatActionTyping tells Telegram the bot is typing.
ChatActionUploadPhoto ChatActionType = "upload_photo" ChatActionTyping ChatActionType = "typing"
ChatActionUploadVideo ChatActionType = "upload_video" // ChatActionUploadPhoto tells Telegram the bot is uploading a photo.
ChatActionUploadVoice ChatActionType = "upload_voice" ChatActionUploadPhoto ChatActionType = "upload_photo"
ChatActionUploadDocument ChatActionType = "upload_document" // ChatActionUploadVideo tells Telegram the bot is uploading a video.
ChatActionChooseSticker ChatActionType = "choose_sticker" ChatActionUploadVideo ChatActionType = "upload_video"
ChatActionFindLocation ChatActionType = "find_location" // ChatActionUploadVoice tells Telegram the bot is uploading a voice message.
ChatActionUploadVoice ChatActionType = "upload_voice"
// ChatActionUploadDocument tells Telegram the bot is uploading a document.
ChatActionUploadDocument ChatActionType = "upload_document"
// ChatActionChooseSticker tells Telegram the bot is choosing a sticker.
ChatActionChooseSticker ChatActionType = "choose_sticker"
// ChatActionFindLocation tells Telegram the bot is finding a location.
ChatActionFindLocation ChatActionType = "find_location"
// ChatActionUploadVideoNote tells Telegram the bot is uploading a video note.
ChatActionUploadVideoNote ChatActionType = "upload_video_note" ChatActionUploadVideoNote ChatActionType = "upload_video_note"
// ChatActionUploadVideoNone is a deprecated alias for ChatActionUploadVideoNote.
ChatActionUploadVideoNone ChatActionType = ChatActionUploadVideoNote ChatActionUploadVideoNone ChatActionType = ChatActionUploadVideoNote
) )
@@ -358,3 +597,70 @@ type SuggestedPostParameters struct {
Price SuggestedPostPrice `json:"price"` Price SuggestedPostPrice `json:"price"`
SendDate int `json:"send_date"` SendDate int `json:"send_date"`
} }
// ManagedBotCreated describes a service message about a newly created managed bot.
// 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.
// See https://core.telegram.org/bots/api#managedbotupdated
type ManagedBotUpdated struct {
User User `json:"user"`
Bot User `json:"bot"`
}
type SharedUser struct {
UserID int64 `json:"user_id"`
FirstName string `json:"first_name,omitempty"`
LastName string `json:"last_name,omitempty"`
Username string `json:"username,omitempty"`
Photo []PhotoSize `json:"photo,omitempty"`
}
type UsersShared struct {
RequestID int `json:"request_id"`
Users []SharedUser `json:"users"`
}
type ChatShared struct {
RequestID int `json:"request_id"`
ChatID int64 `json:"chat_id"`
Title string `json:"title,omitempty"`
Username string `json:"username,omitempty"`
Photo []PhotoSize `json:"photo,omitempty"`
}
type SuggestedPostApproved struct {
SuggestedPostMessage *Message `json:"suggested_post_message,omitempty"`
Price SuggestedPostPrice `json:"price"`
SendDate int `json:"send_date"`
}
type SuggestedPostApprovalFailed struct {
SuggestedPostMessage *Message `json:"suggested_post_message,omitempty"`
Price SuggestedPostPrice `json:"price"`
}
type SuggestedPostDeclined struct {
SuggestedPostMessage *Message `json:"suggested_post_message,omitempty"`
Comment string `json:"comment,omitempty"`
}
type SuggestedPostPaid struct {
SuggestedPostMessage *Message `json:"suggested_post_message,omitempty"`
Currency string `json:"currency"`
Amount int `json:"amount"`
StarAmount *StarAmount `json:"star_amount,omitempty"`
}
type SuggestedPostRefunded struct {
SuggestedPostMessage *Message `json:"suggested_post_message,omitempty"`
Reason string `json:"reason,omitempty"`
}
type VideoChatScheduled struct {
StartDate int64 `json:"start_date"`
}
type VideoChatStarted struct{}
type VideoChatEnded struct {
Duration int64 `json:"duration"`
}
type VideoChatParticipantsInvited struct {
Users []User `json:"users"`
}
+88 -19
View File
@@ -6,7 +6,7 @@ import (
"io" "io"
"net/http" "net/http"
"git.nix13.pw/scuroneko/laniakea/utils" "git.scuroneko.dev/scuroneko/laniakea/utils"
) )
// UpdateParams holds parameters for the getUpdates method. // UpdateParams holds parameters for the getUpdates method.
@@ -33,6 +33,48 @@ func (api *API) GetMeWithContext(ctx context.Context) (User, error) {
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// GetManagedBotToken holds parameters for the getManagedBotToken method.
// See https://core.telegram.org/bots/api#getmanagedbottoken
type GetManagedBotToken struct {
UserID int64 `json:"user_id"`
}
// GetManagedBotToken returns the current token of a managed bot.
// See https://core.telegram.org/bots/api#getmanagedbottoken
func (api *API) GetManagedBotToken(params GetManagedBotToken) (string, error) {
req := NewRequest[string]("getManagedBotToken", params)
return req.Do(api)
}
// GetManagedBotTokenWithContext is the context-aware variant of GetManagedBotToken.
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#getmanagedbottoken
func (api *API) GetManagedBotTokenWithContext(ctx context.Context, params GetManagedBotToken) (string, error) {
req := NewRequest[string]("getManagedBotToken", params)
return req.DoWithContext(ctx, api)
}
// ReplaceManagedBotToken holds parameters for the replaceManagedBotToken method.
// See https://core.telegram.org/bots/api#replacemanagedbottoken
type ReplaceManagedBotToken struct {
UserID int64 `json:"user_id"`
}
// ReplaceManagedBotToken replaces and returns the token of a managed bot.
// See https://core.telegram.org/bots/api#replacemanagedbottoken
func (api *API) ReplaceManagedBotToken(params ReplaceManagedBotToken) (string, error) {
req := NewRequest[string]("replaceManagedBotToken", params)
return req.Do(api)
}
// ReplaceManagedBotTokenWithContext is the context-aware variant of ReplaceManagedBotToken.
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#replacemanagedbottoken
func (api *API) ReplaceManagedBotTokenWithContext(ctx context.Context, params ReplaceManagedBotToken) (string, error) {
req := NewRequest[string]("replaceManagedBotToken", params)
return req.DoWithContext(ctx, api)
}
// LogOut logs the bot out from the cloud Bot API server. // LogOut logs the bot out from the cloud Bot API server.
// Returns true on success. // Returns true on success.
// See https://core.telegram.org/bots/api#logout // See https://core.telegram.org/bots/api#logout
@@ -80,13 +122,13 @@ func (api *API) GetUpdatesWithContext(ctx context.Context, params UpdateParams)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// SetWebhookP holds parameters for the setWebhook method. // SetWebhook holds parameters for the setWebhook method.
// To upload a self-signed certificate, use Uploader.SetWebhook. // To upload a self-signed certificate, use Uploader.SetWebhook.
// See https://core.telegram.org/bots/api#setwebhook // See https://core.telegram.org/bots/api#setwebhook
type SetWebhookP struct { type SetWebhook struct {
URL string `json:"url"` URL string `json:"url"`
IPAddress string `json:"ip_address,omitempty"` IPAddress string `json:"ip_address,omitempty"`
MaxConnections int `json:"max_connections,omitempty"` MaxConnections int8 `json:"max_connections,omitempty"`
AllowedUpdates []UpdateType `json:"allowed_updates,omitempty"` AllowedUpdates []UpdateType `json:"allowed_updates,omitempty"`
DropPendingUpdates bool `json:"drop_pending_updates,omitempty"` DropPendingUpdates bool `json:"drop_pending_updates,omitempty"`
SecretToken string `json:"secret_token,omitempty"` SecretToken string `json:"secret_token,omitempty"`
@@ -96,7 +138,7 @@ type SetWebhookP struct {
// For certificate upload, use Uploader.SetWebhook. // For certificate upload, use Uploader.SetWebhook.
// Returns true on success. // Returns true on success.
// See https://core.telegram.org/bots/api#setwebhook // See https://core.telegram.org/bots/api#setwebhook
func (api *API) SetWebhook(params SetWebhookP) (bool, error) { func (api *API) SetWebhook(params SetWebhook) (bool, error) {
req := NewRequest[bool]("setWebhook", params) req := NewRequest[bool]("setWebhook", params)
return req.Do(api) return req.Do(api)
} }
@@ -105,21 +147,21 @@ func (api *API) SetWebhook(params SetWebhookP) (bool, error) {
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// For certificate upload, use Uploader.SetWebhook. // For certificate upload, use Uploader.SetWebhook.
// See https://core.telegram.org/bots/api#setwebhook // See https://core.telegram.org/bots/api#setwebhook
func (api *API) SetWebhookWithContext(ctx context.Context, params SetWebhookP) (bool, error) { func (api *API) SetWebhookWithContext(ctx context.Context, params SetWebhook) (bool, error) {
req := NewRequest[bool]("setWebhook", params) req := NewRequest[bool]("setWebhook", params)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// DeleteWebhookP holds parameters for the deleteWebhook method. // DeleteWebhook holds parameters for the deleteWebhook method.
// See https://core.telegram.org/bots/api#deletewebhook // See https://core.telegram.org/bots/api#deletewebhook
type DeleteWebhookP struct { type DeleteWebhook struct {
DropPendingUpdates bool `json:"drop_pending_updates,omitempty"` DropPendingUpdates bool `json:"drop_pending_updates,omitempty"`
} }
// DeleteWebhook removes the current webhook integration. // DeleteWebhook removes the current webhook integration.
// Returns true on success. // Returns true on success.
// See https://core.telegram.org/bots/api#deletewebhook // See https://core.telegram.org/bots/api#deletewebhook
func (api *API) DeleteWebhook(params DeleteWebhookP) (bool, error) { func (api *API) DeleteWebhook(params DeleteWebhook) (bool, error) {
req := NewRequest[bool]("deleteWebhook", params) req := NewRequest[bool]("deleteWebhook", params)
return req.Do(api) return req.Do(api)
} }
@@ -127,7 +169,7 @@ func (api *API) DeleteWebhook(params DeleteWebhookP) (bool, error) {
// DeleteWebhookWithContext is the context-aware variant of DeleteWebhook. // DeleteWebhookWithContext is the context-aware variant of DeleteWebhook.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#deletewebhook // See https://core.telegram.org/bots/api#deletewebhook
func (api *API) DeleteWebhookWithContext(ctx context.Context, params DeleteWebhookP) (bool, error) { func (api *API) DeleteWebhookWithContext(ctx context.Context, params DeleteWebhook) (bool, error) {
req := NewRequest[bool]("deleteWebhook", params) req := NewRequest[bool]("deleteWebhook", params)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
@@ -147,15 +189,15 @@ func (api *API) GetWebhookInfoWithContext(ctx context.Context) (WebhookInfo, err
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// GetFileP holds parameters for the getFile method. // GetFile holds parameters for the getFile method.
// See https://core.telegram.org/bots/api#getfile // See https://core.telegram.org/bots/api#getfile
type GetFileP struct { type GetFile struct {
FileId string `json:"file_id"` FileID string `json:"file_id"`
} }
// GetFile returns basic information about a file and prepares it for downloading. // GetFile returns basic information about a file and prepares it for downloading.
// See https://core.telegram.org/bots/api#getfile // See https://core.telegram.org/bots/api#getfile
func (api *API) GetFile(params GetFileP) (File, error) { func (api *API) GetFile(params GetFile) (File, error) {
req := NewRequest[File]("getFile", params) req := NewRequest[File]("getFile", params)
return req.Do(api) return req.Do(api)
} }
@@ -163,13 +205,14 @@ func (api *API) GetFile(params GetFileP) (File, error) {
// GetFileWithContext is the context-aware variant of GetFile. // GetFileWithContext is the context-aware variant of GetFile.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#getfile // See https://core.telegram.org/bots/api#getfile
func (api *API) GetFileWithContext(ctx context.Context, params GetFileP) (File, error) { func (api *API) GetFileWithContext(ctx context.Context, params GetFile) (File, error) {
req := NewRequest[File]("getFile", params) req := NewRequest[File]("getFile", params)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// GetFileByLink downloads a file from Telegram's file server using the provided file link. // GetFileByLink downloads a file from Telegram's file server using the provided file link.
// The link is usually obtained from File.FilePath. // The link is usually obtained from File.FilePath.
// For large files, prefer OpenFileByLink or OpenFileByLinkWithContext to stream the response body.
// See https://core.telegram.org/bots/api#file // See https://core.telegram.org/bots/api#file
func (api *API) GetFileByLink(link string) ([]byte, error) { func (api *API) GetFileByLink(link string) ([]byte, error) {
return api.getFileByLink(context.Background(), link) return api.getFileByLink(context.Background(), link)
@@ -177,12 +220,38 @@ func (api *API) GetFileByLink(link string) ([]byte, error) {
// GetFileByLinkWithContext is the context-aware variant of GetFileByLink. // GetFileByLinkWithContext is the context-aware variant of GetFileByLink.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// For large files, prefer OpenFileByLinkWithContext to stream the response body.
// See https://core.telegram.org/bots/api#file // See https://core.telegram.org/bots/api#file
func (api *API) GetFileByLinkWithContext(ctx context.Context, link string) ([]byte, error) { func (api *API) GetFileByLinkWithContext(ctx context.Context, link string) ([]byte, error) {
return api.getFileByLink(ctx, link) return api.getFileByLink(ctx, link)
} }
// OpenFileByLink opens a streaming response body for a file hosted on Telegram's file server.
// The caller must close the returned ReadCloser.
// See https://core.telegram.org/bots/api#file
func (api *API) OpenFileByLink(link string) (io.ReadCloser, error) {
return api.openFileByLink(context.Background(), link)
}
// OpenFileByLinkWithContext is the context-aware variant of OpenFileByLink.
// The caller must close the returned ReadCloser.
// See https://core.telegram.org/bots/api#file
func (api *API) OpenFileByLinkWithContext(ctx context.Context, link string) (io.ReadCloser, error) {
return api.openFileByLink(ctx, link)
}
func (api *API) getFileByLink(ctx context.Context, link string) ([]byte, error) { func (api *API) getFileByLink(ctx context.Context, link string) ([]byte, error) {
body, err := api.openFileByLink(ctx, link)
if err != nil {
return nil, err
}
defer func() {
_ = body.Close()
}()
return io.ReadAll(body)
}
func (api *API) openFileByLink(ctx context.Context, link string) (io.ReadCloser, error) {
methodPrefix := "" methodPrefix := ""
if api.useTestServer { if api.useTestServer {
methodPrefix = "/test" methodPrefix = "/test"
@@ -199,15 +268,15 @@ func (api *API) getFileByLink(ctx context.Context, link string) ([]byte, error)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer func() {
_ = res.Body.Close()
}()
if res.StatusCode < http.StatusOK || res.StatusCode >= http.StatusMultipleChoices { if res.StatusCode < http.StatusOK || res.StatusCode >= http.StatusMultipleChoices {
defer func() {
_ = res.Body.Close()
}()
body, readErr := io.ReadAll(io.LimitReader(res.Body, 4<<10)) body, readErr := io.ReadAll(io.LimitReader(res.Body, 4<<10))
if readErr != nil { if readErr != nil {
return nil, fmt.Errorf("unexpected status %d", res.StatusCode) return nil, fmt.Errorf("unexpected status %d", res.StatusCode)
} }
return nil, fmt.Errorf("unexpected status %d: %s", res.StatusCode, string(body)) return nil, fmt.Errorf("unexpected status %d: %s", res.StatusCode, string(body))
} }
return io.ReadAll(res.Body) return res.Body, nil
} }
+95
View File
@@ -44,6 +44,44 @@ func TestGetFileByLinkUsesConfiguredAPIURL(t *testing.T) {
} }
} }
func TestOpenFileByLinkStreamsResponseBody(t *testing.T) {
api := NewAPI(
NewAPIOpts("token").
SetAPIUrl("https://example.test").
SetHTTPClient(&http.Client{
Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(strings.NewReader("streamed payload")),
}, nil
}),
}),
)
defer func() {
if err := api.Close(); err != nil {
t.Fatalf("Close returned error: %v", err)
}
}()
body, err := api.OpenFileByLink("files/report.txt")
if err != nil {
t.Fatalf("OpenFileByLink returned error: %v", err)
}
defer func() {
if err := body.Close(); err != nil {
t.Fatalf("Close returned error: %v", err)
}
}()
data, err := io.ReadAll(body)
if err != nil {
t.Fatalf("failed to read body: %v", err)
}
if string(data) != "streamed payload" {
t.Fatalf("unexpected payload: %q", string(data))
}
}
func TestGetFileByLinkReturnsHTTPStatusError(t *testing.T) { func TestGetFileByLinkReturnsHTTPStatusError(t *testing.T) {
client := &http.Client{ client := &http.Client{
Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) { Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
@@ -113,3 +151,60 @@ func TestGetUpdatesOmitsAllowedUpdatesWhenEmpty(t *testing.T) {
t.Fatalf("expected allowed_updates to be omitted, got %v", gotBody["allowed_updates"]) t.Fatalf("expected allowed_updates to be omitted, got %v", gotBody["allowed_updates"])
} }
} }
func TestSetChatMenuButtonSendsStructuredMenuButton(t *testing.T) {
var gotBody map[string]any
client := &http.Client{
Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
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 := NewAPI(
NewAPIOpts("token").
SetAPIUrl("https://example.test").
SetHTTPClient(client),
)
defer func() {
if err := api.Close(); err != nil {
t.Fatalf("Close returned error: %v", err)
}
}()
text := "Open"
if _, err := api.SetChatMenuButton(SetChatMenuButton{
ChatID: 42,
MenuButton: &MenuButton{
Type: MenuButtonWebAppType,
Text: &text,
WebApp: &WebAppInfo{
URL: "https://example.test/app",
},
},
}); err != nil {
t.Fatalf("SetChatMenuButton returned error: %v", err)
}
menuButton, ok := gotBody["menu_button"].(map[string]any)
if !ok {
t.Fatalf("expected structured menu_button, got %#v", gotBody["menu_button"])
}
if menuButton["type"] != string(MenuButtonWebAppType) {
t.Fatalf("unexpected menu button type: %#v", menuButton["type"])
}
if menuButton["text"] != text {
t.Fatalf("unexpected menu button text: %#v", menuButton["text"])
}
}
+2 -16
View File
@@ -10,8 +10,8 @@ const (
ParseHTML ParseMode = "HTML" ParseHTML ParseMode = "HTML"
// ParseMD enables legacy Markdown style parsing. // ParseMD enables legacy Markdown style parsing.
ParseMD ParseMode = "Markdown" ParseMD ParseMode = "Markdown"
// ParseNone disables any parsing. // ParseNone disables parse_mode and leaves plain-text requests unannotated.
ParseNone ParseMode = "None" ParseNone ParseMode = ""
) )
// EmptyParams is a placeholder for methods that take no parameters. // EmptyParams is a placeholder for methods that take no parameters.
@@ -19,17 +19,3 @@ type EmptyParams struct{}
// NoParams is a convenient instance of EmptyParams. // NoParams is a convenient instance of EmptyParams.
var NoParams = EmptyParams{} var NoParams = EmptyParams{}
// WebhookInfo describes the current webhook status.
// See https://core.telegram.org/bots/api#webhookinfo
type WebhookInfo struct {
URL string `json:"url"`
HasCustomCertificate bool `json:"has_custom_certificate"`
PendingUpdateCount int `json:"pending_update_count"`
IPAddress string `json:"ip_address,omitempty"`
LastErrorDate int `json:"last_error_date,omitempty"`
LastErrorMessage string `json:"last_error_message,omitempty"`
LastSynchronizationErrorDate int `json:"last_synchronization_error_date,omitempty"`
MaxConnections int `json:"max_connections,omitempty"`
AllowedUpdates []string `json:"allowed_updates,omitempty"`
}
+37
View File
@@ -0,0 +1,37 @@
package tgapi
import (
"encoding/json"
"strings"
"testing"
)
func TestParseNoneOmitsParseModeInJSON(t *testing.T) {
data, err := json.Marshal(SendMessage{
ChatID: 42,
Text: "hello",
ParseMode: ParseNone,
})
if err != nil {
t.Fatalf("Marshal returned error: %v", err)
}
if strings.Contains(string(data), `"parse_mode"`) {
t.Fatalf("expected parse_mode to be omitted, got %s", string(data))
}
}
func TestParseModeStillSerializesExplicitModes(t *testing.T) {
data, err := json.Marshal(SendMessage{
ChatID: 42,
Text: "hello",
ParseMode: ParseMDV2,
})
if err != nil {
t.Fatalf("Marshal returned error: %v", err)
}
if !strings.Contains(string(data), `"parse_mode":"MarkdownV2"`) {
t.Fatalf("expected MarkdownV2 parse_mode, got %s", string(data))
}
}
+4 -4
View File
@@ -2,9 +2,9 @@ package tgapi
import "context" import "context"
// SetPassportDataErrorsP holds parameters for the setPassportDataErrors method. // SetPassportDataErrors holds parameters for the setPassportDataErrors method.
// See https://core.telegram.org/bots/api#setpassportdataerrors // See https://core.telegram.org/bots/api#setpassportdataerrors
type SetPassportDataErrorsP struct { type SetPassportDataErrors struct {
UserID int64 `json:"user_id"` UserID int64 `json:"user_id"`
Errors []PassportElementError `json:"errors"` Errors []PassportElementError `json:"errors"`
} }
@@ -12,7 +12,7 @@ type SetPassportDataErrorsP struct {
// SetPassportDataErrors informs a user about Telegram Passport data errors. // SetPassportDataErrors informs a user about Telegram Passport data errors.
// Returns true on success. // Returns true on success.
// See https://core.telegram.org/bots/api#setpassportdataerrors // See https://core.telegram.org/bots/api#setpassportdataerrors
func (api *API) SetPassportDataErrors(params SetPassportDataErrorsP) (bool, error) { func (api *API) SetPassportDataErrors(params SetPassportDataErrors) (bool, error) {
req := NewRequest[bool]("setPassportDataErrors", params) req := NewRequest[bool]("setPassportDataErrors", params)
return req.Do(api) return req.Do(api)
} }
@@ -20,7 +20,7 @@ func (api *API) SetPassportDataErrors(params SetPassportDataErrorsP) (bool, erro
// SetPassportDataErrorsWithContext is the context-aware variant of SetPassportDataErrors. // SetPassportDataErrorsWithContext is the context-aware variant of SetPassportDataErrors.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#setpassportdataerrors // See https://core.telegram.org/bots/api#setpassportdataerrors
func (api *API) SetPassportDataErrorsWithContext(ctx context.Context, params SetPassportDataErrorsP) (bool, error) { func (api *API) SetPassportDataErrorsWithContext(ctx context.Context, params SetPassportDataErrors) (bool, error) {
req := NewRequest[bool]("setPassportDataErrors", params) req := NewRequest[bool]("setPassportDataErrors", params)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
+60 -1
View File
@@ -1,5 +1,64 @@
package tgapi package tgapi
type PassportData struct {
Data []EncryptedPassportElement `json:"data"`
Credentials EncryptedCredentials `json:"credentials"`
}
type PassportFile struct {
FileID string `json:"file_id"`
FileUniqueID string `json:"file_unique_id"`
FileSize int64 `json:"file_size"`
FileDate int64 `json:"file_date"`
}
type PassportElementType string
const (
PassportPersonalDetailsType PassportElementType = "personal_details"
PassportPassportType PassportElementType = "passport"
PassportDriverLicenseType PassportElementType = "driver_license"
PassportIdentityCardType PassportElementType = "identity_card"
PassportInternalPassportType PassportElementType = "internal_passport"
PassportAddressType PassportElementType = "address"
PassportUtilityBillType PassportElementType = "utility_bill"
PassportBankStatementType PassportElementType = "bank_statement"
PassportRentalAgreementType PassportElementType = "rental_agreement"
PassportPassportRegistrationType PassportElementType = "passport_registration"
PassportTemporaryRegistrationType PassportElementType = "temporary_registration"
PassportPhoneNumberType PassportElementType = "phone_number"
PassportEmailType PassportElementType = "email"
)
type EncryptedPassportElement struct {
Type PassportElementType `json:"type"`
Data string `json:"data,omitempty"`
PhoneNumber string `json:"phone_number,omitempty"`
Email string `json:"email,omitempty"`
Files []PassportFile `json:"files,omitempty"`
FrontSide *PassportFile `json:"front_side,omitempty"`
ReverseSide *PassportFile `json:"reverse_side,omitempty"`
Selfie *PassportFile `json:"selfie,omitempty"`
Translation *PassportFile `json:"translation,omitempty"`
Hash string `json:"hash,omitempty"`
}
type EncryptedCredentials struct {
Data string `json:"data"`
Hash string `json:"hash"`
Secret string `json:"secret"`
}
// PassportElementError is a JSON-serializable passport element error object. // PassportElementError is a JSON-serializable passport element error object.
// See https://core.telegram.org/bots/api#passportelementerror // See https://core.telegram.org/bots/api#passportelementerror
type PassportElementError map[string]any type PassportElementError struct {
Source string `json:"source"`
Type PassportElementType `json:"type"`
FieldName string `json:"field_name,omitempty"`
DataHash string `json:"data_hash,omitempty"`
FileHash string `json:"file_hash,omitempty"`
FileHashes []string `json:"file_hashes,omitempty"`
ElementHash string `json:"element_hash,omitempty"`
Message string `json:"message"`
}
+16 -16
View File
@@ -2,9 +2,9 @@ package tgapi
import "context" import "context"
// SendInvoiceP holds parameters for the sendInvoice method. // SendInvoice holds parameters for the sendInvoice method.
// See https://core.telegram.org/bots/api#sendinvoice // See https://core.telegram.org/bots/api#sendinvoice
type SendInvoiceP struct { type SendInvoice struct {
ChatID int64 `json:"chat_id"` ChatID int64 `json:"chat_id"`
MessageThreadID int `json:"message_thread_id,omitempty"` MessageThreadID int `json:"message_thread_id,omitempty"`
DirectMessagesTopicID int `json:"direct_messages_topic_id,omitempty"` DirectMessagesTopicID int `json:"direct_messages_topic_id,omitempty"`
@@ -43,7 +43,7 @@ type SendInvoiceP struct {
// SendInvoice sends an invoice. // SendInvoice sends an invoice.
// See https://core.telegram.org/bots/api#sendinvoice // See https://core.telegram.org/bots/api#sendinvoice
func (api *API) SendInvoice(params SendInvoiceP) (Message, error) { func (api *API) SendInvoice(params SendInvoice) (Message, error) {
req := NewRequestWithChatID[Message]("sendInvoice", params, params.ChatID) req := NewRequestWithChatID[Message]("sendInvoice", params, params.ChatID)
return req.Do(api) return req.Do(api)
} }
@@ -51,14 +51,14 @@ func (api *API) SendInvoice(params SendInvoiceP) (Message, error) {
// SendInvoiceWithContext is the context-aware variant of SendInvoice. // SendInvoiceWithContext is the context-aware variant of SendInvoice.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#sendinvoice // See https://core.telegram.org/bots/api#sendinvoice
func (api *API) SendInvoiceWithContext(ctx context.Context, params SendInvoiceP) (Message, error) { func (api *API) SendInvoiceWithContext(ctx context.Context, params SendInvoice) (Message, error) {
req := NewRequestWithChatID[Message]("sendInvoice", params, params.ChatID) req := NewRequestWithChatID[Message]("sendInvoice", params, params.ChatID)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// CreateInvoiceLinkP holds parameters for the createInvoiceLink method. // CreateInvoiceLink holds parameters for the createInvoiceLink method.
// See https://core.telegram.org/bots/api#createinvoicelink // See https://core.telegram.org/bots/api#createinvoicelink
type CreateInvoiceLinkP struct { type CreateInvoiceLink struct {
BusinessConnectionID string `json:"business_connection_id,omitempty"` BusinessConnectionID string `json:"business_connection_id,omitempty"`
Title string `json:"title"` Title string `json:"title"`
@@ -87,7 +87,7 @@ type CreateInvoiceLinkP struct {
// CreateInvoiceLink creates an invoice link. // CreateInvoiceLink creates an invoice link.
// See https://core.telegram.org/bots/api#createinvoicelink // See https://core.telegram.org/bots/api#createinvoicelink
func (api *API) CreateInvoiceLink(params CreateInvoiceLinkP) (string, error) { func (api *API) CreateInvoiceLink(params CreateInvoiceLink) (string, error) {
req := NewRequest[string]("createInvoiceLink", params) req := NewRequest[string]("createInvoiceLink", params)
return req.Do(api) return req.Do(api)
} }
@@ -95,14 +95,14 @@ func (api *API) CreateInvoiceLink(params CreateInvoiceLinkP) (string, error) {
// CreateInvoiceLinkWithContext is the context-aware variant of CreateInvoiceLink. // CreateInvoiceLinkWithContext is the context-aware variant of CreateInvoiceLink.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#createinvoicelink // See https://core.telegram.org/bots/api#createinvoicelink
func (api *API) CreateInvoiceLinkWithContext(ctx context.Context, params CreateInvoiceLinkP) (string, error) { func (api *API) CreateInvoiceLinkWithContext(ctx context.Context, params CreateInvoiceLink) (string, error) {
req := NewRequest[string]("createInvoiceLink", params) req := NewRequest[string]("createInvoiceLink", params)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// AnswerShippingQueryP holds parameters for the answerShippingQuery method. // AnswerShippingQuery holds parameters for the answerShippingQuery method.
// See https://core.telegram.org/bots/api#answershippingquery // See https://core.telegram.org/bots/api#answershippingquery
type AnswerShippingQueryP struct { type AnswerShippingQuery struct {
ShippingQueryID string `json:"shipping_query_id"` ShippingQueryID string `json:"shipping_query_id"`
OK bool `json:"ok"` OK bool `json:"ok"`
ShippingOptions []ShippingOption `json:"shipping_options,omitempty"` ShippingOptions []ShippingOption `json:"shipping_options,omitempty"`
@@ -112,7 +112,7 @@ type AnswerShippingQueryP struct {
// AnswerShippingQuery answers a shipping query. // AnswerShippingQuery answers a shipping query.
// Returns true on success. // Returns true on success.
// See https://core.telegram.org/bots/api#answershippingquery // See https://core.telegram.org/bots/api#answershippingquery
func (api *API) AnswerShippingQuery(params AnswerShippingQueryP) (bool, error) { func (api *API) AnswerShippingQuery(params AnswerShippingQuery) (bool, error) {
req := NewRequest[bool]("answerShippingQuery", params) req := NewRequest[bool]("answerShippingQuery", params)
return req.Do(api) return req.Do(api)
} }
@@ -120,14 +120,14 @@ func (api *API) AnswerShippingQuery(params AnswerShippingQueryP) (bool, error) {
// AnswerShippingQueryWithContext is the context-aware variant of AnswerShippingQuery. // AnswerShippingQueryWithContext is the context-aware variant of AnswerShippingQuery.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#answershippingquery // See https://core.telegram.org/bots/api#answershippingquery
func (api *API) AnswerShippingQueryWithContext(ctx context.Context, params AnswerShippingQueryP) (bool, error) { func (api *API) AnswerShippingQueryWithContext(ctx context.Context, params AnswerShippingQuery) (bool, error) {
req := NewRequest[bool]("answerShippingQuery", params) req := NewRequest[bool]("answerShippingQuery", params)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// AnswerPreCheckoutQueryP holds parameters for the answerPreCheckoutQuery method. // AnswerPreCheckoutQuery holds parameters for the answerPreCheckoutQuery method.
// See https://core.telegram.org/bots/api#answerprecheckoutquery // See https://core.telegram.org/bots/api#answerprecheckoutquery
type AnswerPreCheckoutQueryP struct { type AnswerPreCheckoutQuery struct {
PreCheckoutQueryID string `json:"pre_checkout_query_id"` PreCheckoutQueryID string `json:"pre_checkout_query_id"`
OK bool `json:"ok"` OK bool `json:"ok"`
ErrorMessage string `json:"error_message,omitempty"` ErrorMessage string `json:"error_message,omitempty"`
@@ -136,7 +136,7 @@ type AnswerPreCheckoutQueryP struct {
// AnswerPreCheckoutQuery answers a pre-checkout query. // AnswerPreCheckoutQuery answers a pre-checkout query.
// Returns true on success. // Returns true on success.
// See https://core.telegram.org/bots/api#answerprecheckoutquery // See https://core.telegram.org/bots/api#answerprecheckoutquery
func (api *API) AnswerPreCheckoutQuery(params AnswerPreCheckoutQueryP) (bool, error) { func (api *API) AnswerPreCheckoutQuery(params AnswerPreCheckoutQuery) (bool, error) {
req := NewRequest[bool]("answerPreCheckoutQuery", params) req := NewRequest[bool]("answerPreCheckoutQuery", params)
return req.Do(api) return req.Do(api)
} }
@@ -144,7 +144,7 @@ func (api *API) AnswerPreCheckoutQuery(params AnswerPreCheckoutQueryP) (bool, er
// AnswerPreCheckoutQueryWithContext is the context-aware variant of AnswerPreCheckoutQuery. // AnswerPreCheckoutQueryWithContext is the context-aware variant of AnswerPreCheckoutQuery.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#answerprecheckoutquery // See https://core.telegram.org/bots/api#answerprecheckoutquery
func (api *API) AnswerPreCheckoutQueryWithContext(ctx context.Context, params AnswerPreCheckoutQueryP) (bool, error) { func (api *API) AnswerPreCheckoutQueryWithContext(ctx context.Context, params AnswerPreCheckoutQuery) (bool, error) {
req := NewRequest[bool]("answerPreCheckoutQuery", params) req := NewRequest[bool]("answerPreCheckoutQuery", params)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
+80
View File
@@ -7,6 +7,62 @@ type LabeledPrice struct {
Amount int `json:"amount"` Amount int `json:"amount"`
} }
type Invoice struct {
Title string `json:"title"`
Description string `json:"description"`
StartParameter string `json:"start_parameter"`
Currency string `json:"currency"`
TotalAmount int `json:"total_amount"`
}
// ShippingQuery represents an incoming shipping query.
// See https://core.telegram.org/bots/api#shippingquery
type ShippingQuery struct {
ID string `json:"id"`
From User `json:"from"`
InvoicePayload string `json:"invoice_payload"`
ShippingAddress ShippingAddress `json:"shipping_address"`
}
// ShippingAddress represents a shipping address.
// See https://core.telegram.org/bots/api#shippingaddress
type ShippingAddress struct {
CountryCode string `json:"country_code"`
State string `json:"state"`
City string `json:"city"`
StreetLine1 string `json:"street_line1"`
StreetLine2 string `json:"street_line2"`
PostCode string `json:"post_code"`
}
// OrderInfo represents information about an order.
// See https://core.telegram.org/bots/api#orderinfo
type OrderInfo struct {
Name string `json:"name"`
PhoneNumber string `json:"phone_number"`
Email string `json:"email"`
ShippingAddress ShippingAddress `json:"shipping_address"`
}
// PreCheckoutQuery represents an incoming pre-checkout query.
// See https://core.telegram.org/bots/api#precheckoutquery
type PreCheckoutQuery struct {
ID string `json:"id"`
From User `json:"from"`
Currency string `json:"currency"`
TotalAmount int `json:"total_amount"`
InvoicePayload string `json:"invoice_payload"`
ShippingOptionID string `json:"shipping_option_id"`
OrderInfo *OrderInfo `json:"order_info,omitempty"`
}
// PaidMediaPurchased represents a purchased paid media.
// See https://core.telegram.org/bots/api#paidmediapurchased
type PaidMediaPurchased struct {
From User `json:"from"`
PaidMediaPayload string `json:"paid_media_payload"`
}
// ShippingOption represents one shipping option. // ShippingOption represents one shipping option.
// See https://core.telegram.org/bots/api#shippingoption // See https://core.telegram.org/bots/api#shippingoption
type ShippingOption struct { type ShippingOption struct {
@@ -14,3 +70,27 @@ type ShippingOption struct {
Title string `json:"title"` Title string `json:"title"`
Prices []LabeledPrice `json:"prices"` Prices []LabeledPrice `json:"prices"`
} }
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"`
ShippingOptionID string `json:"shipping_option_id,omitempty"`
OrderInfo *OrderInfo `json:"order_info,omitempty"`
TelegramPaymentChargeID string `json:"telegram_payment_charge_id"`
ProviderPaymentChargeID string `json:"proviced_payment_charge_id"`
}
type RefundedPayment struct {
Currency string `json:"currency"`
TotalAmount int `json:"total_amount"`
InvoicePayload string `json:"invoice_payload"`
TelegramPaymentChargeID string `json:"telegram_payment_charge_id"`
ProviderPaymentChargeID string `json:"proviced_payment_charge_id,omitempty"`
}
+22 -62
View File
@@ -5,44 +5,35 @@ import (
"sync" "sync"
) )
// workerPool — приватная структура, управляющая пулом воркеров.
// Внешний код не может создавать или напрямую взаимодействовать с этой структурой.
// Используется только через экспортируемые методы newWorkerPool, start, stop, submit.
type workerPool struct { type workerPool struct {
taskCh chan requestEnvelope // канал для принятия задач (буферизованный) taskCh chan requestEnvelope
queueSize int // максимальный размер очереди queueSize int
workers int // количество воркеров (горутин) workers int
wg sync.WaitGroup // синхронизирует завершение всех воркеров при остановке wg sync.WaitGroup
quit chan struct{} // канал для сигнала остановки quit chan struct{}
stopOnce sync.Once // гарантирует идемпотентную остановку пула stopOnce sync.Once
started bool // флаг, указывающий, запущен ли пул started bool
stopped bool // флаг, указывающий, что пул остановлен stopped bool
startedMu sync.Mutex // мьютекс для безопасного доступа к started startedMu sync.Mutex
} }
// requestEnvelope — приватная структура, инкапсулирующая задачу и канал для результата.
// Используется только внутри пакета для передачи задач воркерам.
type requestEnvelope struct { type requestEnvelope struct {
ctx context.Context // контекст конкретной задачи ctx context.Context
doFunc func(context.Context) (any, error) // функция, выполняющая запрос doFunc func(context.Context) (any, error)
resultCh chan requestResult // канал, через который воркер вернёт результат resultCh chan requestResult
} }
// requestResult — приватная структура, представляющая результат выполнения задачи.
// Внешний код получает его через канал, но не знает структуры — только через <-chan requestResult.
type requestResult struct { type requestResult struct {
value any // значение, возвращённое задачей value any
err error // ошибка, если возникла err error
} }
// newWorkerPool создаёт новый пул воркеров с заданным количеством горутин и размером очереди.
// Это единственный способ создать workerPool — внешний код не может создать его напрямую.
func newWorkerPool(workers int, queueSize int) *workerPool { func newWorkerPool(workers int, queueSize int) *workerPool {
if workers <= 0 { if workers <= 0 {
workers = 1 // защита от некорректных значений workers = 1
} }
if queueSize <= 0 { if queueSize <= 0 {
queueSize = 100 // разумный дефолт queueSize = 100
} }
return &workerPool{ return &workerPool{
@@ -53,43 +44,32 @@ func newWorkerPool(workers int, queueSize int) *workerPool {
} }
} }
// start запускает воркеры (горутины), которые будут обрабатывать задачи из очереди.
// Метод идемпотентен: если пул уже запущен — ничего не делает.
// Должен вызываться перед первым вызовом submit.
func (p *workerPool) start() { func (p *workerPool) start() {
p.startedMu.Lock() p.startedMu.Lock()
defer p.startedMu.Unlock() defer p.startedMu.Unlock()
if p.started { if p.started {
return // уже запущен — ничего не делаем return
} }
p.started = true p.started = true
// Запускаем воркеры — каждый будет обрабатывать задачи в бесконечном цикле
for i := 0; i < p.workers; i++ { for i := 0; i < p.workers; i++ {
p.wg.Add(1) p.wg.Add(1)
go p.worker() // запускаем горутину go p.worker()
} }
} }
// stop останавливает пул воркеров.
// Отправляет сигнал остановки через quit-канал и ждёт завершения всех активных задач.
// Безопасно вызывать многократно — после остановки повторные вызовы не имеют эффекта.
func (p *workerPool) stop() { func (p *workerPool) stop() {
p.stopOnce.Do(func() { p.stopOnce.Do(func() {
p.startedMu.Lock() p.startedMu.Lock()
p.stopped = true p.stopped = true
p.started = false p.started = false
close(p.quit) // сигнал для всех воркеров — выйти из цикла close(p.quit)
p.startedMu.Unlock() p.startedMu.Unlock()
p.wg.Wait() // ждём, пока все воркеры завершатся p.wg.Wait()
}) })
} }
// submit отправляет задачу в очередь и возвращает канал, через который будет получен результат.
// Если очередь переполнена — возвращает ErrPoolQueueFull.
// Канал результата имеет буфер 1, чтобы не блокировать воркера при записи.
// Контекст используется для отмены задачи, если клиент отменил запрос до отправки.
func (p *workerPool) submit(ctx context.Context, do func(context.Context) (any, error)) (<-chan requestResult, error) { func (p *workerPool) submit(ctx context.Context, do func(context.Context) (any, error)) (<-chan requestResult, error) {
p.startedMu.Lock() p.startedMu.Lock()
if p.stopped || !p.started { if p.stopped || !p.started {
@@ -97,55 +77,39 @@ func (p *workerPool) submit(ctx context.Context, do func(context.Context) (any,
return nil, ErrPoolStopped return nil, ErrPoolStopped
} }
// Проверяем, не превышена ли очередь
if len(p.taskCh) >= p.queueSize { if len(p.taskCh) >= p.queueSize {
p.startedMu.Unlock() p.startedMu.Unlock()
return nil, ErrPoolQueueFull return nil, ErrPoolQueueFull
} }
// Создаём канал для результата — буферизованный, чтобы не блокировать воркера
resultCh := make(chan requestResult, 1) resultCh := make(chan requestResult, 1)
// Создаём обёртку задачи
envelope := requestEnvelope{ envelope := requestEnvelope{
ctx: ctx, ctx: ctx,
doFunc: do, doFunc: do,
resultCh: resultCh, resultCh: resultCh,
} }
// Пытаемся отправить задачу в очередь
select { select {
case <-ctx.Done(): case <-ctx.Done():
p.startedMu.Unlock() p.startedMu.Unlock()
// Клиент отменил операцию до отправки — возвращаем ошибку отмены
return nil, ctx.Err() return nil, ctx.Err()
case p.taskCh <- envelope: case p.taskCh <- envelope:
p.startedMu.Unlock() p.startedMu.Unlock()
// Успешно отправлено — возвращаем канал для чтения результата
return resultCh, nil return resultCh, nil
default: default:
p.startedMu.Unlock() p.startedMu.Unlock()
// Очередь переполнена — не должно происходить при проверке len(p.taskCh), но на всякий случай
return nil, ErrPoolQueueFull return nil, ErrPoolQueueFull
} }
} }
// worker — приватная горутина, выполняющая задачи из очереди.
// Каждый воркер работает в бесконечном цикле, пока не получит сигнал остановки.
// При получении задачи:
// - вызывает doFunc с контекстом
// - записывает результат в resultCh
// - закрывает канал, чтобы клиент мог прочитать и завершить
//
// После закрытия quit-канала — воркер завершает работу.
func (p *workerPool) worker() { func (p *workerPool) worker() {
defer p.wg.Done() // уменьшаем WaitGroup при завершении горутины defer p.wg.Done()
for { for {
select { select {
case <-p.quit: case <-p.quit:
// Получен сигнал остановки — дренируем очередь и выходим. // Drain queued work after stop. No new tasks are accepted.
// После stop() новые задачи не принимаются.
for { for {
select { select {
case envelope := <-p.taskCh: case envelope := <-p.taskCh:
@@ -162,14 +126,10 @@ func (p *workerPool) worker() {
} }
func (p *workerPool) executeEnvelope(envelope requestEnvelope) { func (p *workerPool) executeEnvelope(envelope requestEnvelope) {
// Выполняем задачу с переданным контекстом (клиентский или общий)
value, err := envelope.doFunc(envelope.ctx) value, err := envelope.doFunc(envelope.ctx)
// Записываем результат в канал — не блокируем, т.к. буфер 1
envelope.resultCh <- requestResult{ envelope.resultCh <- requestResult{
value: value, value: value,
err: err, err: err,
} }
// Закрываем канал — клиент знает, что результат пришёл и больше не будет
close(envelope.resultCh) close(envelope.resultCh)
} }
+62
View File
@@ -0,0 +1,62 @@
package tgapi
import (
"context"
"errors"
"testing"
)
func TestWorkerPoolSubmitAfterStop(t *testing.T) {
pool := newWorkerPool(1, 1)
pool.start()
pool.stop()
if _, err := pool.submit(context.Background(), func(context.Context) (any, error) {
return nil, nil
}); !errors.Is(err, ErrPoolStopped) {
t.Fatalf("expected ErrPoolStopped, got %v", err)
}
}
func TestWorkerPoolQueueFull(t *testing.T) {
pool := newWorkerPool(1, 1)
pool.start()
defer pool.stop()
started := make(chan struct{})
release := make(chan struct{})
firstResult, err := pool.submit(context.Background(), func(context.Context) (any, error) {
close(started)
<-release
return "first", nil
})
if err != nil {
t.Fatalf("first submit returned error: %v", err)
}
<-started
secondResult, err := pool.submit(context.Background(), func(context.Context) (any, error) {
return "second", nil
})
if err != nil {
t.Fatalf("second submit returned error: %v", err)
}
if _, err := pool.submit(context.Background(), func(context.Context) (any, error) {
return "third", nil
}); !errors.Is(err, ErrPoolQueueFull) {
t.Fatalf("expected ErrPoolQueueFull, got %v", err)
}
close(release)
first := <-firstResult
if first.err != nil || first.value != "first" {
t.Fatalf("unexpected first result: %+v", first)
}
second := <-secondResult
if second.err != nil || second.value != "second" {
t.Fatalf("unexpected second result: %+v", second)
}
}
+12 -12
View File
@@ -2,9 +2,9 @@ package tgapi
import "context" import "context"
// GetStarTransactionsP holds parameters for the getStarTransactions method. // GetStarTransactions holds parameters for the getStarTransactions method.
// See https://core.telegram.org/bots/api#getstartransactions // See https://core.telegram.org/bots/api#getstartransactions
type GetStarTransactionsP struct { type GetStarTransactions struct {
Offset int `json:"offset,omitempty"` Offset int `json:"offset,omitempty"`
Limit int `json:"limit,omitempty"` Limit int `json:"limit,omitempty"`
} }
@@ -26,7 +26,7 @@ func (api *API) GetMyStarBalanceWithContext(ctx context.Context) (StarAmount, er
// GetStarTransactions returns Telegram Star transactions for the bot. // GetStarTransactions returns Telegram Star transactions for the bot.
// See https://core.telegram.org/bots/api#getstartransactions // See https://core.telegram.org/bots/api#getstartransactions
func (api *API) GetStarTransactions(params GetStarTransactionsP) (StarTransactions, error) { func (api *API) GetStarTransactions(params GetStarTransactions) (StarTransactions, error) {
req := NewRequest[StarTransactions]("getStarTransactions", params) req := NewRequest[StarTransactions]("getStarTransactions", params)
return req.Do(api) return req.Do(api)
} }
@@ -34,14 +34,14 @@ func (api *API) GetStarTransactions(params GetStarTransactionsP) (StarTransactio
// GetStarTransactionsWithContext is the context-aware variant of GetStarTransactions. // GetStarTransactionsWithContext is the context-aware variant of GetStarTransactions.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#getstartransactions // See https://core.telegram.org/bots/api#getstartransactions
func (api *API) GetStarTransactionsWithContext(ctx context.Context, params GetStarTransactionsP) (StarTransactions, error) { func (api *API) GetStarTransactionsWithContext(ctx context.Context, params GetStarTransactions) (StarTransactions, error) {
req := NewRequest[StarTransactions]("getStarTransactions", params) req := NewRequest[StarTransactions]("getStarTransactions", params)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// RefundStarPaymentP holds parameters for the refundStarPayment method. // RefundStarPayment holds parameters for the refundStarPayment method.
// See https://core.telegram.org/bots/api#refundstarpayment // See https://core.telegram.org/bots/api#refundstarpayment
type RefundStarPaymentP struct { type RefundStarPayment struct {
UserID int64 `json:"user_id"` UserID int64 `json:"user_id"`
TelegramPaymentChargeID string `json:"telegram_payment_charge_id"` TelegramPaymentChargeID string `json:"telegram_payment_charge_id"`
} }
@@ -49,7 +49,7 @@ type RefundStarPaymentP struct {
// RefundStarPayment refunds a successful Telegram Stars payment. // RefundStarPayment refunds a successful Telegram Stars payment.
// Returns true on success. // Returns true on success.
// See https://core.telegram.org/bots/api#refundstarpayment // See https://core.telegram.org/bots/api#refundstarpayment
func (api *API) RefundStarPayment(params RefundStarPaymentP) (bool, error) { func (api *API) RefundStarPayment(params RefundStarPayment) (bool, error) {
req := NewRequest[bool]("refundStarPayment", params) req := NewRequest[bool]("refundStarPayment", params)
return req.Do(api) return req.Do(api)
} }
@@ -57,14 +57,14 @@ func (api *API) RefundStarPayment(params RefundStarPaymentP) (bool, error) {
// RefundStarPaymentWithContext is the context-aware variant of RefundStarPayment. // RefundStarPaymentWithContext is the context-aware variant of RefundStarPayment.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#refundstarpayment // See https://core.telegram.org/bots/api#refundstarpayment
func (api *API) RefundStarPaymentWithContext(ctx context.Context, params RefundStarPaymentP) (bool, error) { func (api *API) RefundStarPaymentWithContext(ctx context.Context, params RefundStarPayment) (bool, error) {
req := NewRequest[bool]("refundStarPayment", params) req := NewRequest[bool]("refundStarPayment", params)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// EditUserStarSubscriptionP holds parameters for the editUserStarSubscription method. // EditUserStarSubscription holds parameters for the editUserStarSubscription method.
// See https://core.telegram.org/bots/api#edituserstarsubscription // See https://core.telegram.org/bots/api#edituserstarsubscription
type EditUserStarSubscriptionP struct { type EditUserStarSubscription struct {
UserID int64 `json:"user_id"` UserID int64 `json:"user_id"`
TelegramPaymentChargeID string `json:"telegram_payment_charge_id"` TelegramPaymentChargeID string `json:"telegram_payment_charge_id"`
IsCanceled bool `json:"is_canceled"` IsCanceled bool `json:"is_canceled"`
@@ -73,7 +73,7 @@ type EditUserStarSubscriptionP struct {
// EditUserStarSubscription cancels or re-enables a user star subscription extension. // EditUserStarSubscription cancels or re-enables a user star subscription extension.
// Returns true on success. // Returns true on success.
// See https://core.telegram.org/bots/api#edituserstarsubscription // See https://core.telegram.org/bots/api#edituserstarsubscription
func (api *API) EditUserStarSubscription(params EditUserStarSubscriptionP) (bool, error) { func (api *API) EditUserStarSubscription(params EditUserStarSubscription) (bool, error) {
req := NewRequest[bool]("editUserStarSubscription", params) req := NewRequest[bool]("editUserStarSubscription", params)
return req.Do(api) return req.Do(api)
} }
@@ -81,7 +81,7 @@ func (api *API) EditUserStarSubscription(params EditUserStarSubscriptionP) (bool
// EditUserStarSubscriptionWithContext is the context-aware variant of EditUserStarSubscription. // EditUserStarSubscriptionWithContext is the context-aware variant of EditUserStarSubscription.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#edituserstarsubscription // See https://core.telegram.org/bots/api#edituserstarsubscription
func (api *API) EditUserStarSubscriptionWithContext(ctx context.Context, params EditUserStarSubscriptionP) (bool, error) { func (api *API) EditUserStarSubscriptionWithContext(ctx context.Context, params EditUserStarSubscription) (bool, error) {
req := NewRequest[bool]("editUserStarSubscription", params) req := NewRequest[bool]("editUserStarSubscription", params)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
+64 -64
View File
@@ -2,9 +2,9 @@ package tgapi
import "context" import "context"
// SendStickerP holds parameters for the sendSticker method. // SendSticker holds parameters for the sendSticker method.
// See https://core.telegram.org/bots/api#sendsticker // See https://core.telegram.org/bots/api#sendsticker
type SendStickerP struct { type SendSticker struct {
BusinessConnectionID string `json:"business_connection_id,omitempty"` BusinessConnectionID string `json:"business_connection_id,omitempty"`
ChatID int64 `json:"chat_id"` ChatID int64 `json:"chat_id"`
MessageThreadID int `json:"message_thread_id,omitempty"` MessageThreadID int `json:"message_thread_id,omitempty"`
@@ -24,7 +24,7 @@ type SendStickerP struct {
// SendSticker sends a static .WEBP, animated .TGS, or video .WEBM sticker. // SendSticker sends a static .WEBP, animated .TGS, or video .WEBM sticker.
// See https://core.telegram.org/bots/api#sendsticker // See https://core.telegram.org/bots/api#sendsticker
func (api *API) SendSticker(params SendStickerP) (Message, error) { func (api *API) SendSticker(params SendSticker) (Message, error) {
req := NewRequestWithChatID[Message]("sendSticker", params, params.ChatID) req := NewRequestWithChatID[Message]("sendSticker", params, params.ChatID)
return req.Do(api) return req.Do(api)
} }
@@ -32,20 +32,20 @@ func (api *API) SendSticker(params SendStickerP) (Message, error) {
// SendStickerWithContext is the context-aware variant of SendSticker. // SendStickerWithContext is the context-aware variant of SendSticker.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#sendsticker // See https://core.telegram.org/bots/api#sendsticker
func (api *API) SendStickerWithContext(ctx context.Context, params SendStickerP) (Message, error) { func (api *API) SendStickerWithContext(ctx context.Context, params SendSticker) (Message, error) {
req := NewRequestWithChatID[Message]("sendSticker", params, params.ChatID) req := NewRequestWithChatID[Message]("sendSticker", params, params.ChatID)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// GetStickerSetP holds parameters for the getStickerSet method. // GetStickerSet holds parameters for the getStickerSet method.
// See https://core.telegram.org/bots/api#getstickerset // See https://core.telegram.org/bots/api#getstickerset
type GetStickerSetP struct { type GetStickerSet struct {
Name string `json:"name"` Name string `json:"name"`
} }
// GetStickerSet returns a sticker set by its name. // GetStickerSet returns a sticker set by its name.
// See https://core.telegram.org/bots/api#getstickerset // See https://core.telegram.org/bots/api#getstickerset
func (api *API) GetStickerSet(params GetStickerSetP) (StickerSet, error) { func (api *API) GetStickerSet(params GetStickerSet) (StickerSet, error) {
req := NewRequest[StickerSet]("getStickerSet", params) req := NewRequest[StickerSet]("getStickerSet", params)
return req.Do(api) return req.Do(api)
} }
@@ -53,20 +53,20 @@ func (api *API) GetStickerSet(params GetStickerSetP) (StickerSet, error) {
// GetStickerSetWithContext is the context-aware variant of GetStickerSet. // GetStickerSetWithContext is the context-aware variant of GetStickerSet.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#getstickerset // See https://core.telegram.org/bots/api#getstickerset
func (api *API) GetStickerSetWithContext(ctx context.Context, params GetStickerSetP) (StickerSet, error) { func (api *API) GetStickerSetWithContext(ctx context.Context, params GetStickerSet) (StickerSet, error) {
req := NewRequest[StickerSet]("getStickerSet", params) req := NewRequest[StickerSet]("getStickerSet", params)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// GetCustomEmojiStickersP holds parameters for the getCustomEmojiStickers method. // GetCustomEmojiStickers holds parameters for the getCustomEmojiStickers method.
// See https://core.telegram.org/bots/api#getcustomemojistickers // See https://core.telegram.org/bots/api#getcustomemojistickers
type GetCustomEmojiStickersP struct { type GetCustomEmojiStickers struct {
CustomEmojiIDs []string `json:"custom_emoji_ids"` CustomEmojiIDs []string `json:"custom_emoji_ids"`
} }
// GetCustomEmojiStickers returns information about custom emoji stickers by their IDs. // GetCustomEmojiStickers returns information about custom emoji stickers by their IDs.
// See https://core.telegram.org/bots/api#getcustomemojistickers // See https://core.telegram.org/bots/api#getcustomemojistickers
func (api *API) GetCustomEmojiStickers(params GetCustomEmojiStickersP) ([]Sticker, error) { func (api *API) GetCustomEmojiStickers(params GetCustomEmojiStickers) ([]Sticker, error) {
req := NewRequest[[]Sticker]("getCustomEmojiStickers", params) req := NewRequest[[]Sticker]("getCustomEmojiStickers", params)
return req.Do(api) return req.Do(api)
} }
@@ -74,14 +74,14 @@ func (api *API) GetCustomEmojiStickers(params GetCustomEmojiStickersP) ([]Sticke
// GetCustomEmojiStickersWithContext is the context-aware variant of GetCustomEmojiStickers. // GetCustomEmojiStickersWithContext is the context-aware variant of GetCustomEmojiStickers.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#getcustomemojistickers // See https://core.telegram.org/bots/api#getcustomemojistickers
func (api *API) GetCustomEmojiStickersWithContext(ctx context.Context, params GetCustomEmojiStickersP) ([]Sticker, error) { func (api *API) GetCustomEmojiStickersWithContext(ctx context.Context, params GetCustomEmojiStickers) ([]Sticker, error) {
req := NewRequest[[]Sticker]("getCustomEmojiStickers", params) req := NewRequest[[]Sticker]("getCustomEmojiStickers", params)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// UploadStickerFileP holds parameters for the uploadStickerFile method. // UploadStickerFile holds parameters for the uploadStickerFile method.
// See https://core.telegram.org/bots/api#uploadstickerfile // See https://core.telegram.org/bots/api#uploadstickerfile
type UploadStickerFileP struct { type UploadStickerFile struct {
UserID int64 `json:"user_id"` UserID int64 `json:"user_id"`
StickerFormat InputStickerFormat `json:"sticker_format"` StickerFormat InputStickerFormat `json:"sticker_format"`
} }
@@ -89,7 +89,7 @@ type UploadStickerFileP struct {
// UploadStickerFile uploads a sticker file for later use in sticker set methods. // UploadStickerFile uploads a sticker file for later use in sticker set methods.
// sticker is the file to upload. // sticker is the file to upload.
// See https://core.telegram.org/bots/api#uploadstickerfile // See https://core.telegram.org/bots/api#uploadstickerfile
func (api *API) UploadStickerFile(params UploadStickerFileP, sticker UploaderFile) (File, error) { func (api *API) UploadStickerFile(params UploadStickerFile, sticker UploaderFile) (File, error) {
uploader := NewUploader(api) uploader := NewUploader(api)
defer func() { defer func() {
_ = uploader.Close() _ = uploader.Close()
@@ -101,7 +101,7 @@ func (api *API) UploadStickerFile(params UploadStickerFileP, sticker UploaderFil
// UploadStickerFileWithContext is the context-aware variant of UploadStickerFile. // UploadStickerFileWithContext is the context-aware variant of UploadStickerFile.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#uploadstickerfile // See https://core.telegram.org/bots/api#uploadstickerfile
func (api *API) UploadStickerFileWithContext(ctx context.Context, params UploadStickerFileP, sticker UploaderFile) (File, error) { func (api *API) UploadStickerFileWithContext(ctx context.Context, params UploadStickerFile, sticker UploaderFile) (File, error) {
uploader := NewUploader(api) uploader := NewUploader(api)
defer func() { defer func() {
_ = uploader.Close() _ = uploader.Close()
@@ -110,9 +110,9 @@ func (api *API) UploadStickerFileWithContext(ctx context.Context, params UploadS
return req.DoWithContext(ctx, uploader) return req.DoWithContext(ctx, uploader)
} }
// CreateNewStickerSetP holds parameters for the createNewStickerSet method. // CreateNewStickerSet holds parameters for the createNewStickerSet method.
// See https://core.telegram.org/bots/api#createnewstickerset // See https://core.telegram.org/bots/api#createnewstickerset
type CreateNewStickerSetP struct { type CreateNewStickerSet struct {
UserID int64 `json:"user_id"` UserID int64 `json:"user_id"`
Name string `json:"name"` Name string `json:"name"`
Title string `json:"title"` Title string `json:"title"`
@@ -125,7 +125,7 @@ type CreateNewStickerSetP struct {
// CreateNewStickerSet creates a new sticker set owned by a user. // CreateNewStickerSet creates a new sticker set owned by a user.
// Returns True on success. // Returns True on success.
// See https://core.telegram.org/bots/api#createnewstickerset // See https://core.telegram.org/bots/api#createnewstickerset
func (api *API) CreateNewStickerSet(params CreateNewStickerSetP) (bool, error) { func (api *API) CreateNewStickerSet(params CreateNewStickerSet) (bool, error) {
req := NewRequest[bool]("createNewStickerSet", params) req := NewRequest[bool]("createNewStickerSet", params)
return req.Do(api) return req.Do(api)
} }
@@ -133,14 +133,14 @@ func (api *API) CreateNewStickerSet(params CreateNewStickerSetP) (bool, error) {
// CreateNewStickerSetWithContext is the context-aware variant of CreateNewStickerSet. // CreateNewStickerSetWithContext is the context-aware variant of CreateNewStickerSet.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#createnewstickerset // See https://core.telegram.org/bots/api#createnewstickerset
func (api *API) CreateNewStickerSetWithContext(ctx context.Context, params CreateNewStickerSetP) (bool, error) { func (api *API) CreateNewStickerSetWithContext(ctx context.Context, params CreateNewStickerSet) (bool, error) {
req := NewRequest[bool]("createNewStickerSet", params) req := NewRequest[bool]("createNewStickerSet", params)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// AddStickerToSetP holds parameters for the addStickerToSet method. // AddStickerToSet holds parameters for the addStickerToSet method.
// See https://core.telegram.org/bots/api#addstickertoset // See https://core.telegram.org/bots/api#addstickertoset
type AddStickerToSetP struct { type AddStickerToSet struct {
UserID int64 `json:"user_id"` UserID int64 `json:"user_id"`
Name string `json:"name"` Name string `json:"name"`
Sticker InputSticker `json:"sticker"` Sticker InputSticker `json:"sticker"`
@@ -149,7 +149,7 @@ type AddStickerToSetP struct {
// AddStickerToSet adds a new sticker to a set created by the bot. // AddStickerToSet adds a new sticker to a set created by the bot.
// Returns True on success. // Returns True on success.
// See https://core.telegram.org/bots/api#addstickertoset // See https://core.telegram.org/bots/api#addstickertoset
func (api *API) AddStickerToSet(params AddStickerToSetP) (bool, error) { func (api *API) AddStickerToSet(params AddStickerToSet) (bool, error) {
req := NewRequest[bool]("addStickerToSet", params) req := NewRequest[bool]("addStickerToSet", params)
return req.Do(api) return req.Do(api)
} }
@@ -157,14 +157,14 @@ func (api *API) AddStickerToSet(params AddStickerToSetP) (bool, error) {
// AddStickerToSetWithContext is the context-aware variant of AddStickerToSet. // AddStickerToSetWithContext is the context-aware variant of AddStickerToSet.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#addstickertoset // See https://core.telegram.org/bots/api#addstickertoset
func (api *API) AddStickerToSetWithContext(ctx context.Context, params AddStickerToSetP) (bool, error) { func (api *API) AddStickerToSetWithContext(ctx context.Context, params AddStickerToSet) (bool, error) {
req := NewRequest[bool]("addStickerToSet", params) req := NewRequest[bool]("addStickerToSet", params)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// SetStickerPositionInSetP holds parameters for the setStickerPositionInSet method. // SetStickerPositionInSet holds parameters for the setStickerPositionInSet method.
// See https://core.telegram.org/bots/api#setstickerpositioninset // See https://core.telegram.org/bots/api#setstickerpositioninset
type SetStickerPositionInSetP struct { type SetStickerPositionInSet struct {
Sticker string `json:"sticker"` Sticker string `json:"sticker"`
Position int `json:"position"` Position int `json:"position"`
} }
@@ -172,7 +172,7 @@ type SetStickerPositionInSetP struct {
// SetStickerPositionInSet moves a sticker in a set to a specific position. // SetStickerPositionInSet moves a sticker in a set to a specific position.
// Returns True on success. // Returns True on success.
// See https://core.telegram.org/bots/api#setstickerpositioninset // See https://core.telegram.org/bots/api#setstickerpositioninset
func (api *API) SetStickerPositionInSet(params SetStickerPositionInSetP) (bool, error) { func (api *API) SetStickerPositionInSet(params SetStickerPositionInSet) (bool, error) {
req := NewRequest[bool]("setStickerPositionInSet", params) req := NewRequest[bool]("setStickerPositionInSet", params)
return req.Do(api) return req.Do(api)
} }
@@ -180,21 +180,21 @@ func (api *API) SetStickerPositionInSet(params SetStickerPositionInSetP) (bool,
// SetStickerPositionInSetWithContext is the context-aware variant of SetStickerPositionInSet. // SetStickerPositionInSetWithContext is the context-aware variant of SetStickerPositionInSet.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#setstickerpositioninset // See https://core.telegram.org/bots/api#setstickerpositioninset
func (api *API) SetStickerPositionInSetWithContext(ctx context.Context, params SetStickerPositionInSetP) (bool, error) { func (api *API) SetStickerPositionInSetWithContext(ctx context.Context, params SetStickerPositionInSet) (bool, error) {
req := NewRequest[bool]("setStickerPositionInSet", params) req := NewRequest[bool]("setStickerPositionInSet", params)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// DeleteStickerFromSetP holds parameters for the deleteStickerFromSet method. // DeleteStickerFromSet holds parameters for the deleteStickerFromSet method.
// See https://core.telegram.org/bots/api#deletestickerfromset // See https://core.telegram.org/bots/api#deletestickerfromset
type DeleteStickerFromSetP struct { type DeleteStickerFromSet struct {
Sticker string `json:"sticker"` Sticker string `json:"sticker"`
} }
// DeleteStickerFromSet deletes a sticker from a set created by the bot. // DeleteStickerFromSet deletes a sticker from a set created by the bot.
// Returns True on success. // Returns True on success.
// See https://core.telegram.org/bots/api#deletestickerfromset // See https://core.telegram.org/bots/api#deletestickerfromset
func (api *API) DeleteStickerFromSet(params DeleteStickerFromSetP) (bool, error) { func (api *API) DeleteStickerFromSet(params DeleteStickerFromSet) (bool, error) {
req := NewRequest[bool]("deleteStickerFromSet", params) req := NewRequest[bool]("deleteStickerFromSet", params)
return req.Do(api) return req.Do(api)
} }
@@ -202,14 +202,14 @@ func (api *API) DeleteStickerFromSet(params DeleteStickerFromSetP) (bool, error)
// DeleteStickerFromSetWithContext is the context-aware variant of DeleteStickerFromSet. // DeleteStickerFromSetWithContext is the context-aware variant of DeleteStickerFromSet.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#deletestickerfromset // See https://core.telegram.org/bots/api#deletestickerfromset
func (api *API) DeleteStickerFromSetWithContext(ctx context.Context, params DeleteStickerFromSetP) (bool, error) { func (api *API) DeleteStickerFromSetWithContext(ctx context.Context, params DeleteStickerFromSet) (bool, error) {
req := NewRequest[bool]("deleteStickerFromSet", params) req := NewRequest[bool]("deleteStickerFromSet", params)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// ReplaceStickerInSetP holds parameters for the replaceStickerInSet method. // ReplaceStickerInSet holds parameters for the replaceStickerInSet method.
// See https://core.telegram.org/bots/api#replacestickerinset // See https://core.telegram.org/bots/api#replacestickerinset
type ReplaceStickerInSetP struct { type ReplaceStickerInSet struct {
UserID int64 `json:"user_id"` UserID int64 `json:"user_id"`
Name string `json:"name"` Name string `json:"name"`
OldSticker string `json:"old_sticker"` OldSticker string `json:"old_sticker"`
@@ -219,7 +219,7 @@ type ReplaceStickerInSetP struct {
// ReplaceStickerInSet replaces an existing sticker in a set with a new one. // ReplaceStickerInSet replaces an existing sticker in a set with a new one.
// Returns True on success. // Returns True on success.
// See https://core.telegram.org/bots/api#replacestickerinset // See https://core.telegram.org/bots/api#replacestickerinset
func (api *API) ReplaceStickerInSet(params ReplaceStickerInSetP) (bool, error) { func (api *API) ReplaceStickerInSet(params ReplaceStickerInSet) (bool, error) {
req := NewRequest[bool]("replaceStickerInSet", params) req := NewRequest[bool]("replaceStickerInSet", params)
return req.Do(api) return req.Do(api)
} }
@@ -227,14 +227,14 @@ func (api *API) ReplaceStickerInSet(params ReplaceStickerInSetP) (bool, error) {
// ReplaceStickerInSetWithContext is the context-aware variant of ReplaceStickerInSet. // ReplaceStickerInSetWithContext is the context-aware variant of ReplaceStickerInSet.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#replacestickerinset // See https://core.telegram.org/bots/api#replacestickerinset
func (api *API) ReplaceStickerInSetWithContext(ctx context.Context, params ReplaceStickerInSetP) (bool, error) { func (api *API) ReplaceStickerInSetWithContext(ctx context.Context, params ReplaceStickerInSet) (bool, error) {
req := NewRequest[bool]("replaceStickerInSet", params) req := NewRequest[bool]("replaceStickerInSet", params)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// SetStickerEmojiListP holds parameters for the setStickerEmojiList method. // SetStickerEmojiList holds parameters for the setStickerEmojiList method.
// See https://core.telegram.org/bots/api#setstickeremojilist // See https://core.telegram.org/bots/api#setstickeremojilist
type SetStickerEmojiListP struct { type SetStickerEmojiList struct {
Sticker string `json:"sticker"` Sticker string `json:"sticker"`
EmojiList []string `json:"emoji_list"` EmojiList []string `json:"emoji_list"`
} }
@@ -242,7 +242,7 @@ type SetStickerEmojiListP struct {
// SetStickerEmojiList changes the list of emoji associated with a sticker. // SetStickerEmojiList changes the list of emoji associated with a sticker.
// Returns True on success. // Returns True on success.
// See https://core.telegram.org/bots/api#setstickeremojilist // See https://core.telegram.org/bots/api#setstickeremojilist
func (api *API) SetStickerEmojiList(params SetStickerEmojiListP) (bool, error) { func (api *API) SetStickerEmojiList(params SetStickerEmojiList) (bool, error) {
req := NewRequest[bool]("setStickerEmojiList", params) req := NewRequest[bool]("setStickerEmojiList", params)
return req.Do(api) return req.Do(api)
} }
@@ -250,14 +250,14 @@ func (api *API) SetStickerEmojiList(params SetStickerEmojiListP) (bool, error) {
// SetStickerEmojiListWithContext is the context-aware variant of SetStickerEmojiList. // SetStickerEmojiListWithContext is the context-aware variant of SetStickerEmojiList.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#setstickeremojilist // See https://core.telegram.org/bots/api#setstickeremojilist
func (api *API) SetStickerEmojiListWithContext(ctx context.Context, params SetStickerEmojiListP) (bool, error) { func (api *API) SetStickerEmojiListWithContext(ctx context.Context, params SetStickerEmojiList) (bool, error) {
req := NewRequest[bool]("setStickerEmojiList", params) req := NewRequest[bool]("setStickerEmojiList", params)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// SetStickerKeywordsP holds parameters for the setStickerKeywords method. // SetStickerKeywords holds parameters for the setStickerKeywords method.
// See https://core.telegram.org/bots/api#setstickerkeywords // See https://core.telegram.org/bots/api#setstickerkeywords
type SetStickerKeywordsP struct { type SetStickerKeywords struct {
Sticker string `json:"sticker"` Sticker string `json:"sticker"`
Keywords []string `json:"keywords"` Keywords []string `json:"keywords"`
} }
@@ -265,7 +265,7 @@ type SetStickerKeywordsP struct {
// SetStickerKeywords changes the keywords of a sticker. // SetStickerKeywords changes the keywords of a sticker.
// Returns True on success. // Returns True on success.
// See https://core.telegram.org/bots/api#setstickerkeywords // See https://core.telegram.org/bots/api#setstickerkeywords
func (api *API) SetStickerKeywords(params SetStickerKeywordsP) (bool, error) { func (api *API) SetStickerKeywords(params SetStickerKeywords) (bool, error) {
req := NewRequest[bool]("setStickerKeywords", params) req := NewRequest[bool]("setStickerKeywords", params)
return req.Do(api) return req.Do(api)
} }
@@ -273,14 +273,14 @@ func (api *API) SetStickerKeywords(params SetStickerKeywordsP) (bool, error) {
// SetStickerKeywordsWithContext is the context-aware variant of SetStickerKeywords. // SetStickerKeywordsWithContext is the context-aware variant of SetStickerKeywords.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#setstickerkeywords // See https://core.telegram.org/bots/api#setstickerkeywords
func (api *API) SetStickerKeywordsWithContext(ctx context.Context, params SetStickerKeywordsP) (bool, error) { func (api *API) SetStickerKeywordsWithContext(ctx context.Context, params SetStickerKeywords) (bool, error) {
req := NewRequest[bool]("setStickerKeywords", params) req := NewRequest[bool]("setStickerKeywords", params)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// SetStickerMaskPositionP holds parameters for the setStickerMaskPosition method. // SetStickerMaskPosition holds parameters for the setStickerMaskPosition method.
// See https://core.telegram.org/bots/api#setstickermaskposition // See https://core.telegram.org/bots/api#setstickermaskposition
type SetStickerMaskPositionP struct { type SetStickerMaskPosition struct {
Sticker string `json:"sticker"` Sticker string `json:"sticker"`
MaskPosition *MaskPosition `json:"mask_position,omitempty"` MaskPosition *MaskPosition `json:"mask_position,omitempty"`
} }
@@ -288,7 +288,7 @@ type SetStickerMaskPositionP struct {
// SetStickerMaskPosition changes the mask position of a mask sticker. // SetStickerMaskPosition changes the mask position of a mask sticker.
// Returns True on success. // Returns True on success.
// See https://core.telegram.org/bots/api#setstickermaskposition // See https://core.telegram.org/bots/api#setstickermaskposition
func (api *API) SetStickerMaskPosition(params SetStickerMaskPositionP) (bool, error) { func (api *API) SetStickerMaskPosition(params SetStickerMaskPosition) (bool, error) {
req := NewRequest[bool]("setStickerMaskPosition", params) req := NewRequest[bool]("setStickerMaskPosition", params)
return req.Do(api) return req.Do(api)
} }
@@ -296,14 +296,14 @@ func (api *API) SetStickerMaskPosition(params SetStickerMaskPositionP) (bool, er
// SetStickerMaskPositionWithContext is the context-aware variant of SetStickerMaskPosition. // SetStickerMaskPositionWithContext is the context-aware variant of SetStickerMaskPosition.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#setstickermaskposition // See https://core.telegram.org/bots/api#setstickermaskposition
func (api *API) SetStickerMaskPositionWithContext(ctx context.Context, params SetStickerMaskPositionP) (bool, error) { func (api *API) SetStickerMaskPositionWithContext(ctx context.Context, params SetStickerMaskPosition) (bool, error) {
req := NewRequest[bool]("setStickerMaskPosition", params) req := NewRequest[bool]("setStickerMaskPosition", params)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// SetStickerSetTitleP holds parameters for the setStickerSetTitle method. // SetStickerSetTitle holds parameters for the setStickerSetTitle method.
// See https://core.telegram.org/bots/api#setstickersettitle // See https://core.telegram.org/bots/api#setstickersettitle
type SetStickerSetTitleP struct { type SetStickerSetTitle struct {
Name string `json:"name"` Name string `json:"name"`
Title string `json:"title"` Title string `json:"title"`
} }
@@ -311,7 +311,7 @@ type SetStickerSetTitleP struct {
// SetStickerSetTitle sets the title of a sticker set created by the bot. // SetStickerSetTitle sets the title of a sticker set created by the bot.
// Returns True on success. // Returns True on success.
// See https://core.telegram.org/bots/api#setstickersettitle // See https://core.telegram.org/bots/api#setstickersettitle
func (api *API) SetStickerSetTitle(params SetStickerSetTitleP) (bool, error) { func (api *API) SetStickerSetTitle(params SetStickerSetTitle) (bool, error) {
req := NewRequest[bool]("setStickerSetTitle", params) req := NewRequest[bool]("setStickerSetTitle", params)
return req.Do(api) return req.Do(api)
} }
@@ -319,14 +319,14 @@ func (api *API) SetStickerSetTitle(params SetStickerSetTitleP) (bool, error) {
// SetStickerSetTitleWithContext is the context-aware variant of SetStickerSetTitle. // SetStickerSetTitleWithContext is the context-aware variant of SetStickerSetTitle.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#setstickersettitle // See https://core.telegram.org/bots/api#setstickersettitle
func (api *API) SetStickerSetTitleWithContext(ctx context.Context, params SetStickerSetTitleP) (bool, error) { func (api *API) SetStickerSetTitleWithContext(ctx context.Context, params SetStickerSetTitle) (bool, error) {
req := NewRequest[bool]("setStickerSetTitle", params) req := NewRequest[bool]("setStickerSetTitle", params)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// SetStickerSetThumbnailP holds parameters for the setStickerSetThumbnail method. // SetStickerSetThumbnail holds parameters for the setStickerSetThumbnail method.
// See https://core.telegram.org/bots/api#setstickersetthumbnail // See https://core.telegram.org/bots/api#setstickersetthumbnail
type SetStickerSetThumbnailP struct { type SetStickerSetThumbnail struct {
Name string `json:"name"` Name string `json:"name"`
UserID int64 `json:"user_id"` UserID int64 `json:"user_id"`
Thumbnail string `json:"thumbnail"` Thumbnail string `json:"thumbnail"`
@@ -336,7 +336,7 @@ type SetStickerSetThumbnailP struct {
// SetStickerSetThumbnail sets the thumbnail of a sticker set. // SetStickerSetThumbnail sets the thumbnail of a sticker set.
// Returns True on success. // Returns True on success.
// See https://core.telegram.org/bots/api#setstickersetthumbnail // See https://core.telegram.org/bots/api#setstickersetthumbnail
func (api *API) SetStickerSetThumbnail(params SetStickerSetThumbnailP) (bool, error) { func (api *API) SetStickerSetThumbnail(params SetStickerSetThumbnail) (bool, error) {
req := NewRequest[bool]("setStickerSetThumbnail", params) req := NewRequest[bool]("setStickerSetThumbnail", params)
return req.Do(api) return req.Do(api)
} }
@@ -344,14 +344,14 @@ func (api *API) SetStickerSetThumbnail(params SetStickerSetThumbnailP) (bool, er
// SetStickerSetThumbnailWithContext is the context-aware variant of SetStickerSetThumbnail. // SetStickerSetThumbnailWithContext is the context-aware variant of SetStickerSetThumbnail.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#setstickersetthumbnail // See https://core.telegram.org/bots/api#setstickersetthumbnail
func (api *API) SetStickerSetThumbnailWithContext(ctx context.Context, params SetStickerSetThumbnailP) (bool, error) { func (api *API) SetStickerSetThumbnailWithContext(ctx context.Context, params SetStickerSetThumbnail) (bool, error) {
req := NewRequest[bool]("setStickerSetThumbnail", params) req := NewRequest[bool]("setStickerSetThumbnail", params)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// SetCustomEmojiStickerSetThumbnailP holds parameters for the setCustomEmojiStickerSetThumbnail method. // SetCustomEmojiStickerSetThumbnail holds parameters for the setCustomEmojiStickerSetThumbnail method.
// See https://core.telegram.org/bots/api#setcustomemojistickersetthumbnail // See https://core.telegram.org/bots/api#setcustomemojistickersetthumbnail
type SetCustomEmojiStickerSetThumbnailP struct { type SetCustomEmojiStickerSetThumbnail struct {
Name string `json:"name"` Name string `json:"name"`
CustomEmojiID string `json:"custom_emoji_id,omitempty"` CustomEmojiID string `json:"custom_emoji_id,omitempty"`
} }
@@ -359,7 +359,7 @@ type SetCustomEmojiStickerSetThumbnailP struct {
// SetCustomEmojiStickerSetThumbnail sets the thumbnail of a custom emoji sticker set. // SetCustomEmojiStickerSetThumbnail sets the thumbnail of a custom emoji sticker set.
// Returns True on success. // Returns True on success.
// See https://core.telegram.org/bots/api#setcustomemojistickersetthumbnail // See https://core.telegram.org/bots/api#setcustomemojistickersetthumbnail
func (api *API) SetCustomEmojiStickerSetThumbnail(params SetCustomEmojiStickerSetThumbnailP) (bool, error) { func (api *API) SetCustomEmojiStickerSetThumbnail(params SetCustomEmojiStickerSetThumbnail) (bool, error) {
req := NewRequest[bool]("setCustomEmojiStickerSetThumbnail", params) req := NewRequest[bool]("setCustomEmojiStickerSetThumbnail", params)
return req.Do(api) return req.Do(api)
} }
@@ -367,21 +367,21 @@ func (api *API) SetCustomEmojiStickerSetThumbnail(params SetCustomEmojiStickerSe
// SetCustomEmojiStickerSetThumbnailWithContext is the context-aware variant of SetCustomEmojiStickerSetThumbnail. // SetCustomEmojiStickerSetThumbnailWithContext is the context-aware variant of SetCustomEmojiStickerSetThumbnail.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#setcustomemojistickersetthumbnail // See https://core.telegram.org/bots/api#setcustomemojistickersetthumbnail
func (api *API) SetCustomEmojiStickerSetThumbnailWithContext(ctx context.Context, params SetCustomEmojiStickerSetThumbnailP) (bool, error) { func (api *API) SetCustomEmojiStickerSetThumbnailWithContext(ctx context.Context, params SetCustomEmojiStickerSetThumbnail) (bool, error) {
req := NewRequest[bool]("setCustomEmojiStickerSetThumbnail", params) req := NewRequest[bool]("setCustomEmojiStickerSetThumbnail", params)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// DeleteStickerSetP holds parameters for the deleteStickerSet method. // DeleteStickerSet holds parameters for the deleteStickerSet method.
// See https://core.telegram.org/bots/api#deletestickerset // See https://core.telegram.org/bots/api#deletestickerset
type DeleteStickerSetP struct { type DeleteStickerSet struct {
Name string `json:"name"` Name string `json:"name"`
} }
// DeleteStickerSet deletes a sticker set created by the bot. // DeleteStickerSet deletes a sticker set created by the bot.
// Returns True on success. // Returns True on success.
// See https://core.telegram.org/bots/api#deletestickerset // See https://core.telegram.org/bots/api#deletestickerset
func (api *API) DeleteStickerSet(params DeleteStickerSetP) (bool, error) { func (api *API) DeleteStickerSet(params DeleteStickerSet) (bool, error) {
req := NewRequest[bool]("deleteStickerSet", params) req := NewRequest[bool]("deleteStickerSet", params)
return req.Do(api) return req.Do(api)
} }
@@ -389,7 +389,7 @@ func (api *API) DeleteStickerSet(params DeleteStickerSetP) (bool, error) {
// DeleteStickerSetWithContext is the context-aware variant of DeleteStickerSet. // DeleteStickerSetWithContext is the context-aware variant of DeleteStickerSet.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#deletestickerset // See https://core.telegram.org/bots/api#deletestickerset
func (api *API) DeleteStickerSetWithContext(ctx context.Context, params DeleteStickerSetP) (bool, error) { func (api *API) DeleteStickerSetWithContext(ctx context.Context, params DeleteStickerSet) (bool, error) {
req := NewRequest[bool]("deleteStickerSet", params) req := NewRequest[bool]("deleteStickerSet", params)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
+2 -2
View File
@@ -38,8 +38,8 @@ const (
// Sticker represents a sticker. // Sticker represents a sticker.
// See https://core.telegram.org/bots/api#sticker // See https://core.telegram.org/bots/api#sticker
type Sticker struct { type Sticker struct {
FileId string `json:"file_id"` FileID string `json:"file_id"`
FileUniqueId string `json:"file_unique_id"` FileUniqueID string `json:"file_unique_id"`
Type StickerType `json:"type"` Type StickerType `json:"type"`
Width int `json:"width"` Width int `json:"width"`
Height int `json:"height"` Height int `json:"height"`
+289 -168
View File
@@ -6,6 +6,9 @@ import "encoding/json"
type UpdateType string type UpdateType string
const ( const (
// UpdateTypeUnknown marks an update whose payload does not match a known Telegram update kind.
UpdateTypeUnknown UpdateType = "unknown"
// UpdateTypeMessage is a regular message update. // UpdateTypeMessage is a regular message update.
UpdateTypeMessage UpdateType = "message" UpdateTypeMessage UpdateType = "message"
// UpdateTypeEditedMessage is an edited message update. // UpdateTypeEditedMessage is an edited message update.
@@ -27,8 +30,6 @@ const (
UpdateTypeEditedBusinessMessage UpdateType = "edited_business_message" UpdateTypeEditedBusinessMessage UpdateType = "edited_business_message"
// UpdateTypeDeletedBusinessMessages is a deleted business messages update. // UpdateTypeDeletedBusinessMessages is a deleted business messages update.
UpdateTypeDeletedBusinessMessages UpdateType = "deleted_business_messages" UpdateTypeDeletedBusinessMessages UpdateType = "deleted_business_messages"
// UpdateTypeDeletedBusinessMessage is kept as a backward-compatible alias.
UpdateTypeDeletedBusinessMessage UpdateType = UpdateTypeDeletedBusinessMessages
// UpdateTypeInlineQuery is an inline query update. // UpdateTypeInlineQuery is an inline query update.
UpdateTypeInlineQuery UpdateType = "inline_query" UpdateTypeInlineQuery UpdateType = "inline_query"
@@ -56,11 +57,15 @@ const (
UpdateTypeChatBoost UpdateType = "chat_boost" UpdateTypeChatBoost UpdateType = "chat_boost"
// UpdateTypeRemovedChatBoost is a removed chat boost update. // UpdateTypeRemovedChatBoost is a removed chat boost update.
UpdateTypeRemovedChatBoost UpdateType = "removed_chat_boost" UpdateTypeRemovedChatBoost UpdateType = "removed_chat_boost"
UpdateTypeManagedBot UpdateType = "managed_bot"
) )
// Update represents an incoming update from Telegram. // Update represents an incoming update from Telegram.
// See https://core.telegram.org/bots/api#update // See https://core.telegram.org/bots/api#update
type Update struct { type Update struct {
Type UpdateType `json:"-"`
UpdateID int `json:"update_id"` UpdateID int `json:"update_id"`
Message *Message `json:"message,omitempty"` Message *Message `json:"message,omitempty"`
EditedMessage *Message `json:"edited_message,omitempty"` EditedMessage *Message `json:"edited_message,omitempty"`
@@ -71,7 +76,6 @@ type Update struct {
BusinessMessage *Message `json:"business_message,omitempty"` BusinessMessage *Message `json:"business_message,omitempty"`
EditedBusinessMessage *Message `json:"edited_business_message,omitempty"` EditedBusinessMessage *Message `json:"edited_business_message,omitempty"`
DeletedBusinessMessages *BusinessMessagesDeleted `json:"deleted_business_messages,omitempty"` DeletedBusinessMessages *BusinessMessagesDeleted `json:"deleted_business_messages,omitempty"`
DeletedBusinessMessage *BusinessMessagesDeleted `json:"-"`
MessageReaction *MessageReactionUpdated `json:"message_reaction,omitempty"` MessageReaction *MessageReactionUpdated `json:"message_reaction,omitempty"`
MessageReactionCount *MessageReactionCountUpdated `json:"message_reaction_count,omitempty"` MessageReactionCount *MessageReactionCountUpdated `json:"message_reaction_count,omitempty"`
@@ -89,35 +93,98 @@ type Update struct {
ChatJoinRequest *ChatJoinRequest `json:"chat_join_request,omitempty"` ChatJoinRequest *ChatJoinRequest `json:"chat_join_request,omitempty"`
ChatBoost *ChatBoostUpdated `json:"chat_boost,omitempty"` ChatBoost *ChatBoostUpdated `json:"chat_boost,omitempty"`
RemovedChatBoost *ChatBoostRemoved `json:"removed_chat_boost,omitempty"` RemovedChatBoost *ChatBoostRemoved `json:"removed_chat_boost,omitempty"`
ManagedBot *ManagedBotUpdated `json:"managed_bot,omitempty"`
} }
func (u *Update) syncDeletedBusinessMessages() { // UnmarshalJSON decodes an update and derives its Type from the populated payload field.
if u.DeletedBusinessMessages != nil {
u.DeletedBusinessMessage = u.DeletedBusinessMessages
return
}
if u.DeletedBusinessMessage != nil {
u.DeletedBusinessMessages = u.DeletedBusinessMessage
}
}
// UnmarshalJSON keeps the deprecated DeletedBusinessMessage alias in sync.
func (u *Update) UnmarshalJSON(data []byte) error { func (u *Update) UnmarshalJSON(data []byte) error {
type alias Update type Alias Update
var aux alias
var aux Alias
if err := json.Unmarshal(data, &aux); err != nil { if err := json.Unmarshal(data, &aux); err != nil {
return err return err
} }
*u = Update(aux) *u = Update(aux)
u.syncDeletedBusinessMessages()
switch {
case u.Message != nil:
u.Type = UpdateTypeMessage
case u.EditedMessage != nil:
u.Type = UpdateTypeEditedMessage
case u.ChannelPost != nil:
u.Type = UpdateTypeChannelPost
case u.EditedChannelPost != nil:
u.Type = UpdateTypeEditedChannelPost
case u.BusinessConnection != nil:
u.Type = UpdateTypeBusinessConnection
case u.BusinessMessage != nil:
u.Type = UpdateTypeBusinessMessage
case u.EditedBusinessMessage != nil:
u.Type = UpdateTypeEditedBusinessMessage
case u.DeletedBusinessMessages != nil:
u.Type = UpdateTypeDeletedBusinessMessages
case u.MessageReaction != nil:
u.Type = UpdateTypeMessageReaction
case u.MessageReactionCount != nil:
u.Type = UpdateTypeMessageReactionCount
case u.InlineQuery != nil:
u.Type = UpdateTypeInlineQuery
case u.ChosenInlineResult != nil:
u.Type = UpdateTypeChosenInlineResult
case u.CallbackQuery != nil:
u.Type = UpdateTypeCallbackQuery
case u.ShippingQuery != nil:
u.Type = UpdateTypeShippingQuery
case u.PreCheckoutQuery != nil:
u.Type = UpdateTypePreCheckoutQuery
case u.PurchasedPaidMedia != nil:
u.Type = UpdateTypePurchasedPaidMedia
case u.Poll != nil:
u.Type = UpdateTypePoll
case u.PollAnswer != nil:
u.Type = UpdateTypePollAnswer
case u.MyChatMember != nil:
u.Type = UpdateTypeMyChatMember
case u.ChatMember != nil:
u.Type = UpdateTypeChatMember
case u.ChatJoinRequest != nil:
u.Type = UpdateTypeChatJoinRequest
case u.ChatBoost != nil:
u.Type = UpdateTypeChatBoost
case u.RemovedChatBoost != nil:
u.Type = UpdateTypeRemovedChatBoost
case u.ManagedBot != nil:
u.Type = UpdateTypeManagedBot
default:
u.Type = UpdateTypeUnknown
}
return nil return nil
} }
// MarshalJSON emits the canonical deleted_business_messages field. // WebhookInfo describes the current webhook status.
func (u Update) MarshalJSON() ([]byte, error) { // See https://core.telegram.org/bots/api#webhookinfo
u.syncDeletedBusinessMessages() type WebhookInfo struct {
type alias Update URL string `json:"url"`
return json.Marshal(alias(u)) HasCustomCertificate bool `json:"has_custom_certificate"`
PendingUpdateCount int `json:"pending_update_count"`
IPAddress string `json:"ip_address,omitempty"`
LastErrorDate int `json:"last_error_date,omitempty"`
LastErrorMessage string `json:"last_error_message,omitempty"`
LastSynchronizationErrorDate int `json:"last_synchronization_error_date,omitempty"`
MaxConnections int `json:"max_connections,omitempty"`
AllowedUpdates []string `json:"allowed_updates,omitempty"`
}
type ProximityAlertTriggered struct {
Traveler User `json:"traveler"`
Watcher User `json:"watcher"`
Distance int `json:"distance"`
} }
// InlineQuery represents an incoming inline query. // InlineQuery represents an incoming inline query.
@@ -141,115 +208,15 @@ type ChosenInlineResult struct {
Query string `json:"query"` Query string `json:"query"`
} }
// ShippingQuery represents an incoming shipping query.
// See https://core.telegram.org/bots/api#shippingquery
type ShippingQuery struct {
ID string `json:"id"`
From User `json:"from"`
InvoicePayload string `json:"invoice_payload"`
ShippingAddress ShippingAddress `json:"shipping_address"`
}
// ShippingAddress represents a shipping address.
// See https://core.telegram.org/bots/api#shippingaddress
type ShippingAddress struct {
CountryCode string `json:"country_code"`
State string `json:"state"`
City string `json:"city"`
StreetLine1 string `json:"street_line1"`
StreetLine2 string `json:"street_line2"`
PostCode string `json:"post_code"`
}
// OrderInfo represents information about an order.
// See https://core.telegram.org/bots/api#orderinfo
type OrderInfo struct {
Name string `json:"name"`
PhoneNumber string `json:"phone_number"`
Email string `json:"email"`
ShippingAddress ShippingAddress `json:"shipping_address"`
}
// PreCheckoutQuery represents an incoming pre-checkout query.
// See https://core.telegram.org/bots/api#precheckoutquery
type PreCheckoutQuery struct {
ID string `json:"id"`
From User `json:"from"`
Currency string `json:"currency"`
TotalAmount int `json:"total_amount"`
InvoicePayload string `json:"invoice_payload"`
ShippingOptionID string `json:"shipping_option_id"`
OrderInfo *OrderInfo `json:"order_info,omitempty"`
}
// PaidMediaPurchased represents a purchased paid media.
// See https://core.telegram.org/bots/api#paidmediapurchased
type PaidMediaPurchased struct {
From User `json:"from"`
PaidMediaPayload string `json:"paid_media_payload"`
}
// File represents a file ready to be downloaded. // File represents a file ready to be downloaded.
// See https://core.telegram.org/bots/api#file // See https://core.telegram.org/bots/api#file
type File struct { type File struct {
FileId string `json:"file_id"` FileID string `json:"file_id"`
FileUniqueID string `json:"file_unique_id"` FileUniqueID string `json:"file_unique_id"`
FileSize int64 `json:"file_size,omitempty"` FileSize int64 `json:"file_size,omitempty"`
FilePath string `json:"file_path,omitempty"` FilePath string `json:"file_path,omitempty"`
} }
// Audio represents an audio file to be treated as music by the Telegram clients.
// See https://core.telegram.org/bots/api#audio
type Audio struct {
FileID string `json:"file_id"`
FileUniqueID string `json:"file_unique_id"`
Duration int `json:"duration"`
Performer string `json:"performer,omitempty"`
Title string `json:"title,omitempty"`
FileName string `json:"file_name,omitempty"`
MimeType string `json:"mime_type,omitempty"`
FileSize int64 `json:"file_size,omitempty"`
Thumbnail *PhotoSize `json:"thumbnail,omitempty"`
}
// PollOption contains information about one answer option in a poll.
// See https://core.telegram.org/bots/api#polloption
type PollOption struct {
Text string `json:"text"`
TextEntities []MessageEntity `json:"text_entities"`
VoterCount int `json:"voter_count"`
}
// Poll contains information about a poll.
// See https://core.telegram.org/bots/api#poll
type Poll struct {
ID string `json:"id"`
Question string `json:"question"`
QuestionEntities []MessageEntity `json:"question_entities"`
Options []PollOption `json:"options"`
TotalVoterCount int `json:"total_voter_count"`
IsClosed bool `json:"is_closed"`
IsAnonymous bool `json:"is_anonymous"`
Type PollType `json:"type"`
AllowsMultipleAnswers bool `json:"allows_multiple_answers"`
CorrectOptionID *int `json:"correct_option_id,omitempty"`
Explanation *string `json:"explanation,omitempty"`
ExplanationEntities []MessageEntity `json:"explanation_entities,omitempty"`
OpenPeriod int `json:"open_period,omitempty"`
CloseDate int `json:"close_date,omitempty"`
}
// PollAnswer represents an answer of a user in a poll.
// See https://core.telegram.org/bots/api#pollanswer
type PollAnswer struct {
PollID string `json:"poll_id"`
VoterChat Chat `json:"voter_chat"`
User User `json:"user"`
OptionIDS []int `json:"option_ids"`
}
// ChatMemberUpdated represents changes in the status of a chat member. // ChatMemberUpdated represents changes in the status of a chat member.
// See https://core.telegram.org/bots/api#chatmemberupdated // See https://core.telegram.org/bots/api#chatmemberupdated
type ChatMemberUpdated struct { type ChatMemberUpdated struct {
@@ -311,18 +278,17 @@ type WebAppInfo struct {
URL string `json:"url"` URL string `json:"url"`
} }
type WebAppData struct {
Data string `json:"data"`
ButtonText string `json:"button_text"`
}
// StarAmount represents an amount of Telegram Stars. // StarAmount represents an amount of Telegram Stars.
type StarAmount struct { type StarAmount struct {
Amount int `json:"amount"` Amount int `json:"amount"`
NanostarAmount int `json:"nanostar_amount"` NanostarAmount int `json:"nanostar_amount"`
} }
// Story represents a story.
type Story struct {
Chat Chat `json:"chat"`
ID int `json:"id"`
}
// AcceptedGiftTypes represents the types of gifts accepted by a user or chat. // AcceptedGiftTypes represents the types of gifts accepted by a user or chat.
type AcceptedGiftTypes struct { type AcceptedGiftTypes struct {
UnlimitedGifts bool `json:"unlimited_gifts"` UnlimitedGifts bool `json:"unlimited_gifts"`
@@ -332,6 +298,58 @@ type AcceptedGiftTypes struct {
GiftsFromChannels bool `json:"gifts_from_channels"` GiftsFromChannels bool `json:"gifts_from_channels"`
} }
// GiftBackground represents the background of a gift.
type GiftBackground struct {
CenterColor int `json:"center_color"`
EdgeColor int `json:"edge_color"`
TextColor int `json:"text_color"`
}
// Gift represents a gift that can be sent.
type Gift struct {
ID string `json:"id"`
Sticker Sticker `json:"sticker"`
StarCount int `json:"star_count"`
UpdateStarCount *int `json:"update_star_count,omitempty"`
IsPremium *bool `json:"is_premium,omitempty"`
HasColors *bool `json:"has_colors,omitempty"`
TotalCount *int `json:"total_count,omitempty"`
RemainingCount *int `json:"remaining_count,omitempty"`
PersonalTotalCount *int `json:"personal_total_count,omitempty"`
PersonalRemainingCount *int `json:"personal_remaining_count,omitempty"`
Background *GiftBackground `json:"background,omitempty"`
UniqueGiftVariantColor *int `json:"unique_gift_variant_color,omitempty"`
PublisherChat *Chat `json:"publisher_chat,omitempty"`
}
// Gifts represents a list of gifts.
type Gifts struct {
Gifts []Gift `json:"gifts"`
}
type UniqueGiftModel struct {
Name string `json:"name"`
Sticker Sticker `json:"sticker"`
RarityPerMille int `json:"rarity_per_mille"`
Rarity string `json:"rarity,omitempty"`
}
type UniqueGiftSymbol struct {
Name string `json:"name"`
Sticker Sticker `json:"sticker"`
RarityPerMille int `json:"rarity_per_mille"`
}
type UniqueGiftBackdropColors struct {
CenterColor int `json:"center_color"`
EdgeColor int `json:"edge_color"`
SymbolColor int `json:"symbol_color"`
TextColor int `json:"text_color"`
}
type UniqueGiftBackdrop struct {
Name string `json:"name"`
Colors UniqueGiftBackdropColors `json:"colors"`
RarityPerMille int `json:"rarity_per_mille"`
}
// UniqueGiftColors represents color information for a unique gift. // UniqueGiftColors represents color information for a unique gift.
type UniqueGiftColors struct { type UniqueGiftColors struct {
ModelCustomEmojiID string `json:"model_custom_emoji_id"` ModelCustomEmojiID string `json:"model_custom_emoji_id"`
@@ -342,67 +360,79 @@ type UniqueGiftColors struct {
DarkThemeOtherColors []int `json:"dark_theme_other_colors"` DarkThemeOtherColors []int `json:"dark_theme_other_colors"`
} }
// GiftBackground represents the background of a gift. type UniqueGift struct {
type GiftBackground struct { GiftID string `json:"gift_id"`
CenterColor int `json:"center_color"` BaseName string `json:"base_name"`
EdgeColor int `json:"edge_color"` Name string `json:"name"`
TextColor int `json:"text_color"` Number int `json:"number"`
Model UniqueGiftModel `json:"model"`
Symbol UniqueGiftSymbol `json:"symbol"`
Backdrop UniqueGiftBackdrop `json:"backdrop"`
IsPremium bool `json:"is_premium,omitempty"`
IsBurned bool `json:"is_burned,omitempty"`
IsFromBlockchain bool `json:"is_from_blockchain,omitempty"`
Colors *UniqueGiftColors `json:"colors,omitempty"`
PublisherChat *Chat `json:"publisher_chat,omitempty"`
} }
// Gift represents a gift that can be sent. type GiftInfo struct {
type Gift struct { Gift Gift `json:"gift"`
ID string `json:"id"`
Sticker Sticker `json:"sticker"`
StarCount int `json:"star_count"`
UpdateStarCount *int `json:"update_star_count,omitempty"`
IsPremium *bool `json:"is_premium,omitempty"`
HasColors *bool `json:"has_colors,omitempty"`
TotalCount *int `json:"total_count,omitempty"`
RemainingCount *int `json:"remaining_count,omitempty"`
PersonalTotalCount *int `json:"personal_total_count,omitempty"`
PersonalRemainingCount *int `json:"personal_remaining_count,omitempty"`
Background GiftBackground `json:"background,omitempty"`
UniqueGiftVariantColor *int `json:"unique_gift_variant_color,omitempty"`
PublisherChat *Chat `json:"publisher_chat,omitempty"`
}
// Gifts represents a list of gifts. OwnedGiftID string `json:"owned_gift_id,omitempty"`
type Gifts struct { ConvertStarCount int `json:"convert_star_count,omitempty"`
Gifts []Gift `json:"gifts"` PrepaidUpgradeStarCount int `json:"prepaid_upgrade_star_count,omitempty"`
IsUpgradeSeparate bool `json:"is_upgrade_separate,omitempty"`
CanBeUpgraded bool `json:"can_be_upgraded,omitempty"`
Text string `json:"text,omitempty"`
Entities []MessageEntity `json:"entities,omitempty"`
IsPrivate bool `json:"is_private,omitempty"`
UniqueGiftNumber int `json:"unique_gift_number,omitempty"`
}
type UniqueGiftInfo struct {
Gift UniqueGift `json:"gift"`
Origin string `json:"origin"`
LastResaleCurrency string `json:"last_resale_currency,omitempty"`
LastResaleAmount int `json:"last_resale_amount,omitempty"`
OwnedGiftID string `json:"owned_gift_id,omitempty"`
TransferStarCount int `json:"transfer_star_count,omitempty"`
NextTransferDate int `json:"next_transfer_date,omitempty"`
} }
// OwnedGiftType represents the type of an owned gift. // OwnedGiftType represents the type of an owned gift.
type OwnedGiftType string type OwnedGiftType string
const ( const (
// OwnedGiftRegularType identifies a regular owned gift.
OwnedGiftRegularType OwnedGiftType = "regular" OwnedGiftRegularType OwnedGiftType = "regular"
OwnedGiftUniqueType OwnedGiftType = "unique" // OwnedGiftUniqueType identifies a unique owned gift.
OwnedGiftUniqueType OwnedGiftType = "unique"
) )
// OwnedGift represents a gift owned by a user or chat. // OwnedGift represents a gift owned by a user or chat.
type OwnedGift struct { type OwnedGift struct {
Type OwnedGiftType `json:"type"` Type OwnedGiftType `json:"type"`
OwnerGiftID *string `json:"owner_gift_id,omitempty"` OwnedGiftID string `json:"ownen_gift_id,omitempty"`
SendDate *int `json:"send_date,omitempty"` SendDate int `json:"send_date,omitempty"`
IsSaved *bool `json:"is_saved,omitempty"` IsSaved bool `json:"is_saved,omitempty"`
// Fields specific to "regular" type // Fields specific to "regular" type
Gift Gift `json:"gift"` Gift Gift `json:"gift"`
SenderUser User `json:"sender_user,omitempty"` SenderUser *User `json:"sender_user,omitempty"`
Text string `json:"text,omitempty"` Text string `json:"text,omitempty"`
Entities []MessageEntity `json:"entities,omitempty"` Entities []MessageEntity `json:"entities,omitempty"`
IsPrivate *bool `json:"is_private,omitempty"` IsPrivate bool `json:"is_private,omitempty"`
CanBeUpgraded *bool `json:"can_be_upgraded,omitempty"` CanBeUpgraded bool `json:"can_be_upgraded,omitempty"`
WasRefunded *bool `json:"was_refunded,omitempty"` WasRefunded bool `json:"was_refunded,omitempty"`
ConvertStarCount *int `json:"convert_star_count,omitempty"` ConvertStarCount int `json:"convert_star_count,omitempty"`
PrepaidUpgradeStarCount *int `json:"prepaid_upgrade_star_count,omitempty"` PrepaidUpgradeStarCount int `json:"prepaid_upgrade_star_count,omitempty"`
IsUpgradeSeparate *bool `json:"is_upgrade_separate,omitempty"` IsUpgradeSeparate bool `json:"is_upgrade_separate,omitempty"`
UniqueGiftNumber *int `json:"unique_gift_number,omitempty"` UniqueGiftNumber int `json:"unique_gift_number,omitempty"`
// Fields specific to "unique" type // Fields specific to "unique" type
CanBeTransferred *bool `json:"can_be_transferred,omitempty"` CanBeTransferred bool `json:"can_be_transferred,omitempty"`
TransferStarCount *int `json:"transfer_star_count,omitempty"` TransferStarCount int `json:"transfer_star_count,omitempty"`
NextTransferDate *int `json:"next_transfer_date,omitempty"` NextTransferDate int `json:"next_transfer_date,omitempty"`
} }
// OwnedGifts represents a list of owned gifts with pagination. // OwnedGifts represents a list of owned gifts with pagination.
@@ -411,3 +441,94 @@ type OwnedGifts struct {
Gifts []OwnedGift `json:"gifts"` Gifts []OwnedGift `json:"gifts"`
NextOffset string `json:"next_offset"` NextOffset string `json:"next_offset"`
} }
type GiveawayCreated struct {
PrizeStarCount int `json:"prize_star_count,omitempty"`
}
type Giveaway struct {
Chats []Chat `json:"chats"`
WinnersSelectionDate int `json:"winners_selection_date"`
WinnerCount int `json:"winner_count"`
OnlyNewMembers bool `json:"only_new_members,omitempty"`
HasPublicWinners bool `json:"has_public_winners,omitempty"`
PrizeDescription string `json:"prize_description,omitempty"`
CountryCodes []string `json:"country_codes,omitempty"`
PrizeStarCount int `json:"prize_star_count,omitempty"`
PremiumSubscriptionMonthCount int `json:"premium_subscription_month_count,omitempty"`
}
type GiveawayWinners struct {
Chat Chat `json:"chat"`
GiveawayMessageID int `json:"giveaway_message_id"`
WinnersSelectionDate int `json:"winners_selection_date"`
WinnerCount int `json:"winner_count"`
Winners []User `json:"winners"`
AdditionalChatCount int `json:"additional_chat_count,omitempty"`
PrizeStarCount int `json:"prize_star_count,omitempty"`
PremiumSubscriptionMonthCount int `json:"premium_subscription_month_count,omitempty"`
UnclaimedPrizeCount int `json:"unclaimed_prize_count,omitempty"`
OnlyNewMembers bool `json:"only_new_members,omitempty"`
WasRefunded bool `json:"was_refunded,omitempty"`
PrizeDescription string `json:"prize_description,omitempty"`
}
type GiveawayCompleted struct {
WinnerCount int `json:"winner_count"`
UnclaimedPrizeCount int `json:"unclaimed_prize_count,omitempty"`
GiveawayMessage *Message `json:"giveaway_message,omitempty"`
IsStarGiveaway bool `json:"is_star_giveaway,omitempty"`
}
type WriteAccessAllowed struct {
FromRequest bool `json:"from_request,omitempty"`
WebAppName string `json:"web_app_name,omitempty"`
FromAttachmentMenu bool `json:"from_attachment_menu,omitempty"`
}
type BackgroundFillType string
const (
BackgroundFillSolidType BackgroundFillType = "solid"
BackgroundFillGradientType BackgroundFillType = "gradient"
BackgroundFillFreeformGradientType BackgroundFillType = "freeform_gradient"
)
type BackgroundFill struct {
Type BackgroundFillType `json:"type"`
Color int `json:"color,omitempty"`
TopColor int `json:"top_color,omitempty"`
BottomColor int `json:"bottom_color,omitempty"`
RotationAngle int `json:"rotation_angle,omitempty"`
Colors []int `json:"colors,omitempty"`
}
type BackgroundTypeType string
const (
BackgroundTypeFillType BackgroundTypeType = "fill"
BackgroundTypeWallpaperType BackgroundTypeType = "wallpaper"
BackgroundTypePatternType BackgroundTypeType = "pattern"
BackgroundTypeChatThemeType BackgroundTypeType = "chat_theme"
)
type BackgroundType struct {
Type BackgroundTypeType `json:"type"`
Fill *BackgroundFill `json:"fill,omitempty"`
DarkThemeDimming int `json:"dark_theme_dimming,omitempty"`
Document *Document `json:"document,omitempty"`
IsBlurred bool `json:"is_blurred,omitempty"`
IsMoving bool `json:"is_moving,omitempty"`
Intensity int `json:"intensity,omitempty"`
IsInverted bool `json:"is_inverted,omitempty"`
ThemeName string `json:"theme_name,omitempty"`
}
+181 -33
View File
@@ -6,41 +6,130 @@ import (
"testing" "testing"
) )
func TestUpdateDeletedBusinessMessagesUnmarshalSetsAlias(t *testing.T) { func TestUpdateUnmarshalSetsType(t *testing.T) {
var update Update tests := []struct {
err := json.Unmarshal([]byte(`{ name string
"update_id": 1, body string
"deleted_business_messages": { want UpdateType
"business_connection_id": "conn", }{
"chat": {"id": 42, "type": "private"}, {
"message_ids": [3, 5] name: "deleted business messages",
} body: `{
}`), &update) "update_id": 1,
if err != nil { "deleted_business_messages": {
t.Fatalf("Unmarshal returned error: %v", err) "business_connection_id": "conn",
"chat": {"id": 42, "type": "private"},
"message_ids": [3, 5]
}
}`,
want: UpdateTypeDeletedBusinessMessages,
},
{
name: "callback query",
body: `{
"update_id": 2,
"callback_query": {
"id": "cb",
"from": {"id": 1, "is_bot": false, "first_name": "Test"},
"chat_instance": "instance",
"data": "payload"
}
}`,
want: UpdateTypeCallbackQuery,
},
{
name: "chat boost",
body: `{
"update_id": 3,
"chat_boost": {
"chat": {"id": -1001, "type": "supergroup", "title": "Boosted"},
"boost": {
"boost_id": "boost-1",
"add_date": 1735689600,
"expiration_date": 1738291600,
"source": {
"source": "premium",
"user": {"id": 1, "is_bot": false, "first_name": "Test"}
}
}
}
}`,
want: UpdateTypeChatBoost,
},
{
name: "unknown",
body: `{"update_id":4}`,
want: UpdateTypeUnknown,
},
{
name: "managed bot",
body: `{
"update_id": 5,
"managed_bot": {
"user": {"id": 11, "is_bot": false, "first_name": "Manager"},
"bot": {"id": 12, "is_bot": true, "first_name": "Worker"}
}
}`,
want: UpdateTypeManagedBot,
},
} }
if update.DeletedBusinessMessages == nil { for _, tt := range tests {
t.Fatal("expected DeletedBusinessMessages to be populated") t.Run(tt.name, func(t *testing.T) {
} var update Update
if update.DeletedBusinessMessage == nil { if err := json.Unmarshal([]byte(tt.body), &update); err != nil {
t.Fatal("expected deprecated DeletedBusinessMessage alias to be populated") t.Fatalf("Unmarshal returned error: %v", err)
} }
if update.DeletedBusinessMessages != update.DeletedBusinessMessage { if update.Type != tt.want {
t.Fatal("expected deleted business message fields to share the same payload") t.Fatalf("unexpected update type: got %q want %q", update.Type, tt.want)
} }
if got := update.DeletedBusinessMessages.MessageIDs; len(got) != 2 || got[0] != 3 || got[1] != 5 { if tt.want == UpdateTypeChatBoost && update.ChatBoost.Boost.BoostID != "boost-1" {
t.Fatalf("unexpected message ids: %v", got) t.Fatalf("unexpected boost id: got %q want %q", update.ChatBoost.Boost.BoostID, "boost-1")
}
if tt.want == UpdateTypeManagedBot && update.ManagedBot.Bot.ID != 12 {
t.Fatalf("unexpected managed bot id: got %d want %d", update.ManagedBot.Bot.ID, 12)
}
})
} }
} }
func TestUpdateMarshalUsesCanonicalDeletedBusinessMessagesField(t *testing.T) { func TestPollUnmarshalSupportsBotAPI96Fields(t *testing.T) {
var poll Poll
body := `{
"id": "poll-1",
"question": "Pick winners",
"question_entities": [],
"options": [],
"total_voter_count": 2,
"is_closed": false,
"is_anonymous": false,
"type": "quiz",
"allows_multiple_answers": true,
"allows_revoting": true,
"correct_option_ids": [1, 3]
}`
if err := json.Unmarshal([]byte(body), &poll); err != nil {
t.Fatalf("Unmarshal returned error: %v", err)
}
if !poll.AllowsRevoting {
t.Fatal("expected allows_revoting to be decoded")
}
if len(poll.CorrectOptionIDs) != 2 || poll.CorrectOptionIDs[0] != 1 || poll.CorrectOptionIDs[1] != 3 {
t.Fatalf("unexpected correct option ids: %#v", poll.CorrectOptionIDs)
}
}
func TestUpdateMarshalOmitsSyntheticTypeField(t *testing.T) {
update := Update{ update := Update{
UpdateID: 1, UpdateID: 1,
DeletedBusinessMessage: &BusinessMessagesDeleted{ Type: UpdateTypeCallbackQuery,
BusinessConnectionID: "conn", CallbackQuery: &CallbackQuery{
Chat: Chat{ID: 42, Type: string(ChatTypePrivate)}, ID: "cb",
MessageIDs: []int{7}, From: User{ID: 1, FirstName: "Test"},
ChatInstance: "instance",
Data: "payload",
}, },
} }
@@ -50,11 +139,8 @@ func TestUpdateMarshalUsesCanonicalDeletedBusinessMessagesField(t *testing.T) {
} }
got := string(data) got := string(data)
if !strings.Contains(got, `"deleted_business_messages"`) { if strings.Contains(got, `"type"`) {
t.Fatalf("expected canonical deleted_business_messages field, got %s", got) t.Fatalf("unexpected synthetic type field, got %s", got)
}
if strings.Contains(got, `"deleted_business_message"`) {
t.Fatalf("unexpected singular deleted_business_message field, got %s", got)
} }
} }
@@ -66,4 +152,66 @@ func TestUpdateShippingQueryIsNilWhenAbsent(t *testing.T) {
if update.ShippingQuery != nil { if update.ShippingQuery != nil {
t.Fatalf("expected ShippingQuery to be nil, got %+v", update.ShippingQuery) t.Fatalf("expected ShippingQuery to be nil, got %+v", update.ShippingQuery)
} }
if update.Type != UpdateTypeUnknown {
t.Fatalf("expected UpdateTypeUnknown, got %q", update.Type)
}
}
func TestMaybeInaccessibleMessageUnmarshalAccessibleMessage(t *testing.T) {
var wrapper MaybeInaccessibleMessage
body := `{
"message_id": 10,
"date": 1700000000,
"chat": {"id": 42, "type": "private"},
"text": "hello"
}`
if err := json.Unmarshal([]byte(body), &wrapper); err != nil {
t.Fatalf("Unmarshal returned error: %v", err)
}
if !wrapper.IsAccessible() {
t.Fatal("expected accessible message payload")
}
if wrapper.IsInaccessible() {
t.Fatal("expected inaccessible payload to be empty")
}
if wrapper.Message() == nil || wrapper.Message().Text != "hello" {
t.Fatalf("unexpected accessible payload: %#v", wrapper.Message())
}
if wrapper.MessageID() != 10 {
t.Fatalf("unexpected message id: got %d want %d", wrapper.MessageID(), 10)
}
if wrapper.Chat() == nil || wrapper.Chat().ID != 42 {
t.Fatalf("unexpected chat payload: %#v", wrapper.Chat())
}
}
func TestMaybeInaccessibleMessageUnmarshalInaccessibleMessage(t *testing.T) {
var wrapper MaybeInaccessibleMessage
body := `{
"message_id": 7,
"date": 0,
"chat": {"id": -1001, "type": "supergroup"}
}`
if err := json.Unmarshal([]byte(body), &wrapper); err != nil {
t.Fatalf("Unmarshal returned error: %v", err)
}
if wrapper.IsAccessible() {
t.Fatal("expected accessible payload to be empty")
}
if !wrapper.IsInaccessible() {
t.Fatal("expected inaccessible message payload")
}
if wrapper.InaccessibleMessage() == nil || wrapper.InaccessibleMessage().MessageID != 7 {
t.Fatalf("unexpected inaccessible payload: %#v", wrapper.InaccessibleMessage())
}
if wrapper.MessageID() != 7 {
t.Fatalf("unexpected message id: got %d want %d", wrapper.MessageID(), 7)
}
if wrapper.Chat() == nil || wrapper.Chat().ID != -1001 {
t.Fatalf("unexpected chat payload: %#v", wrapper.Chat())
}
} }
+20 -11
View File
@@ -7,10 +7,11 @@ import (
"mime/multipart" "mime/multipart"
"net/http" "net/http"
"path/filepath" "path/filepath"
"strings"
"time" "time"
"git.nix13.pw/scuroneko/laniakea/utils" "git.scuroneko.dev/scuroneko/laniakea/utils"
"git.nix13.pw/scuroneko/slog" "git.scuroneko.dev/scuroneko/slog"
) )
const ( const (
@@ -70,6 +71,11 @@ type Uploader struct {
// NewUploader creates a multipart uploader bound to an API client. // NewUploader creates a multipart uploader bound to an API client.
func NewUploader(api *API) *Uploader { func NewUploader(api *API) *Uploader {
logger := utils.CreateLogger("UPLOADER", utils.GetLoggerLevel()) logger := utils.CreateLogger("UPLOADER", utils.GetLoggerLevel())
if api == nil {
logger.Errorln("api is nil")
_ = logger.Close()
return nil
}
return &Uploader{api, logger} return &Uploader{api, logger}
} }
@@ -81,8 +87,12 @@ func (u *Uploader) Close() error { return u.logger.Close() }
// See https://core.telegram.org/bots/api // See https://core.telegram.org/bots/api
func (u *Uploader) GetLogger() *slog.Logger { return u.logger } func (u *Uploader) GetLogger() *slog.Logger { return u.logger }
// UploaderRequest is a multipart file upload request to the Telegram API. // UploaderRequest is a low-level multipart upload request wrapper.
// Use NewUploaderRequest or NewUploaderRequestWithChatID to construct one. //
// Prefer method-specific helpers such as SendPhoto or SetWebhook. UploaderRequest
// is intended for advanced use cases where callers manage the method name, files,
// and request/response types themselves. In that sense it is an unsafe escape
// hatch compared with the typed uploader API.
type UploaderRequest[R, P any] struct { type UploaderRequest[R, P any] struct {
method string method string
files []UploaderFile files []UploaderFile
@@ -90,16 +100,17 @@ type UploaderRequest[R, P any] struct {
chatId int64 chatId int64
} }
// NewUploaderRequest creates a new multipart upload request with no associated chat ID. // NewUploaderRequest creates a low-level multipart upload request with no associated chat ID.
func NewUploaderRequest[R, P any](method string, params P, files ...UploaderFile) UploaderRequest[R, P] { func NewUploaderRequest[R, P any](method string, params P, files ...UploaderFile) UploaderRequest[R, P] {
return UploaderRequest[R, P]{method: method, files: files, params: params, chatId: 0} return UploaderRequest[R, P]{method: method, files: files, params: params, chatId: 0}
} }
// NewUploaderRequestWithChatID creates a new multipart upload request with an associated chat ID. // NewUploaderRequestWithChatID creates a low-level multipart upload request with an associated chat ID.
// The chat ID is used for per-chat rate limiting. // The chat ID is used for per-chat rate limiting.
func NewUploaderRequestWithChatID[R, P any](method string, params P, chatId int64, files ...UploaderFile) UploaderRequest[R, P] { func NewUploaderRequestWithChatID[R, P any](method string, params P, chatId int64, files ...UploaderFile) UploaderRequest[R, P] {
return UploaderRequest[R, P]{method: method, files: files, params: params, chatId: chatId} return UploaderRequest[R, P]{method: method, files: files, params: params, chatId: chatId}
} }
func (r UploaderRequest[R, P]) doRequest(ctx context.Context, up *Uploader) (R, error) { func (r UploaderRequest[R, P]) doRequest(ctx context.Context, up *Uploader) (R, error) {
var zero R var zero R
@@ -204,8 +215,7 @@ func (r UploaderRequest[R, P]) Do(up *Uploader) (R, error) {
return r.DoWithContext(context.Background(), up) return r.DoWithContext(context.Background(), up)
} }
// prepareMultipart builds a multipart form body from the given files and params. // Internal helper that builds a finalized multipart body from files and params.
// Params are encoded via utils.Encode. The writer boundary is finalized before returning.
func prepareMultipart[P any](files []UploaderFile, params P) (*bytes.Buffer, string, error) { func prepareMultipart[P any](files []UploaderFile, params P) (*bytes.Buffer, string, error) {
buf := bytes.NewBuffer(nil) buf := bytes.NewBuffer(nil)
w := multipart.NewWriter(buf) w := multipart.NewWriter(buf)
@@ -238,10 +248,9 @@ func prepareMultipart[P any](files []UploaderFile, params P) (*bytes.Buffer, str
return buf, w.FormDataContentType(), nil return buf, w.FormDataContentType(), nil
} }
// uploaderTypeByExt infers the Telegram upload field name from a file extension. // Internal helper that infers an upload field name from a file extension.
// Falls back to UploaderDocumentType for unrecognized extensions.
func uploaderTypeByExt(filename string) UploaderFileType { func uploaderTypeByExt(filename string) UploaderFileType {
ext := filepath.Ext(filename) ext := strings.ToLower(filepath.Ext(filename))
switch ext { switch ext {
case ".jpg", ".jpeg", ".png", ".webp", ".bmp": case ".jpg", ".jpeg", ".png", ".webp", ".bmp":
return UploaderPhotoType return UploaderPhotoType
+22 -1
View File
@@ -57,7 +57,7 @@ func TestUploaderEncodesJSONFieldsAndLeavesAcceptEncodingToHTTPTransport(t *test
}() }()
msg, err := uploader.SendPhoto( msg, err := uploader.SendPhoto(
UploadPhotoP{ UploadPhoto{
ChatID: 42, ChatID: 42,
CaptionEntities: []MessageEntity{{ CaptionEntities: []MessageEntity{{
Type: MessageEntityBold, Type: MessageEntityBold,
@@ -104,6 +104,27 @@ func TestUploaderEncodesJSONFieldsAndLeavesAcceptEncodingToHTTPTransport(t *test
} }
} }
func TestNewUploaderFileDetectsFileTypeCaseInsensitively(t *testing.T) {
tests := []struct {
name string
filename string
want UploaderFileType
}{
{name: "uppercase photo", filename: "PHOTO.JPG", want: UploaderPhotoType},
{name: "uppercase voice", filename: "voice.OGG", want: UploaderVoiceType},
{name: "unknown defaults to document", filename: "archive.BIN", want: UploaderDocumentType},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
file := NewUploaderFile(tt.filename, []byte("x"))
if file.field != tt.want {
t.Fatalf("unexpected uploader field: got %q want %q", file.field, tt.want)
}
})
}
}
func readMultipartRequest(req *http.Request) (map[string]string, string, []byte, error) { func readMultipartRequest(req *http.Request) (map[string]string, string, []byte, error) {
_, params, err := mime.ParseMediaType(req.Header.Get("Content-Type")) _, params, err := mime.ParseMediaType(req.Header.Get("Content-Type"))
if err != nil { if err != nil {
+37 -37
View File
@@ -2,9 +2,9 @@ package tgapi
import "context" import "context"
// UploadPhotoP holds parameters for uploading a photo using the Uploader. // UploadPhoto holds parameters for uploading a photo using the Uploader.
// See https://core.telegram.org/bots/api#sendphoto // See https://core.telegram.org/bots/api#sendphoto
type UploadPhotoP struct { type UploadPhoto struct {
BusinessConnectionID string `json:"business_connection_id,omitempty"` BusinessConnectionID string `json:"business_connection_id,omitempty"`
ChatID int64 `json:"chat_id"` ChatID int64 `json:"chat_id"`
MessageThreadID int `json:"message_thread_id,omitempty"` MessageThreadID int `json:"message_thread_id,omitempty"`
@@ -29,7 +29,7 @@ type UploadPhotoP struct {
// SendPhoto uploads a photo via multipart and sends it as a message. // SendPhoto uploads a photo via multipart and sends it as a message.
// file is the photo file to upload. // file is the photo file to upload.
// See https://core.telegram.org/bots/api#sendphoto // See https://core.telegram.org/bots/api#sendphoto
func (u *Uploader) SendPhoto(params UploadPhotoP, file UploaderFile) (Message, error) { func (u *Uploader) SendPhoto(params UploadPhoto, file UploaderFile) (Message, error) {
req := NewUploaderRequestWithChatID[Message]("sendPhoto", params, params.ChatID, file) req := NewUploaderRequestWithChatID[Message]("sendPhoto", params, params.ChatID, file)
return req.Do(u) return req.Do(u)
} }
@@ -39,14 +39,14 @@ func (u *Uploader) SendPhoto(params UploadPhotoP, file UploaderFile) (Message, e
// SendPhotoWithContext is the context-aware variant of SendPhoto. // SendPhotoWithContext is the context-aware variant of SendPhoto.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#sendphoto // See https://core.telegram.org/bots/api#sendphoto
func (u *Uploader) SendPhotoWithContext(ctx context.Context, params UploadPhotoP, file UploaderFile) (Message, error) { func (u *Uploader) SendPhotoWithContext(ctx context.Context, params UploadPhoto, file UploaderFile) (Message, error) {
req := NewUploaderRequestWithChatID[Message]("sendPhoto", params, params.ChatID, file) req := NewUploaderRequestWithChatID[Message]("sendPhoto", params, params.ChatID, file)
return req.DoWithContext(ctx, u) return req.DoWithContext(ctx, u)
} }
// UploadAudioP holds parameters for uploading an audio file using the Uploader. // UploadAudio holds parameters for uploading an audio file using the Uploader.
// See https://core.telegram.org/bots/api#sendaudio // See https://core.telegram.org/bots/api#sendaudio
type UploadAudioP struct { type UploadAudio struct {
BusinessConnectionID string `json:"business_connection_id,omitempty"` BusinessConnectionID string `json:"business_connection_id,omitempty"`
ChatID int64 `json:"chat_id"` ChatID int64 `json:"chat_id"`
MessageThreadID int `json:"message_thread_id,omitempty"` MessageThreadID int `json:"message_thread_id,omitempty"`
@@ -73,7 +73,7 @@ type UploadAudioP struct {
// SendAudio uploads an audio file via multipart and sends it as a message. // SendAudio uploads an audio file via multipart and sends it as a message.
// files are the audio file(s) to upload (typically one file). // files are the audio file(s) to upload (typically one file).
// See https://core.telegram.org/bots/api#sendaudio // See https://core.telegram.org/bots/api#sendaudio
func (u *Uploader) SendAudio(params UploadAudioP, files ...UploaderFile) (Message, error) { func (u *Uploader) SendAudio(params UploadAudio, files ...UploaderFile) (Message, error) {
req := NewUploaderRequestWithChatID[Message]("sendAudio", params, params.ChatID, files...) req := NewUploaderRequestWithChatID[Message]("sendAudio", params, params.ChatID, files...)
return req.Do(u) return req.Do(u)
} }
@@ -83,14 +83,14 @@ func (u *Uploader) SendAudio(params UploadAudioP, files ...UploaderFile) (Messag
// SendAudioWithContext is the context-aware variant of SendAudio. // SendAudioWithContext is the context-aware variant of SendAudio.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#sendaudio // See https://core.telegram.org/bots/api#sendaudio
func (u *Uploader) SendAudioWithContext(ctx context.Context, params UploadAudioP, files ...UploaderFile) (Message, error) { func (u *Uploader) SendAudioWithContext(ctx context.Context, params UploadAudio, files ...UploaderFile) (Message, error) {
req := NewUploaderRequestWithChatID[Message]("sendAudio", params, params.ChatID, files...) req := NewUploaderRequestWithChatID[Message]("sendAudio", params, params.ChatID, files...)
return req.DoWithContext(ctx, u) return req.DoWithContext(ctx, u)
} }
// UploadDocumentP holds parameters for uploading a document using the Uploader. // UploadDocument holds parameters for uploading a document using the Uploader.
// See https://core.telegram.org/bots/api#senddocument // See https://core.telegram.org/bots/api#senddocument
type UploadDocumentP struct { type UploadDocument struct {
BusinessConnectionID string `json:"business_connection_id,omitempty"` BusinessConnectionID string `json:"business_connection_id,omitempty"`
ChatID int64 `json:"chat_id"` ChatID int64 `json:"chat_id"`
MessageThreadID int `json:"message_thread_id,omitempty"` MessageThreadID int `json:"message_thread_id,omitempty"`
@@ -114,7 +114,7 @@ type UploadDocumentP struct {
// SendDocument uploads a document via multipart and sends it as a message. // SendDocument uploads a document via multipart and sends it as a message.
// files are the document file(s) to upload (typically one file). // files are the document file(s) to upload (typically one file).
// See https://core.telegram.org/bots/api#senddocument // See https://core.telegram.org/bots/api#senddocument
func (u *Uploader) SendDocument(params UploadDocumentP, files ...UploaderFile) (Message, error) { func (u *Uploader) SendDocument(params UploadDocument, files ...UploaderFile) (Message, error) {
req := NewUploaderRequestWithChatID[Message]("sendDocument", params, params.ChatID, files...) req := NewUploaderRequestWithChatID[Message]("sendDocument", params, params.ChatID, files...)
return req.Do(u) return req.Do(u)
} }
@@ -124,14 +124,14 @@ func (u *Uploader) SendDocument(params UploadDocumentP, files ...UploaderFile) (
// SendDocumentWithContext is the context-aware variant of SendDocument. // SendDocumentWithContext is the context-aware variant of SendDocument.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#senddocument // See https://core.telegram.org/bots/api#senddocument
func (u *Uploader) SendDocumentWithContext(ctx context.Context, params UploadDocumentP, files ...UploaderFile) (Message, error) { func (u *Uploader) SendDocumentWithContext(ctx context.Context, params UploadDocument, files ...UploaderFile) (Message, error) {
req := NewUploaderRequestWithChatID[Message]("sendDocument", params, params.ChatID, files...) req := NewUploaderRequestWithChatID[Message]("sendDocument", params, params.ChatID, files...)
return req.DoWithContext(ctx, u) return req.DoWithContext(ctx, u)
} }
// UploadVideoP holds parameters for uploading a video using the Uploader. // UploadVideo holds parameters for uploading a video using the Uploader.
// See https://core.telegram.org/bots/api#sendvideo // See https://core.telegram.org/bots/api#sendvideo
type UploadVideoP struct { type UploadVideo struct {
BusinessConnectionID string `json:"business_connection_id,omitempty"` BusinessConnectionID string `json:"business_connection_id,omitempty"`
ChatID int64 `json:"chat_id"` ChatID int64 `json:"chat_id"`
MessageThreadID int `json:"message_thread_id,omitempty"` MessageThreadID int `json:"message_thread_id,omitempty"`
@@ -162,7 +162,7 @@ type UploadVideoP struct {
// SendVideo uploads a video via multipart and sends it as a message. // SendVideo uploads a video via multipart and sends it as a message.
// files are the video file(s) to upload (typically one file). // files are the video file(s) to upload (typically one file).
// See https://core.telegram.org/bots/api#sendvideo // See https://core.telegram.org/bots/api#sendvideo
func (u *Uploader) SendVideo(params UploadVideoP, files ...UploaderFile) (Message, error) { func (u *Uploader) SendVideo(params UploadVideo, files ...UploaderFile) (Message, error) {
req := NewUploaderRequestWithChatID[Message]("sendVideo", params, params.ChatID, files...) req := NewUploaderRequestWithChatID[Message]("sendVideo", params, params.ChatID, files...)
return req.Do(u) return req.Do(u)
} }
@@ -172,14 +172,14 @@ func (u *Uploader) SendVideo(params UploadVideoP, files ...UploaderFile) (Messag
// SendVideoWithContext is the context-aware variant of SendVideo. // SendVideoWithContext is the context-aware variant of SendVideo.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#sendvideo // See https://core.telegram.org/bots/api#sendvideo
func (u *Uploader) SendVideoWithContext(ctx context.Context, params UploadVideoP, files ...UploaderFile) (Message, error) { func (u *Uploader) SendVideoWithContext(ctx context.Context, params UploadVideo, files ...UploaderFile) (Message, error) {
req := NewUploaderRequestWithChatID[Message]("sendVideo", params, params.ChatID, files...) req := NewUploaderRequestWithChatID[Message]("sendVideo", params, params.ChatID, files...)
return req.DoWithContext(ctx, u) return req.DoWithContext(ctx, u)
} }
// UploadAnimationP holds parameters for uploading an animation using the Uploader. // UploadAnimation holds parameters for uploading an animation using the Uploader.
// See https://core.telegram.org/bots/api#sendanimation // See https://core.telegram.org/bots/api#sendanimation
type UploadAnimationP struct { type UploadAnimation struct {
BusinessConnectionID string `json:"business_connection_id,omitempty"` BusinessConnectionID string `json:"business_connection_id,omitempty"`
ChatID int64 `json:"chat_id"` ChatID int64 `json:"chat_id"`
MessageThreadID int `json:"message_thread_id,omitempty"` MessageThreadID int `json:"message_thread_id,omitempty"`
@@ -208,7 +208,7 @@ type UploadAnimationP struct {
// SendAnimation uploads an animation via multipart and sends it as a message. // SendAnimation uploads an animation via multipart and sends it as a message.
// files are the animation file(s) to upload (typically one file). // files are the animation file(s) to upload (typically one file).
// See https://core.telegram.org/bots/api#sendanimation // See https://core.telegram.org/bots/api#sendanimation
func (u *Uploader) SendAnimation(params UploadAnimationP, files ...UploaderFile) (Message, error) { func (u *Uploader) SendAnimation(params UploadAnimation, files ...UploaderFile) (Message, error) {
req := NewUploaderRequestWithChatID[Message]("sendAnimation", params, params.ChatID, files...) req := NewUploaderRequestWithChatID[Message]("sendAnimation", params, params.ChatID, files...)
return req.Do(u) return req.Do(u)
} }
@@ -218,14 +218,14 @@ func (u *Uploader) SendAnimation(params UploadAnimationP, files ...UploaderFile)
// SendAnimationWithContext is the context-aware variant of SendAnimation. // SendAnimationWithContext is the context-aware variant of SendAnimation.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#sendanimation // See https://core.telegram.org/bots/api#sendanimation
func (u *Uploader) SendAnimationWithContext(ctx context.Context, params UploadAnimationP, files ...UploaderFile) (Message, error) { func (u *Uploader) SendAnimationWithContext(ctx context.Context, params UploadAnimation, files ...UploaderFile) (Message, error) {
req := NewUploaderRequestWithChatID[Message]("sendAnimation", params, params.ChatID, files...) req := NewUploaderRequestWithChatID[Message]("sendAnimation", params, params.ChatID, files...)
return req.DoWithContext(ctx, u) return req.DoWithContext(ctx, u)
} }
// UploadVoiceP holds parameters for uploading a voice note using the Uploader. // UploadVoice holds parameters for uploading a voice note using the Uploader.
// See https://core.telegram.org/bots/api#sendvoice // See https://core.telegram.org/bots/api#sendvoice
type UploadVoiceP struct { type UploadVoice struct {
BusinessConnectionID string `json:"business_connection_id,omitempty"` BusinessConnectionID string `json:"business_connection_id,omitempty"`
ChatID int64 `json:"chat_id"` ChatID int64 `json:"chat_id"`
MessageThreadID int `json:"message_thread_id,omitempty"` MessageThreadID int `json:"message_thread_id,omitempty"`
@@ -249,7 +249,7 @@ type UploadVoiceP struct {
// SendVoice uploads a voice note via multipart and sends it as a message. // SendVoice uploads a voice note via multipart and sends it as a message.
// files are the voice file(s) to upload (typically one file). // files are the voice file(s) to upload (typically one file).
// See https://core.telegram.org/bots/api#sendvoice // See https://core.telegram.org/bots/api#sendvoice
func (u *Uploader) SendVoice(params UploadVoiceP, files ...UploaderFile) (Message, error) { func (u *Uploader) SendVoice(params UploadVoice, files ...UploaderFile) (Message, error) {
req := NewUploaderRequestWithChatID[Message]("sendVoice", params, params.ChatID, files...) req := NewUploaderRequestWithChatID[Message]("sendVoice", params, params.ChatID, files...)
return req.Do(u) return req.Do(u)
} }
@@ -259,14 +259,14 @@ func (u *Uploader) SendVoice(params UploadVoiceP, files ...UploaderFile) (Messag
// SendVoiceWithContext is the context-aware variant of SendVoice. // SendVoiceWithContext is the context-aware variant of SendVoice.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#sendvoice // See https://core.telegram.org/bots/api#sendvoice
func (u *Uploader) SendVoiceWithContext(ctx context.Context, params UploadVoiceP, files ...UploaderFile) (Message, error) { func (u *Uploader) SendVoiceWithContext(ctx context.Context, params UploadVoice, files ...UploaderFile) (Message, error) {
req := NewUploaderRequestWithChatID[Message]("sendVoice", params, params.ChatID, files...) req := NewUploaderRequestWithChatID[Message]("sendVoice", params, params.ChatID, files...)
return req.DoWithContext(ctx, u) return req.DoWithContext(ctx, u)
} }
// UploadVideoNoteP holds parameters for uploading a video note (rounded video) using the Uploader. // UploadVideoNote holds parameters for uploading a video note (rounded video) using the Uploader.
// See https://core.telegram.org/bots/api#sendvideonote // See https://core.telegram.org/bots/api#sendvideonote
type UploadVideoNoteP struct { type UploadVideoNote struct {
BusinessConnectionID string `json:"business_connection_id,omitempty"` BusinessConnectionID string `json:"business_connection_id,omitempty"`
ChatID int64 `json:"chat_id"` ChatID int64 `json:"chat_id"`
MessageThreadID int `json:"message_thread_id,omitempty"` MessageThreadID int `json:"message_thread_id,omitempty"`
@@ -288,7 +288,7 @@ type UploadVideoNoteP struct {
// SendVideoNote uploads a video note via multipart and sends it as a message. // SendVideoNote uploads a video note via multipart and sends it as a message.
// files are the video note file(s) to upload (typically one file). // files are the video note file(s) to upload (typically one file).
// See https://core.telegram.org/bots/api#sendvideonote // See https://core.telegram.org/bots/api#sendvideonote
func (u *Uploader) SendVideoNote(params UploadVideoNoteP, files ...UploaderFile) (Message, error) { func (u *Uploader) SendVideoNote(params UploadVideoNote, files ...UploaderFile) (Message, error) {
req := NewUploaderRequestWithChatID[Message]("sendVideoNote", params, params.ChatID, files...) req := NewUploaderRequestWithChatID[Message]("sendVideoNote", params, params.ChatID, files...)
return req.Do(u) return req.Do(u)
} }
@@ -298,21 +298,21 @@ func (u *Uploader) SendVideoNote(params UploadVideoNoteP, files ...UploaderFile)
// SendVideoNoteWithContext is the context-aware variant of SendVideoNote. // SendVideoNoteWithContext is the context-aware variant of SendVideoNote.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#sendvideonote // See https://core.telegram.org/bots/api#sendvideonote
func (u *Uploader) SendVideoNoteWithContext(ctx context.Context, params UploadVideoNoteP, files ...UploaderFile) (Message, error) { func (u *Uploader) SendVideoNoteWithContext(ctx context.Context, params UploadVideoNote, files ...UploaderFile) (Message, error) {
req := NewUploaderRequestWithChatID[Message]("sendVideoNote", params, params.ChatID, files...) req := NewUploaderRequestWithChatID[Message]("sendVideoNote", params, params.ChatID, files...)
return req.DoWithContext(ctx, u) return req.DoWithContext(ctx, u)
} }
// UploadChatPhotoP holds parameters for uploading a chat photo using the Uploader. // UploadChatPhoto holds parameters for uploading a chat photo using the Uploader.
// See https://core.telegram.org/bots/api#setchatphoto // See https://core.telegram.org/bots/api#setchatphoto
type UploadChatPhotoP struct { type UploadChatPhoto struct {
ChatID int64 `json:"chat_id"` ChatID int64 `json:"chat_id"`
} }
// SetChatPhoto uploads a new chat photo. // SetChatPhoto uploads a new chat photo.
// photo is the photo file to upload. // photo is the photo file to upload.
// See https://core.telegram.org/bots/api#setchatphoto // See https://core.telegram.org/bots/api#setchatphoto
func (u *Uploader) SetChatPhoto(params UploadChatPhotoP, photo UploaderFile) (bool, error) { func (u *Uploader) SetChatPhoto(params UploadChatPhoto, photo UploaderFile) (bool, error) {
req := NewUploaderRequestWithChatID[bool]("setChatPhoto", params, params.ChatID, photo) req := NewUploaderRequestWithChatID[bool]("setChatPhoto", params, params.ChatID, photo)
return req.Do(u) return req.Do(u)
} }
@@ -322,18 +322,18 @@ func (u *Uploader) SetChatPhoto(params UploadChatPhotoP, photo UploaderFile) (bo
// SetChatPhotoWithContext is the context-aware variant of SetChatPhoto. // SetChatPhotoWithContext is the context-aware variant of SetChatPhoto.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#setchatphoto // See https://core.telegram.org/bots/api#setchatphoto
func (u *Uploader) SetChatPhotoWithContext(ctx context.Context, params UploadChatPhotoP, photo UploaderFile) (bool, error) { func (u *Uploader) SetChatPhotoWithContext(ctx context.Context, params UploadChatPhoto, photo UploaderFile) (bool, error) {
req := NewUploaderRequestWithChatID[bool]("setChatPhoto", params, params.ChatID, photo) req := NewUploaderRequestWithChatID[bool]("setChatPhoto", params, params.ChatID, photo)
return req.DoWithContext(ctx, u) return req.DoWithContext(ctx, u)
} }
// UploadSetWebhookP holds multipart parameters for the setWebhook method. // UploadSetWebhook holds multipart parameters for the setWebhook method.
// Use this type when uploading a self-signed certificate file. // Use this type when uploading a self-signed certificate file.
// See https://core.telegram.org/bots/api#setwebhook // See https://core.telegram.org/bots/api#setwebhook
type UploadSetWebhookP struct { type UploadSetWebhook struct {
URL string `json:"url"` URL string `json:"url"`
IPAddress string `json:"ip_address,omitempty"` IPAddress string `json:"ip_address,omitempty"`
MaxConnections int `json:"max_connections,omitempty"` MaxConnections int8 `json:"max_connections,omitempty"`
AllowedUpdates []UpdateType `json:"allowed_updates,omitempty"` AllowedUpdates []UpdateType `json:"allowed_updates,omitempty"`
DropPendingUpdates bool `json:"drop_pending_updates,omitempty"` DropPendingUpdates bool `json:"drop_pending_updates,omitempty"`
SecretToken string `json:"secret_token,omitempty"` SecretToken string `json:"secret_token,omitempty"`
@@ -342,7 +342,7 @@ type UploadSetWebhookP struct {
// SetWebhook uploads a certificate and sets a webhook URL. // SetWebhook uploads a certificate and sets a webhook URL.
// certificate maps to the multipart field \"certificate\". // certificate maps to the multipart field \"certificate\".
// See https://core.telegram.org/bots/api#setwebhook // See https://core.telegram.org/bots/api#setwebhook
func (u *Uploader) SetWebhook(params UploadSetWebhookP, certificate UploaderFile) (bool, error) { func (u *Uploader) SetWebhook(params UploadSetWebhook, certificate UploaderFile) (bool, error) {
req := NewUploaderRequest[bool]("setWebhook", params, certificate.SetType(UploaderCertificateType)) req := NewUploaderRequest[bool]("setWebhook", params, certificate.SetType(UploaderCertificateType))
return req.Do(u) return req.Do(u)
} }
@@ -350,7 +350,7 @@ func (u *Uploader) SetWebhook(params UploadSetWebhookP, certificate UploaderFile
// SetWebhookWithContext is the context-aware variant of SetWebhook. // SetWebhookWithContext is the context-aware variant of SetWebhook.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#setwebhook // See https://core.telegram.org/bots/api#setwebhook
func (u *Uploader) SetWebhookWithContext(ctx context.Context, params UploadSetWebhookP, certificate UploaderFile) (bool, error) { func (u *Uploader) SetWebhookWithContext(ctx context.Context, params UploadSetWebhook, certificate UploaderFile) (bool, error) {
req := NewUploaderRequest[bool]("setWebhook", params, certificate.SetType(UploaderCertificateType)) req := NewUploaderRequest[bool]("setWebhook", params, certificate.SetType(UploaderCertificateType))
return req.DoWithContext(ctx, u) return req.DoWithContext(ctx, u)
} }
+16 -16
View File
@@ -2,9 +2,9 @@ package tgapi
import "context" import "context"
// GetUserProfilePhotosP holds parameters for the GetUserProfilePhotos method. // GetUserProfilePhotos holds parameters for the GetUserProfilePhotos method.
// See https://core.telegram.org/bots/api#getuserprofilephotos // See https://core.telegram.org/bots/api#getuserprofilephotos
type GetUserProfilePhotosP struct { type GetUserProfilePhotos struct {
UserID int64 `json:"user_id"` UserID int64 `json:"user_id"`
Offset int `json:"offset,omitempty"` Offset int `json:"offset,omitempty"`
Limit int `json:"limit,omitempty"` Limit int `json:"limit,omitempty"`
@@ -12,7 +12,7 @@ type GetUserProfilePhotosP struct {
// GetUserProfilePhotos returns a list of profile pictures for a user. // GetUserProfilePhotos returns a list of profile pictures for a user.
// See https://core.telegram.org/bots/api#getuserprofilephotos // See https://core.telegram.org/bots/api#getuserprofilephotos
func (api *API) GetUserProfilePhotos(params GetUserProfilePhotosP) (UserProfilePhotos, error) { func (api *API) GetUserProfilePhotos(params GetUserProfilePhotos) (UserProfilePhotos, error) {
req := NewRequest[UserProfilePhotos]("getUserProfilePhotos", params) req := NewRequest[UserProfilePhotos]("getUserProfilePhotos", params)
return req.Do(api) return req.Do(api)
} }
@@ -20,14 +20,14 @@ func (api *API) GetUserProfilePhotos(params GetUserProfilePhotosP) (UserProfileP
// GetUserProfilePhotosWithContext is the context-aware variant of GetUserProfilePhotos. // GetUserProfilePhotosWithContext is the context-aware variant of GetUserProfilePhotos.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#getuserprofilephotos // See https://core.telegram.org/bots/api#getuserprofilephotos
func (api *API) GetUserProfilePhotosWithContext(ctx context.Context, params GetUserProfilePhotosP) (UserProfilePhotos, error) { func (api *API) GetUserProfilePhotosWithContext(ctx context.Context, params GetUserProfilePhotos) (UserProfilePhotos, error) {
req := NewRequest[UserProfilePhotos]("getUserProfilePhotos", params) req := NewRequest[UserProfilePhotos]("getUserProfilePhotos", params)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// GetUserProfileAudiosP holds parameters for the GetUserProfileAudios method. // GetUserProfileAudios holds parameters for the GetUserProfileAudios method.
// See https://core.telegram.org/bots/api#getuserprofileaudios // See https://core.telegram.org/bots/api#getuserprofileaudios
type GetUserProfileAudiosP struct { type GetUserProfileAudios struct {
UserID int64 `json:"user_id"` UserID int64 `json:"user_id"`
Offset int `json:"offset,omitempty"` Offset int `json:"offset,omitempty"`
Limit int `json:"limit,omitempty"` Limit int `json:"limit,omitempty"`
@@ -35,7 +35,7 @@ type GetUserProfileAudiosP struct {
// GetUserProfileAudios returns a list of profile audios for a user. // GetUserProfileAudios returns a list of profile audios for a user.
// See https://core.telegram.org/bots/api#getuserprofileaudios // See https://core.telegram.org/bots/api#getuserprofileaudios
func (api *API) GetUserProfileAudios(params GetUserProfileAudiosP) (UserProfileAudios, error) { func (api *API) GetUserProfileAudios(params GetUserProfileAudios) (UserProfileAudios, error) {
req := NewRequest[UserProfileAudios]("getUserProfileAudios", params) req := NewRequest[UserProfileAudios]("getUserProfileAudios", params)
return req.Do(api) return req.Do(api)
} }
@@ -43,14 +43,14 @@ func (api *API) GetUserProfileAudios(params GetUserProfileAudiosP) (UserProfileA
// GetUserProfileAudiosWithContext is the context-aware variant of GetUserProfileAudios. // GetUserProfileAudiosWithContext is the context-aware variant of GetUserProfileAudios.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#getuserprofileaudios // See https://core.telegram.org/bots/api#getuserprofileaudios
func (api *API) GetUserProfileAudiosWithContext(ctx context.Context, params GetUserProfileAudiosP) (UserProfileAudios, error) { func (api *API) GetUserProfileAudiosWithContext(ctx context.Context, params GetUserProfileAudios) (UserProfileAudios, error) {
req := NewRequest[UserProfileAudios]("getUserProfileAudios", params) req := NewRequest[UserProfileAudios]("getUserProfileAudios", params)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// SetUserEmojiStatusP holds parameters for the SetUserEmojiStatus method. // SetUserEmojiStatus holds parameters for the SetUserEmojiStatus method.
// See https://core.telegram.org/bots/api#setuseremojistatus // See https://core.telegram.org/bots/api#setuseremojistatus
type SetUserEmojiStatusP struct { type SetUserEmojiStatus struct {
UserID int64 `json:"user_id"` UserID int64 `json:"user_id"`
EmojiID string `json:"emoji_status_custom_emoji_id,omitempty"` EmojiID string `json:"emoji_status_custom_emoji_id,omitempty"`
ExpirationDate int `json:"emoji_status_expiration_date,omitempty"` ExpirationDate int `json:"emoji_status_expiration_date,omitempty"`
@@ -59,7 +59,7 @@ type SetUserEmojiStatusP struct {
// SetUserEmojiStatus sets a custom emoji status for a user. // SetUserEmojiStatus sets a custom emoji status for a user.
// Returns true on success. // Returns true on success.
// See https://core.telegram.org/bots/api#setuseremojistatus // See https://core.telegram.org/bots/api#setuseremojistatus
func (api *API) SetUserEmojiStatus(params SetUserEmojiStatusP) (bool, error) { func (api *API) SetUserEmojiStatus(params SetUserEmojiStatus) (bool, error) {
req := NewRequest[bool]("setUserEmojiStatus", params) req := NewRequest[bool]("setUserEmojiStatus", params)
return req.Do(api) return req.Do(api)
} }
@@ -67,14 +67,14 @@ func (api *API) SetUserEmojiStatus(params SetUserEmojiStatusP) (bool, error) {
// SetUserEmojiStatusWithContext is the context-aware variant of SetUserEmojiStatus. // SetUserEmojiStatusWithContext is the context-aware variant of SetUserEmojiStatus.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#setuseremojistatus // See https://core.telegram.org/bots/api#setuseremojistatus
func (api *API) SetUserEmojiStatusWithContext(ctx context.Context, params SetUserEmojiStatusP) (bool, error) { func (api *API) SetUserEmojiStatusWithContext(ctx context.Context, params SetUserEmojiStatus) (bool, error) {
req := NewRequest[bool]("setUserEmojiStatus", params) req := NewRequest[bool]("setUserEmojiStatus", params)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
// GetUserGiftsP holds parameters for the GetUserGifts method. // GetUserGifts holds parameters for the GetUserGifts method.
// See https://core.telegram.org/bots/api#getusergifts // See https://core.telegram.org/bots/api#getusergifts
type GetUserGiftsP struct { type GetUserGifts struct {
UserID int64 `json:"user_id"` UserID int64 `json:"user_id"`
ExcludeUnlimited bool `json:"exclude_unlimited,omitempty"` ExcludeUnlimited bool `json:"exclude_unlimited,omitempty"`
ExcludeLimitedUpgradable bool `json:"exclude_limited_upgradable,omitempty"` ExcludeLimitedUpgradable bool `json:"exclude_limited_upgradable,omitempty"`
@@ -88,7 +88,7 @@ type GetUserGiftsP struct {
// GetUserGifts returns gifts owned by a user. // GetUserGifts returns gifts owned by a user.
// See https://core.telegram.org/bots/api#getusergifts // See https://core.telegram.org/bots/api#getusergifts
func (api *API) GetUserGifts(params GetUserGiftsP) (OwnedGifts, error) { func (api *API) GetUserGifts(params GetUserGifts) (OwnedGifts, error) {
req := NewRequest[OwnedGifts]("getUserGifts", params) req := NewRequest[OwnedGifts]("getUserGifts", params)
return req.Do(api) return req.Do(api)
} }
@@ -96,7 +96,7 @@ func (api *API) GetUserGifts(params GetUserGiftsP) (OwnedGifts, error) {
// GetUserGiftsWithContext is the context-aware variant of GetUserGifts. // GetUserGiftsWithContext is the context-aware variant of GetUserGifts.
// It executes the same request but uses ctx for cancellation and deadlines. // It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#getusergifts // See https://core.telegram.org/bots/api#getusergifts
func (api *API) GetUserGiftsWithContext(ctx context.Context, params GetUserGiftsP) (OwnedGifts, error) { func (api *API) GetUserGiftsWithContext(ctx context.Context, params GetUserGifts) (OwnedGifts, error) {
req := NewRequest[OwnedGifts]("getUserGifts", params) req := NewRequest[OwnedGifts]("getUserGifts", params)
return req.DoWithContext(ctx, api) return req.DoWithContext(ctx, api)
} }
+1
View File
@@ -13,6 +13,7 @@ type User struct {
AddedToAttachmentMenu *bool `json:"added_to_attachment_menu,omitempty"` AddedToAttachmentMenu *bool `json:"added_to_attachment_menu,omitempty"`
CanJoinGroups *bool `json:"can_join_groups,omitempty"` CanJoinGroups *bool `json:"can_join_groups,omitempty"`
CanReadAllGroupMessages *bool `json:"can_read_all_group_messages,omitempty"` CanReadAllGroupMessages *bool `json:"can_read_all_group_messages,omitempty"`
CanManageBots *bool `json:"can_manage_bots,omitempty"`
SupportsInlineQueries *bool `json:"supports_inline_queries,omitempty"` SupportsInlineQueries *bool `json:"supports_inline_queries,omitempty"`
CanConnectToBusiness *bool `json:"can_connect_to_business,omitempty"` CanConnectToBusiness *bool `json:"can_connect_to_business,omitempty"`
HasMainWebApp *bool `json:"has_main_web_app,omitempty"` HasMainWebApp *bool `json:"has_main_web_app,omitempty"`
+223
View File
@@ -0,0 +1,223 @@
package laniakea
import (
"time"
"git.scuroneko.dev/scuroneko/laniakea/tgapi"
)
func (bot *Bot[T]) handleUpdate(u *tgapi.Update, ctx *MsgContext) bool {
handled := false
for _, plugin := range bot.plugins {
handler, ok := plugin.handlers[u.Type]
if !ok {
continue
}
pluginCtx := cloneMsgContext(ctx)
if plugin.logger != nil {
pluginCtx.Logger = plugin.logger
}
if !plugin.executeMiddlewares(pluginCtx, bot.appData) {
continue
}
startTime := time.Now()
bot.safeEmitEvent(pluginCtx.Context(), HandlerStartedEvent{
UpdateID: u.UpdateID,
UpdateType: u.Type,
Plugin: plugin.name,
HandlerKind: HandlerUpdateKind,
HandlerName: string(u.Type),
FromID: pluginCtx.FromID,
ChatID: pluginCtx.ChatID,
})
err := handler(pluginCtx, bot.appData)
endEvent := HandlerFinishedEvent{
UpdateID: u.UpdateID,
UpdateType: u.Type,
Plugin: plugin.name,
HandlerKind: HandlerUpdateKind,
HandlerName: string(u.Type),
FromID: pluginCtx.FromID,
ChatID: pluginCtx.ChatID,
Duration: time.Since(startTime),
}
if err != nil {
endEvent.Err = err
endEvent.UserFacing = IsUserError(err)
}
bot.safeEmitEvent(pluginCtx.Context(), endEvent)
if err != nil {
bot.safeEmitEvent(pluginCtx.Context(), ErrorEvent{
UpdateID: u.UpdateID,
UpdateType: u.Type,
Plugin: plugin.name,
HandlerKind: HandlerUpdateKind,
HandlerName: string(u.Type),
FromID: pluginCtx.FromID,
ChatID: pluginCtx.ChatID,
Err: err,
UserFacing: IsUserError(err),
})
pluginCtx.error(err)
}
handled = true
}
return handled
}
func (bot *Bot[T]) prepareUpdateCtx(u *tgapi.Update, ctx *MsgContext) {
var from *tgapi.User
var chat *tgapi.Chat
switch u.Type {
case tgapi.UpdateTypeMessage:
if u.Message != nil {
ctx.Msg = u.Message
if u.Message.Chat != nil {
chat = u.Message.Chat
}
if u.Message.From != nil {
from = u.Message.From
}
}
case tgapi.UpdateTypeEditedMessage:
if u.EditedMessage != nil {
ctx.Msg = u.EditedMessage
if u.EditedMessage.Chat != nil {
chat = u.EditedMessage.Chat
}
if u.EditedMessage.From != nil {
from = u.EditedMessage.From
}
}
case tgapi.UpdateTypeChannelPost:
if u.ChannelPost != nil {
ctx.Msg = u.ChannelPost
if u.ChannelPost.Chat != nil {
chat = u.ChannelPost.Chat
}
if u.ChannelPost.From != nil {
from = u.ChannelPost.From
}
}
case tgapi.UpdateTypeEditedChannelPost:
if u.EditedChannelPost != nil {
ctx.Msg = u.EditedChannelPost
if u.EditedChannelPost.Chat != nil {
chat = u.EditedChannelPost.Chat
}
if u.EditedChannelPost.From != nil {
from = u.EditedChannelPost.From
}
}
case tgapi.UpdateTypeBusinessMessage:
if u.BusinessMessage != nil {
ctx.Msg = u.BusinessMessage
if u.BusinessMessage.Chat != nil {
chat = u.BusinessMessage.Chat
}
if u.BusinessMessage.From != nil {
from = u.BusinessMessage.From
}
}
case tgapi.UpdateTypeEditedBusinessMessage:
if u.EditedBusinessMessage != nil {
ctx.Msg = u.EditedBusinessMessage
if u.EditedBusinessMessage.Chat != nil {
chat = u.EditedBusinessMessage.Chat
}
if u.EditedBusinessMessage.From != nil {
from = u.EditedBusinessMessage.From
}
}
case tgapi.UpdateTypeInlineQuery:
if u.InlineQuery != nil {
from = &u.InlineQuery.From
}
case tgapi.UpdateTypeChosenInlineResult:
if u.ChosenInlineResult != nil {
from = &u.ChosenInlineResult.From
}
case tgapi.UpdateTypeCallbackQuery:
if u.CallbackQuery != nil {
if u.CallbackQuery.Message != nil {
ctx.Msg = u.CallbackQuery.Message
ctx.CallbackMsgId = u.CallbackQuery.Message.MessageID
if u.CallbackQuery.Message.Chat != nil {
chat = u.CallbackQuery.Message.Chat
}
}
if u.CallbackQuery.InlineMessageID != nil {
ctx.InlineMsgId = *u.CallbackQuery.InlineMessageID
}
ctx.CallbackQueryId = u.CallbackQuery.ID
from = &u.CallbackQuery.From
}
case tgapi.UpdateTypeShippingQuery:
if u.ShippingQuery != nil {
from = &u.ShippingQuery.From
}
case tgapi.UpdateTypePreCheckoutQuery:
if u.PreCheckoutQuery != nil {
from = &u.PreCheckoutQuery.From
}
case tgapi.UpdateTypePurchasedPaidMedia:
if u.PurchasedPaidMedia != nil {
from = &u.PurchasedPaidMedia.From
}
case tgapi.UpdateTypeMyChatMember:
if u.MyChatMember != nil {
from = &u.MyChatMember.From
chat = &u.MyChatMember.Chat
}
case tgapi.UpdateTypeChatMember:
if u.ChatMember != nil {
from = &u.ChatMember.From
chat = &u.ChatMember.Chat
}
case tgapi.UpdateTypeChatJoinRequest:
if u.ChatJoinRequest != nil {
from = &u.ChatJoinRequest.From
chat = &u.ChatJoinRequest.Chat
}
case tgapi.UpdateTypeBusinessConnection:
if u.BusinessConnection != nil {
from = &u.BusinessConnection.User
}
case tgapi.UpdateTypePollAnswer:
if u.PollAnswer != nil {
from = &u.PollAnswer.User
}
case tgapi.UpdateTypeMessageReaction:
if u.MessageReaction != nil {
from = u.MessageReaction.User
chat = u.MessageReaction.Chat
}
case tgapi.UpdateTypeChatBoost:
if u.ChatBoost != nil {
from = &u.ChatBoost.Boost.Source.User
chat = &u.ChatBoost.Chat
}
case tgapi.UpdateTypeRemovedChatBoost:
if u.RemovedChatBoost != nil {
from = &u.RemovedChatBoost.Source.User
chat = &u.RemovedChatBoost.Chat
}
}
if ctx.Msg != nil && from == nil {
from = ctx.Msg.From
}
if from != nil {
ctx.From = from
ctx.FromID = from.ID
} else {
ctx.FromID = 0
}
if chat != nil {
ctx.Chat = chat
ctx.ChatID = chat.ID
} else {
ctx.ChatID = 0
}
}
+10 -6
View File
@@ -3,7 +3,7 @@ package laniakea
import ( import (
"strings" "strings"
"git.nix13.pw/scuroneko/laniakea/utils" "git.scuroneko.dev/scuroneko/laniakea/utils"
) )
// Ptr returns a pointer to v. // Ptr returns a pointer to v.
@@ -53,11 +53,15 @@ func EscapePunctuation(s string) string {
return s return s
} }
// Version constants mirror values from the internal utils/version package.
const ( const (
// VersionString re-exports the module version string.
VersionString = utils.VersionString VersionString = utils.VersionString
VersionMajor = utils.VersionMajor // VersionMajor re-exports the module major version.
VersionMinor = utils.VersionMinor VersionMajor = utils.VersionMajor
VersionPatch = utils.VersionPatch // VersionMinor re-exports the module minor version.
VersionBeta = utils.VersionBeta VersionMinor = utils.VersionMinor
// VersionPatch re-exports the module patch version.
VersionPatch = utils.VersionPatch
// VersionBeta re-exports the module prerelease counter.
VersionBeta = utils.VersionBeta
) )
+5 -7
View File
@@ -9,6 +9,7 @@ import (
"golang.org/x/time/rate" "golang.org/x/time/rate"
) )
// ErrDropOverflow is returned when drop mode rejects a rate-limited request.
var ErrDropOverflow = errors.New("drop overflow limit") var ErrDropOverflow = errors.New("drop overflow limit")
// RateLimiter implements per-chat and global rate limiting with optional blocking. // RateLimiter implements per-chat and global rate limiting with optional blocking.
@@ -102,7 +103,7 @@ func (rl *RateLimiter) Wait(ctx context.Context, chatID int64) error {
return chatLimiter.Wait(ctx) return chatLimiter.Wait(ctx)
} }
// getGlobalLimiter returns the global limiter safely under read lock. // Internal helper that returns the global limiter under read lock.
func (rl *RateLimiter) getGlobalLimiter() *rate.Limiter { func (rl *RateLimiter) getGlobalLimiter() *rate.Limiter {
rl.globalMu.RLock() rl.globalMu.RLock()
defer rl.globalMu.RUnlock() defer rl.globalMu.RUnlock()
@@ -190,8 +191,7 @@ func (rl *RateLimiter) Check(ctx context.Context, dropOverflow bool, chatID int6
return nil return nil
} }
// waitForGlobalUnlock blocks until global cooldown expires or context is done. // Internal helper that waits for the global cooldown to expire.
// Does not check token bucket — only cooldown.
func (rl *RateLimiter) waitForGlobalUnlock(ctx context.Context) error { func (rl *RateLimiter) waitForGlobalUnlock(ctx context.Context) error {
rl.globalMu.RLock() rl.globalMu.RLock()
until := rl.globalLockUntil until := rl.globalLockUntil
@@ -209,8 +209,7 @@ func (rl *RateLimiter) waitForGlobalUnlock(ctx context.Context) error {
} }
} }
// waitForChatUnlock blocks until the specified chat's cooldown expires or context is done. // Internal helper that waits for a chat-specific cooldown to expire.
// Does not check token bucket — only cooldown.
func (rl *RateLimiter) waitForChatUnlock(ctx context.Context, chatID int64) error { func (rl *RateLimiter) waitForChatUnlock(ctx context.Context, chatID int64) error {
rl.chatMu.RLock() rl.chatMu.RLock()
until, ok := rl.chatLocks[chatID] until, ok := rl.chatLocks[chatID]
@@ -228,8 +227,7 @@ func (rl *RateLimiter) waitForChatUnlock(ctx context.Context, chatID int64) erro
} }
} }
// getChatLimiter returns the rate limiter for the given chat, creating it if needed. // Internal helper that returns or creates a per-chat limiter.
// Uses 1 request per second with burst of 1 — conservative for per-user limits.
func (rl *RateLimiter) getChatLimiter(chatID int64) *rate.Limiter { func (rl *RateLimiter) getChatLimiter(chatID int64) *rate.Limiter {
rl.chatMu.Lock() rl.chatMu.Lock()
defer rl.chatMu.Unlock() defer rl.chatMu.Unlock()
+41
View File
@@ -0,0 +1,41 @@
package utils
import (
"context"
"errors"
"testing"
"time"
)
func TestRateLimiterCheckDropOverflowHonorsGlobalLock(t *testing.T) {
rl := NewRateLimiter()
rl.SetGlobalLock(1)
if err := rl.Check(context.Background(), true, 0); !errors.Is(err, ErrDropOverflow) {
t.Fatalf("expected ErrDropOverflow, got %v", err)
}
}
func TestRateLimiterChatLocksAreScopedPerChat(t *testing.T) {
rl := NewRateLimiter()
rl.SetChatLock(42, 1)
if rl.Allow(42) {
t.Fatal("expected locked chat to be rejected")
}
if !rl.Allow(7) {
t.Fatal("expected unrelated chat to remain allowed")
}
}
func TestRateLimiterGlobalWaitRespectsContextCancellation(t *testing.T) {
rl := NewRateLimiter()
rl.SetGlobalLock(1)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond)
defer cancel()
if err := rl.GlobalWait(ctx); !errors.Is(err, context.DeadlineExceeded) {
t.Fatalf("expected DeadlineExceeded, got %v", err)
}
}
+1 -2
View File
@@ -3,7 +3,6 @@ package utils
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io"
"mime/multipart" "mime/multipart"
"reflect" "reflect"
"slices" "slices"
@@ -110,6 +109,6 @@ func writeMultipartValue(w *multipart.Writer, fieldName string, value []byte) er
if err != nil { if err != nil {
return err return err
} }
_, err = io.Copy(fw, strings.NewReader(string(value))) _, err = fw.Write(value)
return err return err
} }
+3 -4
View File
@@ -6,8 +6,8 @@ import (
"mime/multipart" "mime/multipart"
"testing" "testing"
"git.nix13.pw/scuroneko/laniakea/tgapi" "git.scuroneko.dev/scuroneko/laniakea/tgapi"
"git.nix13.pw/scuroneko/laniakea/utils" "git.scuroneko.dev/scuroneko/laniakea/utils"
) )
type multipartEncodeParams struct { type multipartEncodeParams struct {
@@ -19,10 +19,9 @@ type multipartEncodeParams struct {
} }
func TestEncodeMultipartJSONFields(t *testing.T) { func TestEncodeMultipartJSONFields(t *testing.T) {
threadID := 7
params := multipartEncodeParams{ params := multipartEncodeParams{
ChatID: 42, ChatID: 42,
MessageThreadID: &threadID, MessageThreadID: new(7),
ReplyMarkup: &tgapi.ReplyMarkup{ ReplyMarkup: &tgapi.ReplyMarkup{
InlineKeyboard: [][]tgapi.InlineKeyboardButton{{ InlineKeyboard: [][]tgapi.InlineKeyboardButton{{
{Text: "A", CallbackData: "b"}, {Text: "A", CallbackData: "b"},
+1 -1
View File
@@ -3,7 +3,7 @@ package utils
import ( import (
"os" "os"
"git.nix13.pw/scuroneko/slog" "git.scuroneko.dev/scuroneko/slog"
) )
// GetLoggerLevel returns DEBUG when DEBUG=true in env, otherwise FATAL. // GetLoggerLevel returns DEBUG when DEBUG=true in env, otherwise FATAL.
+1 -1
View File
@@ -6,7 +6,7 @@ import (
"strings" "strings"
"testing" "testing"
"git.nix13.pw/scuroneko/slog" "git.scuroneko.dev/scuroneko/slog"
) )
func TestCreateFileLoggerWritesToConfiguredFile(t *testing.T) { func TestCreateFileLoggerWritesToConfiguredFile(t *testing.T) {
+10 -5
View File
@@ -1,9 +1,14 @@
package utils package utils
const ( const (
VersionString = "1.0.0-rc.7" // VersionString is the module version string.
VersionMajor = 1 VersionString = "1.0.0-rc.14"
VersionMinor = 0 // VersionMajor is the module major version.
VersionPatch = 0 VersionMajor = 1
VersionBeta = 7 // VersionMinor is the module minor version.
VersionMinor = 0
// VersionPatch is the module patch version.
VersionPatch = 0
// VersionBeta is the prerelease counter for the current version.
VersionBeta = 14
) )