diff --git a/.cargo/config.toml b/.cargo/config.toml index d6e070ff..a16645bb 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,12 +1,14 @@ [alias] -# We pass "--cfg docsrs" when building docs to turn on nightly-only rustdoc features like -# `This is supported on feature="..." only.` +# Using `--features=full --features=nightly` instead of `--all-features` because of +# https://github.com/rust-lang/cargo/issues/10333 # -# "--cfg dep_docsrs" is used for the same reason, but for `teloxide-core`. -docs = """ -doc - --all-features - --config build.rustflags=["--cfg=dep_docsrs"] - --config build.rustdocflags=["--cfg=docsrs","-Znormalize-docs"] - -Zrustdoc-scrape-examples=examples +# "tokio/macros" and "tokio/rt-multi-thread" are required for examples +docs = """doc +-Zrustdoc-scrape-examples=examples +--features=full --features=nightly +--features=tokio/macros --features=tokio/rt-multi-thread """ + +[build] +# We pass "--cfg docsrs" when building docs to add `This is supported on feature="..." only.` +rustdocflags = ["--cfg", "docsrs", "-Znormalize-docs"] diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e8396993..5c6a7fdb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,13 +15,18 @@ env: CARGO_NET_RETRY: 10 RUSTUP_MAX_RETRIES: 10 - rust_nightly: nightly-2022-09-01 # When updating this, also update: - # - README.md - # - src/lib.rs + # - crates/teloxide-core/src/codegen.rs + # - rust-toolchain.toml + rust_nightly: nightly-2022-09-23 + # When updating this, also update: + # - **/README.md + # - **/src/lib.rs # - down below in a matrix rust_msrv: 1.64.0 + CI: 1 + jobs: # Depends on all action that are required for a "successful" CI run. ci-pass: @@ -82,7 +87,7 @@ jobs: toolchain: beta features: "--features full" - rust: nightly - toolchain: nightly-2022-09-01 + toolchain: nightly-2022-09-23 features: "--all-features" - rust: msrv toolchain: 1.64.0 diff --git a/.gitignore b/.gitignore index b1c241c5..97693892 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,5 @@ /target -**/*.rs.bk Cargo.lock .idea/ .vscode/ -examples/*/target *.sqlite diff --git a/CHANGELOG.md b/CHANGELOG.md index 596a52e5..367631d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## unreleased +## Removed + +- `rocksdb-storage` feature and associated items (See [PR #761](https://github.com/teloxide/teloxide/pull/761) for reasoning) [**BC**] + ## 0.11.2 - 2022-11-18 ### Fixed @@ -313,14 +317,14 @@ This release was yanked because it accidentally [breaks backwards compatibility] - Export `teloxide_macros::teloxide` in `prelude`. - `dispatching::dialogue::serializer::{JSON -> Json, CBOR -> Cbor}` - Allow `bot_name` be `N`, where `N: Into + ...` in `commands_repl` & `commands_repl_with_listener`. -- 'Edit methods' (namely `edit_message_live_location`, `stop_message_live_location`, `edit_message_text`, - `edit_message_caption`, `edit_message_media` and `edit_message_reply_markup`) are split into common and inline +- 'Edit methods' (namely `edit_message_live_location`, `stop_message_live_location`, `edit_message_text`, + `edit_message_caption`, `edit_message_media` and `edit_message_reply_markup`) are split into common and inline versions (e.g.: `edit_message_text` and `edit_inline_message_text`). Instead of `ChatOrInlineMessage` common versions - accept `chat_id: impl Into` and `message_id: i32` whereas inline versions accept + accept `chat_id: impl Into` and `message_id: i32` whereas inline versions accept `inline_message_id: impl Into`. Also note that return type of inline versions is `True` ([issue 253], [pr 257]) -- `ChatOrInlineMessage` is renamed to `TargetMessage`, it's `::Chat` variant is renamed to `::Common`, - `#[non_exhaustive]` annotation is removed from the enum, type of `TargetMessage::Inline::inline_message_id` changed - `i32` => `String`. `TargetMessage` now implements `From`, `get_game_high_scores` and `set_game_score` use +- `ChatOrInlineMessage` is renamed to `TargetMessage`, it's `::Chat` variant is renamed to `::Common`, + `#[non_exhaustive]` annotation is removed from the enum, type of `TargetMessage::Inline::inline_message_id` changed + `i32` => `String`. `TargetMessage` now implements `From`, `get_game_high_scores` and `set_game_score` use `Into` to accept `String`s. ([issue 253], [pr 257]) - Remove `ResponseResult` from `prelude`. diff --git a/Cargo.toml b/Cargo.toml index 031827af..c66a4d73 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,172 +1,2 @@ -[package] -name = "teloxide" -version = "0.11.2" -edition = "2021" -description = "An elegant Telegram bots framework for Rust" -repository = "https://github.com/teloxide/teloxide" -documentation = "https://docs.rs/teloxide/" -readme = "README.md" -keywords = ["teloxide", "telegram", "telegram-bot", "telegram-bot-api"] -categories = ["web-programming", "api-bindings", "asynchronous"] -license = "MIT" -exclude = ["media"] - -[features] -default = ["native-tls", "ctrlc_handler", "teloxide-core/default", "auto-send"] - -webhooks = ["rand"] -webhooks-axum = ["webhooks", "axum", "tower", "tower-http"] - -sqlite-storage = ["sqlx"] -redis-storage = ["redis"] -rocksdb-storage = ["rocksdb"] -cbor-serializer = ["serde_cbor"] -bincode-serializer = ["bincode"] - -macros = ["teloxide-macros"] - -ctrlc_handler = ["tokio/signal"] - -native-tls = ["teloxide-core/native-tls"] -rustls = ["teloxide-core/rustls"] -auto-send = ["teloxide-core/auto_send"] -throttle = ["teloxide-core/throttle"] -cache-me = ["teloxide-core/cache_me"] -trace-adaptor = ["teloxide-core/trace_adaptor"] -erased = ["teloxide-core/erased"] - -# currently used for `README.md` tests, building docs for `docsrs` to add `This is supported on feature="..." only.`, -# and for teloxide-core. -nightly = ["teloxide-core/nightly"] - -full = [ - "webhooks-axum", - "sqlite-storage", - "redis-storage", - "rocksdb-storage", - "cbor-serializer", - "bincode-serializer", - "macros", - "ctrlc_handler", - "teloxide-core/full", - "native-tls", - "rustls", - "auto-send", - "throttle", - "cache-me", - "trace-adaptor", - "erased", -] - -[dependencies] -teloxide-core = { version = "0.8.0", default-features = false } -teloxide-macros = { version = "0.7.0", optional = true } - -serde_json = "1.0" -serde = { version = "1.0", features = ["derive"] } - -dptree = "0.3.0" - -# These lines are used only for development. -# teloxide-core = { git = "https://github.com/teloxide/teloxide-core", rev = "00165e6", default-features = false } -# teloxide-macros = { git = "https://github.com/teloxide/teloxide-macros", rev = "e715105", optional = true } -# dptree = { git = "https://github.com/teloxide/dptree", rev = "df578e4" } - -tokio = { version = "1.8", features = ["fs"] } -tokio-util = "0.7" -tokio-stream = "0.1.8" - -url = "2.2.2" -log = "0.4" -bytes = "1.0" -mime = "0.3" - -derive_more = "0.99" -thiserror = "1.0" -futures = "0.3.15" -pin-project = "1.0" -serde_with_macros = "1.4" -aquamarine = "0.1.11" - -sqlx = { version = "0.6", optional = true, default-features = false, features = [ - "runtime-tokio-native-tls", - "macros", - "sqlite", -] } -redis = { version = "0.21", features = ["tokio-comp"], optional = true } -rocksdb = { version = "0.19", optional = true, default-features = false, features = [ - "lz4", -] } -serde_cbor = { version = "0.11", optional = true } -bincode = { version = "1.3", optional = true } -axum = { version = "0.5.13", optional = true } -tower = { version = "0.4.12", optional = true } -tower-http = { version = "0.3.4", features = ["trace"], optional = true } -rand = { version = "0.8.5", optional = true } - -[dev-dependencies] -rand = "0.8.3" -pretty_env_logger = "0.4.0" -serde = "1" -serde_json = "1" -tokio = { version = "1.8", features = ["fs", "rt-multi-thread", "macros"] } -reqwest = "0.11.11" -chrono = "0.4" -tokio-stream = "0.1" - -[package.metadata.docs.rs] -all-features = true -# FIXME: Add back "-Znormalize-docs" when https://github.com/rust-lang/rust/issues/93703 is fixed -rustdoc-args = ["--cfg", "docsrs"] -rustc-args = ["--cfg", "dep_docsrs"] -cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples=examples"] - -[[test]] -name = "redis" -path = "tests/redis.rs" -required-features = ["redis-storage", "cbor-serializer", "bincode-serializer"] - -[[test]] -name = "sqlite" -path = "tests/sqlite.rs" -required-features = ["sqlite-storage", "cbor-serializer", "bincode-serializer"] - -[[example]] -name = "dialogue" -required-features = ["macros"] - -[[example]] -name = "command" -required-features = ["macros"] - -[[example]] -name = "db_remember" -required-features = ["sqlite-storage", "redis-storage", "bincode-serializer", "macros"] - -[[example]] -name = "inline" -required-features = ["macros"] - -[[example]] -name = "buttons" -required-features = ["macros"] - -[[example]] -name = "admin" -required-features = ["macros"] - -[[example]] -name = "dispatching_features" -required-features = ["macros"] - -[[example]] -name = "ngrok_ping_pong" -required-features = ["webhooks-axum"] - -[[example]] -name = "heroku_ping_pong" -required-features = ["webhooks-axum"] - -[[example]] -name = "purchase" -required-features = ["macros"] +[workspace] +members = ["crates/*"] diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index 4bfbc892..9eb98735 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -1,6 +1,14 @@ This document describes breaking changes of `teloxide` crate, as well as the ways to update code. Note that the list of required changes is not fully exhaustive and it may lack something in rare cases. +## 0.11 -> 0.?? + +### teloxide + +`rocksdb-storage` feature and associated items were removed. +If you are using rocksdb storage, you'll need to either write `Storage` impl yourself, or use a third party crate. + + ## 0.11 -> 0.11.2 ### teloxide diff --git a/README.md b/README.md index 127ca103..1da47ab3 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ > [v0.11 -> v0.11.2 migration guide >>](MIGRATION_GUIDE.md#011---0112)
- +

teloxide

@@ -31,11 +31,10 @@ - **Feature-rich.** You can use both long polling and webhooks, configure an underlying HTTPS client, set a custom URL of a Telegram API server, do graceful shutdown, and much more. - - **Simple dialogues.** Our dialogues subsystem is simple and easy-to-use, and, furthermore, is agnostic of how/where dialogues are stored. For example, you can just replace a one line to achieve [persistence]. Out-of-the-box storages include [Redis], [RocksDB] and [Sqlite]. + - **Simple dialogues.** Our dialogues subsystem is simple and easy-to-use, and, furthermore, is agnostic of how/where dialogues are stored. For example, you can just replace a one line to achieve [persistence]. Out-of-the-box storages include [Redis] and [Sqlite]. [persistence]: https://en.wikipedia.org/wiki/Persistence_(computer_science) [Redis]: https://redis.io/ -[RocksDB]: https://rocksdb.org/ [Sqlite]: https://www.sqlite.org - **Strongly typed commands.** Define bot commands as an `enum` and teloxide will parse them automatically — just like JSON structures in [`serde-json`] and command-line arguments in [`structopt`]. @@ -85,7 +84,7 @@ tokio = { version = "1.8", features = ["rt-multi-thread", "macros"] } This bot replies with a die throw to each received message: -[[`examples/throw_dice.rs`](examples/throw_dice.rs)] +[[`examples/throw_dice.rs`](crates/teloxide/examples/throw_dice.rs)] ```rust,no_run use teloxide::prelude::*; @@ -120,7 +119,7 @@ Commands are strongly typed and defined declaratively, similar to how we define [structopt]: https://docs.rs/structopt/0.3.9/structopt/ [serde-json]: https://github.com/serde-rs/json -[[`examples/command.rs`](examples/command.rs)] +[[`examples/command.rs`](crates/teloxide/examples/command.rs)] ```rust,no_run use teloxide::{prelude::*, utils::command::BotCommands}; @@ -174,7 +173,7 @@ A dialogue is typically described by an enumeration where each variant is one po Below is a bot that asks you three questions and then sends the answers back to you: -[[`examples/dialogue.rs`](examples/dialogue.rs)] +[[`examples/dialogue.rs`](crates/teloxide/examples/dialogue.rs)] ```rust,ignore use teloxide::{dispatching::dialogue::InMemStorage, prelude::*}; @@ -285,7 +284,7 @@ async fn receive_location(
-[More examples >>](examples/) +[More examples >>](crates/teloxide/examples/) ## FAQ @@ -307,11 +306,11 @@ A: No, only the bots API. **Q: Can I use webhooks?** -A: You can! `teloxide` has a built-in support for webhooks in `dispatching::update_listeners::webhooks` module. See how it's used in [`examples/ngrok_ping_pong_bot`](examples/ngrok_ping_pong.rs) and [`examples/heroku_ping_pong_bot`](examples/heroku_ping_pong.rs). +A: You can! `teloxide` has a built-in support for webhooks in `dispatching::update_listeners::webhooks` module. See how it's used in [`examples/ngrok_ping_pong_bot`](crates/teloxide/examples/ngrok_ping_pong.rs) and [`examples/heroku_ping_pong_bot`](crates/teloxide/examples/heroku_ping_pong.rs). **Q: Can I handle both callback queries and messages within a single dialogue?** -A: Yes, see [`examples/purchase.rs`](examples/purchase.rs). +A: Yes, see [`examples/purchase.rs`](crates/teloxide/examples/purchase.rs). ## Community bots diff --git a/crates/teloxide-core/CHANGELOG.md b/crates/teloxide-core/CHANGELOG.md new file mode 100644 index 00000000..c9e9d582 --- /dev/null +++ b/crates/teloxide-core/CHANGELOG.md @@ -0,0 +1,644 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## unreleased + +### Changed + +- The methods `ChatMember::{can_pin_messages, can_invite_users, can_change_info}` now take into account the permissions of `Restricted` chat member kind ([#764][pr764]) +- The method `ChatMemberKind::is_present` now takes into account the value of `Restricted::is_member` field ([#764][pr764]) + +### Added + +- `Restricted::{is_member, can_change_info, can_invite_users, can_pin_messages, can_send_polls}` fields ([#764][pr764]) +- `ChatMember::can_send_polls` method ([#764][pr764]) + +[pr764]: https://github.com/teloxide/teloxide/pull/764 + +## 0.8.0 - 2022-10-03 + +### Added + +- Support for Telegram Bot API [version 6.2](https://core.telegram.org/bots/api#august-12-2022) ([#251][pr251]) + +[pr251]: https://github.com/teloxide/teloxide-core/pull/251 + +### Changed + +- Removed `file_` prefix from `File` and `FileMeta` fields [#255][pr255] +- `Animation`, `Audio`, `Document`, `PassportFile`, `PhotoSize`, `Video`, `VideoNote` and `Voice` now contain `FileMeta` instead of its fields ([#253][pr253]) + - Combined with `File` fields renaming, instead of `.file_size` you can write `.file.size` and similarly with other fields +- **You can now `.await` any `Request`!** ([#249][pr249]) + - `Request` now requires `Self: IntoFuture` + - There is no need for `AutoSend` anymore +- MSRV (Minimal Supported Rust Version) was bumped from `1.58.0` to `1.64.0` +- Message id parameters and fields now use `MessageId` type instead of `i32` ([#254][pr254]) +- Refactored `Sticker` and related types ([#251][pr251]) + +[pr253]: https://github.com/teloxide/teloxide-core/pull/253 +[pr254]: https://github.com/teloxide/teloxide-core/pull/254 +[pr255]: https://github.com/teloxide/teloxide-core/pull/255 + +### Removed + +- Methods for creating `InlineQuery` ([#246][pr244]) + +[pr244]: https://github.com/teloxide/teloxide-core/pull/246 + +### Fixed + +- `SetWebhook` request can now properly send certificate ([#250][pr250]) +- Serialization of `InputSticker::Webm` ([#252][pr252]) + +[pr250]: https://github.com/teloxide/teloxide-core/pull/250 +[pr252]: https://github.com/teloxide/teloxide-core/pull/252 + +### Deprecated + +- `AutoSend` adaptor ([#249][pr249]) + +[pr249]: https://github.com/teloxide/teloxide-core/pull/249 + +## 0.7.1 - 2022-08-19 + +### Fixed + +- `ErasedRequester` now implements `Clone` even if `E` is not `Clone` ([#244][pr244]) + +[pr244]: https://github.com/teloxide/teloxide-core/pull/244 + +### Added + +- `From`, `From` and `From` impls for `RequestError` ([#241][pr241]) + +[pr241]: https://github.com/teloxide/teloxide-core/pull/241 + +### Changed + +- More functions are now marked with `#[must_use]` ([#242][PR242]) + +[pr242]: https://github.com/teloxide/teloxide-core/pull/242 + +## 0.7.0 - 2022-07-19 + +### Added + +- `InlineKeyboardButton::{pay, login, web_app, callback_game, pay}` constructors ([#231][pr231]) +- Support for Telegram Bot API [version 6.1](https://core.telegram.org/bots/api#june-20-2022) ([#233][pr233]) +- `StickerKind` that is now used instead of `is_animated` and `is_video` fields of `Sticker` and `StickerSet` ([#238][pr238]) + +[pr238]: https://github.com/teloxide/teloxide-core/pull/238 + +### Changed + +- `InlineKeyboardButtonKind::Pay`'s only field now has type `True` ([#231][pr231]) +- `file_size` fields are now always `u32` ([#237][pr237]) +- `File` is now split into `File` and `FileMeta`, the latter is used in `UploadStickerFile` and `Sticker::premium_animation` ([#237][pr237]) + +[pr237]: https://github.com/teloxide/teloxide-core/pull/237 + +### Deprecated + +- `InlineKeyboardButton::{text, kind}` functions ([#231][pr231]) + +[pr231]: https://github.com/teloxide/teloxide-core/pull/231 +[pr233]: https://github.com/teloxide/teloxide-core/pull/233 + +### Removed + +- `ChatPrivate::type_` field ([#232][pr232]) + +[pr232]: https://github.com/teloxide/teloxide-core/pull/232 + +## 0.6.3 - 2022-06-21 + +### Fixed + +- Fix `Message::parse_caption_entities` ([#229][pr229]) + +[pr229]: https://github.com/teloxide/teloxide-core/pull/229 + +## 0.6.2 - 2022-06-16 + +### Fixed + +- Fix `ChatPrivate` serialization ([#226][pr226]) +- Build with particular crates versions (enable `"codec"` feature of `tokio-util`) ([#225][pr225]) +- Remove trailing `/` from `Message::url` (on ios it caused problems) ([#223][pr223]) +- Fix incorrect panic in `User::is_channel` ([#222][pr222]) + +[pr226]: https://github.com/teloxide/teloxide-core/pull/226 +[pr225]: https://github.com/teloxide/teloxide-core/pull/225 +[pr222]: https://github.com/teloxide/teloxide-core/pull/222 + +### Added + +- `Message::{url_of, comment_url, comment_url_of, url_in_thread, url_in_thread_of}` functions ([#223][pr223]) +- Utilities to parse message entities (see `Message::parse_entities`) ([#217][pr217]) + +[pr223]: https://github.com/teloxide/teloxide-core/pull/223 +[pr212]: https://github.com/teloxide/teloxide-core/pull/212 + +## 0.6.1 - 2022-06-02 + +### Fixed + +- Deserialization of `File` when `file_path` or `file_size` are missing ([#220][pr220]) +- Correct how `NotFound` and `UserDeactivated` errors are deserialized ([#219][pr219]) + +[pr220]: https://github.com/teloxide/teloxide-core/pull/220 +[pr219]: https://github.com/teloxide/teloxide-core/pull/219 + +### Added + +- `is_*` methods to `ChatMemberStatus` analogous to the `ChatMember{,Kind}` methods ([#216][pr216]) +- `ChatId` and `UserId` to the prelude ([#212][pr212]) + +[pr216]: https://github.com/teloxide/teloxide-core/pull/216 +[pr212]: https://github.com/teloxide/teloxide-core/pull/212 + +## 0.6.0 - 2022-04-25 + +### Added + +- Support for Telegram Bot API [version 6.0](https://core.telegram.org/bots/api#april-16-2022) ([#206][pr206], [#211][pr211]) + - Note that some field were renamed +- Shortcut methods for `MessageEntity` ([#208][pr208], [#210][pr210]) + +[pr208]: https://github.com/teloxide/teloxide-core/pull/208 +[pr206]: https://github.com/teloxide/teloxide-core/pull/206 +[pr210]: https://github.com/teloxide/teloxide-core/pull/210 +[pr211]: https://github.com/teloxide/teloxide-core/pull/211 + +### Changed + +- Make `KeyboardMarkup` creation more convenient ([#207][pr207]) + - Accept `IntoIterator` in `KeyboardMarkup::append_row`. + - Accept `Into` instead of `String` in `InlineKeyboardButton::{url, callback, switch_inline_query, switch_inline_query_current_chat}`. + +[pr207]: https://github.com/teloxide/teloxide-core/pull/207 + +## 0.5.1 - 2022-04-18 + +### Fixed + +- Document the `errors` module. + +## 0.5.0 - 2022-04-13 + +### Added + +- `errors` module and `errors::AsResponseParameters` trait ([#130][pr130]) +- `UserId::{url, is_anonymous, is_channel, is_telegram}` convenience functions ([#197][pr197]) +- `User::{tme_url, preferably_tme_url}` convenience functions ([#197][pr197]) +- `Me::username` and `Deref` implementation for `Me` ([#197][pr197]) +- `Me::{mention, tme_url}` ([#197][pr197]) +- `AllowedUpdate::ChatJoinRequest` ([#201][pr201]) +- `ChatId::{is_user, is_group, is_channel_or_supergroup}` functions [#198][pr198] + +[pr197]: https://github.com/teloxide/teloxide-core/pull/197 +[pr198]: https://github.com/teloxide/teloxide-core/pull/198 +[pr201]: https://github.com/teloxide/teloxide-core/pull/201 + +### Changed + +- `user.id` now uses `UserId` type, `ChatId` now represents only _chat id_, not channel username, all `chat_id` function parameters now accept `Recipient` [**BC**] +- Improve `Throttling` adoptor ([#130][pr130]) + - Freeze when getting `RetryAfter(_)` error + - Retry requests that previously returned `RetryAfter(_)` error +- `RequestError::RetryAfter` now has a `Duration` field instead of `i32` + +### Fixed + +- A bug in `Message::url` implementation ([#198][pr198]) +- Fix never ending loop that caused programs that used `Throttling` to never stop, see issue [#535][issue535] ([#130][pr130]) + +[issue535]: https://github.com/teloxide/teloxide/issues/535 +[pr130]: https://github.com/teloxide/teloxide-core/pull/130 + +## 0.4.5 - 2022-04-03 + +### Fixed + +- Hide bot token in errors ([#200][200]) + +[200]: https://github.com/teloxide/teloxide-core/pull/200 + +## 0.4.4 - 2022-04-21 + +### Added + +- `WrongFileIdOrUrl` and `FailedToGetUrlContent` errors ([#188][pr188]) +- `NotFound` error ([#190][pr190]) +- `HasPayload::with_payload_mut` function ([#189][pr189]) + +[pr188]: https://github.com/teloxide/teloxide-core/pull/188 +[pr189]: https://github.com/teloxide/teloxide-core/pull/189 +[pr190]: https://github.com/teloxide/teloxide-core/pull/190 + +## 0.4.3 - 2022-03-08 + +### Added + +- `User::is_telegram` function ([#186][pr186]) + +[pr186]: https://github.com/teloxide/teloxide-core/pull/186 + +### Fixed + +- `Update::chat()` now returns `Some(&Chat)` for `UpdateKind::ChatMember`, `UpdateKind::MyChatMember`, + `UpdateKind::ChatJoinRequest` ([#184][pr184]) +- `get_updates` timeouts (partially revert buggy [#180][pr180]) ([#185][pr185]) + +[pr184]: https://github.com/teloxide/teloxide-core/pull/184 +[pr185]: https://github.com/teloxide/teloxide-core/pull/185 + +## 0.4.2 - 2022-02-17 [yanked] + +### Deprecated + +- `Message::chat_id` use `.chat.id` field instead ([#182][pr182]) + +[pr182]: https://github.com/teloxide/teloxide-core/pull/182 + +### Fixed + +- Serialization of `SendPoll::type_` (it's now possible to send quiz polls) ([#181][pr181]) + +[pr181]: https://github.com/teloxide/teloxide-core/pull/181 + +### Added + +- `Payload::timeout_hint` method to properly handle long running requests like `GetUpdates` ([#180][pr180]) + +[pr180]: https://github.com/teloxide/teloxide-core/pull/180 + +## 0.4.1 - 2022-02-13 + +### Fixed + +- Deserialization of `UntilDate` ([#178][pr178]) + +[pr178]: https://github.com/teloxide/teloxide-core/pull/178 + +## 0.4.0 - 2022-02-03 + +### Added + +- `ApiError::TooMuchInlineQueryResults` ([#135][pr135]) +- `ApiError::NotEnoughRightsToChangeChatPermissions` ([#155][pr155]) +- Support for 5.4 telegram bot API ([#133][pr133]) +- Support for 5.5 telegram bot API ([#143][pr143], [#164][pr164]) +- Support for 5.6 telegram bot API ([#162][pr162]) +- Support for 5.7 telegram bot API ([#175][pr175]) +- `EditedMessageIsTooLong` error ([#109][pr109]) +- `UntilDate` enum and use it for `{Restricted, Banned}::until_date` ([#117][pr117]) +- `Limits::messages_per_min_channel` ([#121][pr121]) +- `media_group_id` field to `MediaDocument` and `MediaAudio` ([#139][pr139]) +- `caption_entities` method to `InputMediaPhoto` ([#140][pr140]) +- `User::is_anonymous` and `User::is_channel` functions ([#151][pr151]) +- `UpdateKind::Error` ([#156][pr156]) + +[pr109]: https://github.com/teloxide/teloxide-core/pull/109 +[pr117]: https://github.com/teloxide/teloxide-core/pull/117 +[pr121]: https://github.com/teloxide/teloxide-core/pull/121 +[pr135]: https://github.com/teloxide/teloxide-core/pull/135 +[pr139]: https://github.com/teloxide/teloxide-core/pull/139 +[pr140]: https://github.com/teloxide/teloxide-core/pull/140 +[pr143]: https://github.com/teloxide/teloxide-core/pull/143 +[pr151]: https://github.com/teloxide/teloxide-core/pull/151 +[pr155]: https://github.com/teloxide/teloxide-core/pull/155 +[pr156]: https://github.com/teloxide/teloxide-core/pull/156 +[pr162]: https://github.com/teloxide/teloxide-core/pull/162 +[pr164]: https://github.com/teloxide/teloxide-core/pull/164 +[pr175]: https://github.com/teloxide/teloxide-core/pull/175 + +### Changed + +- Refactor `InputFile` ([#167][pr167]) + - Make it an opaque structure, instead of enum + - Add `read` constructor, that allows creating `InputFile` from `impl AsyncRead` + - Internal changes +- Refactor errors ([#134][pr134]) + - Rename `DownloadError::NetworkError` to `Network` + - Rename `RequestError::ApiError` to `Api` + - Remove `RequestError::Api::status_code` and rename `RequestError::Api::kind` to `0` (struct to tuple struct) + - Rename `RequestError::NetworkError` to `Network` + - Implement `Error` for `ApiError` +- Use `url::Url` for urls, use `chrono::DateTime` for dates in types ([#115][pr115]) +- Mark `ApiError` as `non_exhaustive` ([#125][pr125]) +- `InputFile` and related structures now do **not** implement `PartialEq`, `Eq` and `Hash` ([#133][pr133]) +- How forwarded messages are represented ([#151][pr151]) +- `RequestError::InvalidJson` now has a `raw` field with raw json for easier debugability ([#150][pr150]) +- `ChatPermissions` is now bitflags ([#157][pr157]) +- Type of `WebhookInfo::ip_address` from `Option` to `Option` ([#172][pr172]) +- Type of `WebhookInfo::allowed_updates` from `Option>` to `Option>` ([#174][pr174]) + +[pr115]: https://github.com/teloxide/teloxide-core/pull/115 +[pr125]: https://github.com/teloxide/teloxide-core/pull/125 +[pr134]: https://github.com/teloxide/teloxide-core/pull/134 +[pr150]: https://github.com/teloxide/teloxide-core/pull/150 +[pr157]: https://github.com/teloxide/teloxide-core/pull/157 +[pr167]: https://github.com/teloxide/teloxide-core/pull/167 +[pr172]: https://github.com/teloxide/teloxide-core/pull/172 +[pr174]: https://github.com/teloxide/teloxide-core/pull/174 + +### Fixed + +- Deserialization of chat migrations, see issue [#427][issue427] ([#143][pr143]) +- Type of `BanChatMember::until_date`: `u64` -> `chrono::DateTime` ([#117][pr117]) +- Type of `Poll::correct_option_id`: `i32` -> `u8` ([#119][pr119]) +- Type of `Poll::open_period`: `i32` -> `u16` ([#119][pr119]) +- `Throttle` adaptor not honouring chat/min limits ([#121][pr121]) +- Make `SendPoll::type_` optional ([#133][pr133]) +- Bug with `caption_entities`, see issue [#473][issue473] +- Type of response for `CopyMessage` method ([#141][pr141], [#142][pr142]) +- Bad request serialization when the `language` field of `MessageEntityKind::Pre` is `None` ([#145][pr145]) +- Deserialization of `MediaKind::Venue` ([#147][pr147]) +- Deserialization of `VoiceChat{Started,Ended}` messages ([#153][pr153]) +- Serialization of `BotCommandScope::Chat{,Administrators}` ([#154][pr154]) + +[pr119]: https://github.com/teloxide/teloxide-core/pull/119 +[pr133]: https://github.com/teloxide/teloxide-core/pull/133 +[pr141]: https://github.com/teloxide/teloxide-core/pull/141 +[pr142]: https://github.com/teloxide/teloxide-core/pull/142 +[pr143]: https://github.com/teloxide/teloxide-core/pull/143 +[pr145]: https://github.com/teloxide/teloxide-core/pull/145 +[pr147]: https://github.com/teloxide/teloxide-core/pull/147 +[pr153]: https://github.com/teloxide/teloxide-core/pull/153 +[pr154]: https://github.com/teloxide/teloxide-core/pull/154 +[issue473]: https://github.com/teloxide/teloxide/issues/473 +[issue427]: https://github.com/teloxide/teloxide/issues/427 + +### Removed + +- `get_updates_fault_tolerant` method and `SemiparsedVec` ([#156][pr156]) + +## 0.3.3 - 2021-08-03 + +### Fixed + +- Compilation with `nightly` feature (use `type_alias_impl_trait` instead of `min_type_alias_impl_trait`) ([#108][pr108]) + +[pr108]: https://github.com/teloxide/teloxide-core/pull/108 + +## 0.3.2 - 2021-07-27 + +### Added + +- `ErasedRequester` bot adaptor, `ErasedRequest` struct, `{Request, RequesterExt}::erase` functions ([#105][pr105]) +- `Trace` bot adaptor ([#104][pr104]) +- `HasPayload`, `Request` and `Requester` implementations for `either::Either` ([#103][pr103]) + +[pr103]: https://github.com/teloxide/teloxide-core/pull/103 +[pr104]: https://github.com/teloxide/teloxide-core/pull/104 +[pr105]: https://github.com/teloxide/teloxide-core/pull/105 + +## 0.3.1 - 2021-07-07 + +- Minor documentation tweaks ([#102][pr102]) +- Remove `Self: 'static` bound on `RequesterExt::throttle` ([#102][pr102]) + +[pr102]: https://github.com/teloxide/teloxide-core/pull/102 + +## 0.3.0 - 2021-07-05 + +### Added + +- `impl Clone` for {`CacheMe`, `DefaultParseMode`, `Throttle`} ([#76][pr76]) +- `DefaultParseMode::parse_mode` which allows to get currently used default parse mode ([#77][pr77]) +- `Thrrotle::{limits,set_limits}` functions ([#77][pr77]) +- `Throttle::{with_settings,spawn_with_settings}` and `throttle::Settings` ([#96][pr96]) +- Getters for fields nested in `Chat` ([#80][pr80]) +- API errors: `ApiError::NotEnoughRightsToManagePins`, `ApiError::BotKickedFromSupergroup` ([#84][pr84]) +- Telegram bot API 5.2 support ([#86][pr86]) +- Telegram bot API 5.3 support ([#99][pr99]) +- `net::default_reqwest_settings` function ([#90][pr90]) + +[pr75]: https://github.com/teloxide/teloxide-core/pull/75 +[pr77]: https://github.com/teloxide/teloxide-core/pull/77 +[pr76]: https://github.com/teloxide/teloxide-core/pull/76 +[pr80]: https://github.com/teloxide/teloxide-core/pull/80 +[pr84]: https://github.com/teloxide/teloxide-core/pull/84 +[pr86]: https://github.com/teloxide/teloxide-core/pull/86 +[pr90]: https://github.com/teloxide/teloxide-core/pull/90 +[pr96]: https://github.com/teloxide/teloxide-core/pull/96 +[pr99]: https://github.com/teloxide/teloxide-core/pull/99 + +### Changed + +- `Message::url` now returns links to messages in private groups too ([#80][pr80]) +- Refactor `ChatMember` methods ([#74][pr74]) + - impl `Deref` to make `ChatMemberKind`'s methods callable directly on `ChatMember` + - Add `ChatMemberKind::is_{creator,administrator,member,restricted,left,kicked}` which check `kind` along with `is_privileged` and `is_in_chat` which combine some of the above. + - Refactor privilege getters +- Rename `ChatAction::{RecordAudio => RecordVoice, UploadAudio => UploadVoice}` ([#86][pr86]) +- Use `url::Url` for urls, use `chrono::DateTime` for dates ([#97][pr97]) + +[pr74]: https://github.com/teloxide/teloxide-core/pull/74 +[pr97]: https://github.com/teloxide/teloxide-core/pull/97 + +### Fixed + +- telegram_response: fix issue `retry_after` and `migrate_to_chat_id` handling ([#94][pr94]) +- Type of `PublicChatSupergroup::slow_mode_delay` field: `Option`=> `Option` ([#80][pr80]) +- Add missing `Chat::message_auto_delete_time` field ([#80][pr80]) +- Output types of `LeaveChat` `PinChatMessage`, `SetChatDescription`, `SetChatPhoto` `SetChatTitle`, `UnpinAllChatMessages` and `UnpinChatMessage`: `String` => `True` ([#79][pr79]) +- `SendChatAction` output type `Message` => `True` ([#75][pr75]) +- `GetChatAdministrators` output type `ChatMember` => `Vec` ([#73][pr73]) +- `reqwest` dependency bringing `native-tls` in even when `rustls` was selected ([#71][pr71]) +- Type of `{Restricted,Kicked}::until_date` fields: `i32` => `i64` ([#74][pr74]) +- Type of `PhotoSize::{width,height}` fields: `i32` => `u32` ([#100][pr100]) + +[pr71]: https://github.com/teloxide/teloxide-core/pull/71 +[pr73]: https://github.com/teloxide/teloxide-core/pull/73 +[pr75]: https://github.com/teloxide/teloxide-core/pull/75 +[pr79]: https://github.com/teloxide/teloxide-core/pull/79 +[pr94]: https://github.com/teloxide/teloxide-core/pull/94 +[pr100]: https://github.com/teloxide/teloxide-core/pull/100 + +## 0.2.2 - 2020-03-22 + +### Fixed + +- Typo: `ReplyMarkup::{keyboad => keyboard}` ([#69][pr69]) + - Note: method with the old name was deprecated and hidden from docs + +[pr69]: https://github.com/teloxide/teloxide-core/pull/69 + +## 0.2.1 - 2020-03-19 + +### Fixed + +- Types fields privacy (make fields of some types public) ([#68][pr68]) + - `Dice::{emoji, value}` + - `MessageMessageAutoDeleteTimerChanged::message_auto_delete_timer_changed` + - `PassportElementError::{message, kind}` + - `StickerSet::thumb` + +[pr68]: https://github.com/teloxide/teloxide-core/pull/68 + +## 0.2.0 - 2020-03-16 + +### Changed + +- Refactor `ReplyMarkup` ([#pr65][pr65]) (**BC**) + - Rename `ReplyMarkup::{InlineKeyboardMarkup => InlineKeyboard, ReplyKeyboardMarkup => Keyboard, ReplyKeyboardRemove => KeyboardRemove}` + - Add `inline_kb`, `keyboad`, `kb_remove` and `force_reply` `ReplyMarkup` consructors + - Rename `ReplyKeyboardMarkup` => `KeyboardMarkup` + - Rename `ReplyKeyboardRemove` => `KeyboardRemove` + - Remove useless generic param from `ReplyKeyboardMarkup::new` and `InlineKeyboardMarkup::new` + - Change parameters order in `ReplyKeyboardMarkup::append_to_row` and `InlineKeyboardMarkup::append_to_row` +- Support telegram bot API version 5.1 (see it's [changelog](https://core.telegram.org/bots/api#march-9-2021)) ([#pr63][pr63]) (**BC**) +- Support telegram bot API version 5.0 (see it's [changelog](https://core.telegram.org/bots/api#november-4-2020)) ([#pr62][pr62]) (**BC**) + +[pr62]: https://github.com/teloxide/teloxide-core/pull/62 +[pr63]: https://github.com/teloxide/teloxide-core/pull/63 +[pr65]: https://github.com/teloxide/teloxide-core/pull/65 + +### Added + +- `GetUpdatesFaultTolerant` - fault toletant version of `GetUpdates` ([#58][pr58]) (**BC**) +- Derive `Clone` for `AutoSend`. + +[pr58]: https://github.com/teloxide/teloxide-core/pull/58 + +### Fixed + +- Make `MediaContact::contact` public ([#pr64][pr64]) +- `set_webhook` signature (make `allowed_updates` optional) ([#59][pr59]) +- Fix typos in payloads ([#57][pr57]): + - `get_updates`: `offset` `i64` -> `i32` + - `send_location`: make `live_period` optional +- `send_contact` signature (`phone_number` and `first_name` `f64` => `String`) ([#56][pr56]) + +[pr56]: https://github.com/teloxide/teloxide-core/pull/56 +[pr57]: https://github.com/teloxide/teloxide-core/pull/57 +[pr59]: https://github.com/teloxide/teloxide-core/pull/59 +[pr64]: https://github.com/teloxide/teloxide-core/pull/64 + +### Removed + +- `Message::text_owned` ([#pr62][pr62]) (**BC**) + +### Changed + +- `NonStrictVec` -> `SemiparsedVec`. + +## 0.1.1 - 2020-02-17 + +### Fixed + +- Remove `dbg!` call from internals ([#53][pr53]) + +[pr53]: https://github.com/teloxide/teloxide-core/pull/53 + +## 0.1.0 - 2020-02-17 + +### Added + +- `#[non_exhaustive]` on `InputFile` since we may want to add new ways to send files in the future ([#49][pr49]) +- `MultipartPayload` for future proofing ([#49][pr49]) +- Support for `rustls` ([#24][pr24]) +- `#[must_use]` attr to payloads implemented by macro ([#22][pr22]) +- forward-to-deref `Requester` impls ([#39][pr39]) +- `Bot::{set_,}api_url` methods ([#26][pr26], [#35][pr35]) +- `payloads` module +- `RequesterExt` trait which is implemented for all `Requester`s and allows easily wrapping them in adaptors +- `adaptors` module ([#14][pr14]) + - `throttle`, `cache_me`, `auto_send` and `full` crate features + - Request throttling - opt-in feature represented by `Throttle` bot adapter which allows automatically checking telegram limits ([#10][pr10], [#46][pr46], [#50][pr50]) + - Request auto sending - ability to `.await` requests without need to call `.send()` (opt-in feature represented by `AutoSend` bot adapter, [#8][pr8]) + - `get_me` caching (opt-in feature represented by `CacheMe` bot adapter) +- `Requester` trait which represents bot-clients ([#7][pr7], [#12][pr12], [#27][pr27]) +- `{Json,Multipart}Request` the `Bot` requests types ([#6][pr6]) +- `Output` alias to `<::Payload as Payload>::Output` +- `Payload`, `HasPayload` and `Request` traits which represent different parts of the request ([#5][pr5]) +- `GetUpdatesNonStrict` 'telegram' method, that behaves just like `GetUpdates` but doesn't [#2][pr2] + fail if one of updates fails to be deserialized +- Move core code here from the [`teloxide`] main repo, for older changes see it's [`CHANGELOG.md`]. + - Following modules were moved: + - `bot` + - `requests` [except `requests::respond` function] + - `types` + - `errors` + - `net` [private] + - `client_from_env` was moved from `teloxide::utils` to crate root of `teloxide-core` + - To simplify `GetUpdates` request it was changed to simply return `Vec` + (instead of `Vec>`) + +[pr2]: https://github.com/teloxide/teloxide-core/pull/2 +[pr5]: https://github.com/teloxide/teloxide-core/pull/5 +[pr6]: https://github.com/teloxide/teloxide-core/pull/6 +[pr7]: https://github.com/teloxide/teloxide-core/pull/7 +[pr8]: https://github.com/teloxide/teloxide-core/pull/8 +[pr10]: https://github.com/teloxide/teloxide-core/pull/10 +[pr12]: https://github.com/teloxide/teloxide-core/pull/12 +[pr14]: https://github.com/teloxide/teloxide-core/pull/14 +[pr22]: https://github.com/teloxide/teloxide-core/pull/22 +[pr24]: https://github.com/teloxide/teloxide-core/pull/24 +[pr26]: https://github.com/teloxide/teloxide-core/pull/26 +[pr27]: https://github.com/teloxide/teloxide-core/pull/27 +[pr35]: https://github.com/teloxide/teloxide-core/pull/35 +[pr39]: https://github.com/teloxide/teloxide-core/pull/39 +[pr46]: https://github.com/teloxide/teloxide-core/pull/46 +[pr49]: https://github.com/teloxide/teloxide-core/pull/49 +[pr50]: https://github.com/teloxide/teloxide-core/pull/50 + +### Changed + +- Cleanup setters in `types::*` (remove most of them) ([#44][pr44]) +- Refactor `KeyboardButtonPollType` ([#44][pr44]) +- Replace `Into>` by `IntoIterator` in function arguments ([#44][pr44]) +- Update dependencies (including tokio 1.0) ([#37][pr37]) +- Refactor file downloading ([#30][pr30]): + - Make `net` module public + - Move `Bot::download_file{,_stream}` methods to a new `Download` trait + - Impl `Download` for all bot adaptors & the `Bot` itself + - Change return type of `download_file_stream` — return ` Stream>``, instead of `Future>>>`` + - Add `api_url` param to standalone versions of `download_file{,_stream}` + - Make `net::{TELEGRAM_API_URL, download_file{,_stream}}` pub +- Refactor `Bot` ([#29][pr29]): + - Move default parse mode to an adaptor (`DefaultParseMode`) + - Remove bot builder (it's not usefull anymore, since parse_mode is moved away) + - Undeprecate bot constructors (`Bot::{new, with_client, from_env_with_client}`) +- Rename `StickerType` => `InputSticker`, `{CreateNewStickerSet,AddStickerToSet}::sticker_type}` => `sticker` ([#23][pr23], [#43][pr43]) +- Use `_: IntoIterator` bound instead of `_: Into>` in telegram methods which accept collections ([#21][pr21]) +- Make `MessageDice::dice` pub ([#20][pr20]) +- Merge `ApiErrorKind` and `KnownApiErrorKind` into `ApiError` ([#13][pr13]) +- Refactor ChatMember ([#9][pr9]) + - Replace a bunch of `Option<_>` fields with `ChatMemberKind` + - Remove setters (users are not expected to create this struct) + - Add getters +- Changed internal mechanism of sending multipart requests ([#1][pr1]) +- Added `RequestError::Io(io::Error)` to wrap I/O error those can happen while sending files to telegram +- Make all fields of all methods `pub` ([#3][pr3]) + +[pr1]: https://github.com/teloxide/teloxide-core/pull/1 +[pr3]: https://github.com/teloxide/teloxide-core/pull/3 +[pr9]: https://github.com/teloxide/teloxide-core/pull/9 +[pr13]: https://github.com/teloxide/teloxide-core/pull/13 +[pr20]: https://github.com/teloxide/teloxide-core/pull/20 +[pr21]: https://github.com/teloxide/teloxide-core/pull/21 +[pr23]: https://github.com/teloxide/teloxide-core/pull/23 +[pr29]: https://github.com/teloxide/teloxide-core/pull/29 +[pr30]: https://github.com/teloxide/teloxide-core/pull/30 +[pr37]: https://github.com/teloxide/teloxide-core/pull/37 +[pr43]: https://github.com/teloxide/teloxide-core/pull/43 + +### Removed + +- `unstable-stream` feature (now `Bot::download_file_stream` is accesable by default) +- old `Request` trait +- `RequestWithFile`, now multipart requests use `Request` +- Remove all `#[non_exhaustive]` annotations ([#4][pr4]) +- Remove `MessageEntity::text_from` because it's wrong ([#44][pr44]) + +[pr4]: https://github.com/teloxide/teloxide-core/pull/4 +[pr44]: https://github.com/teloxide/teloxide-core/pull/44 +[`teloxide`]: https://github.com/teloxide/teloxide +[`changelog.md`]: https://github.com/teloxide/teloxide/blob/master/CHANGELOG.md diff --git a/crates/teloxide-core/Cargo.toml b/crates/teloxide-core/Cargo.toml new file mode 100644 index 00000000..fb1cfcb1 --- /dev/null +++ b/crates/teloxide-core/Cargo.toml @@ -0,0 +1,106 @@ +[package] +name = "teloxide-core" +description = "Core part of the `teloxide` library - telegram bot API client" +version = "0.8.0" +edition = "2021" + +license = "MIT" +repository = "https://github.com/teloxide/teloxide-core/" +homepage = "https://github.com/teloxide/teloxide-core/" +documentation = "https://docs.rs/teloxide-core/" +readme = "README.md" + +keywords = ["telegram", "bot", "tba"] +categories = ["api-bindings", "asynchronous"] + +exclude = [ + ".github/*", + "netlify.toml", +] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +futures = "0.3.5" +tokio = { version = "1.12.0", features = ["fs"] } +tokio-util = { version = "0.7.0", features = ["codec"] } +pin-project = "1.0.12" +bytes = "1.0.0" +reqwest = { version = "0.11.10", features = ["json", "stream", "multipart"], default-features = false } +url = { version = "2", features = ["serde"] } +log = "0.4" + +serde = { version = "1.0.114", features = ["derive"] } +serde_json = "1.0.55" +serde_with_macros = "1.5.2" +uuid = { version = "1.1.0", features = ["v4"] } # for attaching input files + +derive_more = "0.99.9" +mime = "0.3.16" +thiserror = "1.0.20" +once_cell = "1.5.0" +takecell = "0.1" +take_mut = "0.2" +rc-box = "1.1.1" +never = "0.1.0" +chrono = { version = "0.4.19", default-features = false } +either = "1.6.1" +bitflags = { version = "1.2" } + +vecrem = { version = "0.1", optional = true } + +[dev-dependencies] +pretty_env_logger = "0.4" +tokio = { version = "1.8.0", features = ["fs", "macros", "macros", "rt-multi-thread"] } +cool_asserts = "2.0.3" + +xshell = "0.2" +ron = "0.7" +indexmap = { version = "1.9", features = ["serde-1"] } +aho-corasick = "0.7" +itertools = "0.10" + +[features] +default = ["native-tls"] + +rustls = ["reqwest/rustls-tls"] +native-tls = ["reqwest/native-tls"] + +# Features which require nightly compiler. +# +# Currently the only used compiler feature is feature(type_alias_impl_trait) +# which allow implementing `Future`s without boxing. +nightly = [] + +# Throttling bot adaptor +throttle = ["vecrem", "tokio/macros"] + +# Trace bot adaptor +trace_adaptor = [] + +# Erased bot adaptor +erased = [] + +# CacheMe bot adaptor +cache_me = [] + +# AutoSend bot adaptor +auto_send = [] + +# All features except nightly and tls-related +full = ["throttle", "trace_adaptor", "erased", "cache_me", "auto_send"] + +[package.metadata.docs.rs] +features = ["full", "nightly", "tokio/macros", "tokio/rt-multi-thread"] +rustdoc-args = ["--cfg", "docsrs", "-Znormalize-docs"] + +# https://github.com/rust-lang/rust/issues/88791 +cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples=examples"] + +[[example]] +name = "self_info" +required-features = ["tokio/macros", "tokio/rt-multi-thread"] + +[[example]] +name = "erased" +required-features = ["tokio/macros", "tokio/rt-multi-thread", "erased", "trace_adaptor"] diff --git a/crates/teloxide-core/LICENSE b/crates/teloxide-core/LICENSE new file mode 120000 index 00000000..30cff740 --- /dev/null +++ b/crates/teloxide-core/LICENSE @@ -0,0 +1 @@ +../../LICENSE \ No newline at end of file diff --git a/crates/teloxide-core/README.md b/crates/teloxide-core/README.md new file mode 100644 index 00000000..d4c80ba5 --- /dev/null +++ b/crates/teloxide-core/README.md @@ -0,0 +1,34 @@ +
+ + +

teloxide-core

+
+ + + + + + + + + + + + + + + + + + + The core part of [`teloxide`] providing tools for making requests to the [Telegram Bot API] with ease. This library is fully asynchronous and built using [`tokio`]. +
+ +```toml +teloxide-core = "0.8" +``` +_Compiler support: requires rustc 1.64+_. + +[`teloxide`]: https://docs.rs/teloxide +[Telegram Bot API]: https://core.telegram.org/bots/api +[`tokio`]: https://tokio.rs diff --git a/crates/teloxide-core/examples/erased.rs b/crates/teloxide-core/examples/erased.rs new file mode 100644 index 00000000..feeffbdc --- /dev/null +++ b/crates/teloxide-core/examples/erased.rs @@ -0,0 +1,42 @@ +use std::{env::VarError, time::Duration}; + +use teloxide_core::{adaptors::trace, prelude::*, types::ChatAction}; + +#[tokio::main] +async fn main() -> Result<(), Box> { + pretty_env_logger::init(); + + let chat_id = + ChatId(std::env::var("CHAT_ID").expect("Expected CHAT_ID env var").parse::()?); + + let trace_settings = match std::env::var("TRACE").as_deref() { + Ok("EVERYTHING_VERBOSE") => trace::Settings::TRACE_EVERYTHING_VERBOSE, + Ok("EVERYTHING") => trace::Settings::TRACE_EVERYTHING, + Ok("REQUESTS_VERBOSE") => trace::Settings::TRACE_REQUESTS_VERBOSE, + Ok("REQUESTS") => trace::Settings::TRACE_REQUESTS, + Ok("RESPONSES_VERBOSE") => trace::Settings::TRACE_RESPONSES_VERBOSE, + Ok("RESPONSES") => trace::Settings::TRACE_RESPONSES, + Ok("EMPTY") | Ok("") | Err(VarError::NotPresent) => trace::Settings::empty(), + Ok(_) | Err(VarError::NotUnicode(_)) => { + panic!( + "Expected `TRACE` environment variable to be equal to any of the following: \ + `EVERYTHING_VERBOSE`, `EVERYTHING`, `REQUESTS_VERBOSE`, `REQUESTS`, \ + `RESPONSES_VERBOSE`, `RESPONSES`, `EMPTY`, `` (empty string)" + ) + } + }; + + log::info!("Trace settings: {:?}", trace_settings); + + let bot = if trace_settings.is_empty() { + Bot::from_env().erase() + } else { + Bot::from_env().trace(trace_settings).erase() + }; + + bot.send_chat_action(chat_id, ChatAction::Typing).await?; + tokio::time::sleep(Duration::from_secs(1)).await; + bot.send_message(chat_id, "Hey hey hey").await?; + + Ok(()) +} diff --git a/crates/teloxide-core/examples/self_info.rs b/crates/teloxide-core/examples/self_info.rs new file mode 100644 index 00000000..6ea7cb24 --- /dev/null +++ b/crates/teloxide-core/examples/self_info.rs @@ -0,0 +1,21 @@ +use teloxide_core::{ + prelude::*, + types::{DiceEmoji, Me, ParseMode}, +}; + +#[tokio::main] +async fn main() -> Result<(), Box> { + pretty_env_logger::init(); + + let chat_id = + ChatId(std::env::var("CHAT_ID").expect("Expected CHAT_ID env var").parse::()?); + + let bot = Bot::from_env().parse_mode(ParseMode::MarkdownV2); + + let Me { user: me, .. } = bot.get_me().await?; + + bot.send_dice(chat_id).emoji(DiceEmoji::Dice).await?; + bot.send_message(chat_id, format!("Hi, my name is **{}** 👋", me.first_name)).await?; + + Ok(()) +} diff --git a/crates/teloxide-core/schema.ron b/crates/teloxide-core/schema.ron new file mode 100644 index 00000000..dfe1e459 --- /dev/null +++ b/crates/teloxide-core/schema.ron @@ -0,0 +1,3863 @@ +//! This file is written in [RON] (Rusty Object Notation). +//! +//! This "schema" is a formalized version of the +//! [telegram bot api documentation][tbadoc] which is not machine readable. +//! (note: this schema currently covers only API methods and **not** types). +//! +//! Also, note that this file is **hand written** and may contain typos, +//! deviations from original doc, and other kinds of typical human errors. +//! If you found an error please open an issue (or make a PR) on [github]. +//! +//! This schema is targeting code generation for API wrappers in a statically +//! typed language, though you may use it whatever you want. +//! +//! This scheme also has some intentional differences from original doc: +//! * New types: +//! + `ChatId` - type of `chat_id` parameter, in the original documentation +//! written as `Integer or String +//! + `UserId` - type of `user_id` parameters +//! + `ChatAction` - type of `action` param in `sendChatAction` method +//! + `AllowedUpdate` inner type of `allowed_updates` in `getUpdates` and +//! `setWebhook` (so type is `ArrayOf(AllowedUpdate)`) +//! + `ReplyMarkup` - type of `reply_markup` parameter, in the original +//! documentation written as `InlineKeyboardMarkup or ReplyKeyboardMarkup or +//! ReplyKeyboardRemove or ForceReply` +//! + `ParseMode` type of `parse_mode` params +//! + `PollType` type of poll, either “quiz” or “regular” +//! + `DiceEmoji` emoji that can be used in `sendDice` one of “🎲”, “🎯”, or “🏀” +//! + `TargetMessage` either `inline_message_id: String` or `chat_id: ChatId` and `message_id: i64` +//! + `InputSticker` +//! * Integers represented with more strict (when possible) types, e.g.: +//! `u8` (unsigned, 8-bit integer), `u32` (unsigned, 32-bit), +//! `i64` (signed, 64-bit), etc +//! * Instead of optional parameters `Option(Ty)` is used +//! * Instead of `InputFile or String` just `InputFile` is used (assuming that +//! `InputFile` is a sum-type or something and it can contain `String`s) +//! * `f64` ~= `Float number` +//! +//! [tbadoc]: https://core.telegram.org/bots/api +//! [RON]: https://github.com/ron-rs/ron +//! [github]: https://github.com/WaffleLapkin/tg-methods-schema + +Schema( + api_version: ApiVersion(ver: "6.1", date: "June 20, 2022"), + methods: [ + Method( + names: ("getUpdates", "GetUpdates", "get_updates"), + return_ty: ArrayOf(RawTy("Update")), + doc: Doc( + md: "Use this method to receive incoming updates using long polling ([wiki]). An Array of [Update] objects is returned.", + md_links: { + "wiki": "https://en.wikipedia.org/wiki/Push_technology#Long_polling", + "Update": "https://core.telegram.org/bots/api#update", + }, + ), + tg_doc: "https://core.telegram.org/bots/api#getupdates", + tg_category: "Getting updates", + params: [ + Param( + name: "offset", + ty: Option(i32), + descr: Doc( + md: "Identifier of the first update to be returned. Must be greater by one than the highest among the identifiers of previously received updates. By default, updates starting with the earliest unconfirmed update are returned. An update is considered confirmed as soon as [getUpdates] is called with an offset higher than its update_id. The negative offset can be specified to retrieve updates starting from -offset update from the end of the updates queue. All previous updates will forgotten.", + md_links: { + "getUpdates": "https://core.telegram.org/bots/api#getupdates", + } + ) + ), + Param( + name: "limit", + ty: Option(u8), + descr: Doc(md: "Limits the number of updates to be retrieved. Values between 1-100 are accepted. Defaults to 100."), + ), + Param( + name: "timeout", + ty: Option(u32), + descr: Doc(md: "Timeout in seconds for long polling. Defaults to 0, i.e. usual short polling. Should be positive, short polling should be used for testing purposes only."), + ), + Param( + name: "allowed_updates", + ty: Option(ArrayOf(RawTy("AllowedUpdate"))), + descr: Doc( + md: "A JSON-serialized list of the update types you want your bot to receive. For example, specify [“message”, “edited_channel_post”, “callback_query”] to only receive updates of these types. See [Update] for a complete list of available update types. Specify an empty list to receive all update types except chat_member (default). If not specified, the previous setting will be used.\n\nPlease note that this parameter doesn't affect updates created before the call to the getUpdates, so unwanted updates may be received for a short period of time.", + md_links: {"Update":"https://core.telegram.org/bots/api#update"}, + ), + ) + ], + notes: [ + (md: "This method will not work if an outgoing webhook is set up."), + (md: "In order to avoid getting duplicate updates, recalculate _offset_ after each server response.") + ], + ), + Method( + names: ("setWebhook", "SetWebhook", "set_webhook"), + return_ty: True, + doc: Doc( + md: "Use this method to specify a url and receive incoming updates via an outgoing webhook. Whenever there is an update for the bot, we will send an HTTPS POST request to the specified url, containing a JSON-serialized [Update]. In case of an unsuccessful request, we will give up after a reasonable amount of attempts. Returns True on success.\n\nIf you'd like to make sure that the Webhook request comes from Telegram, we recommend using a secret path in the URL, e.g. `https://www.example.com/`. Since nobody else knows your bot's token, you can be pretty sure it's us.", + md_links: {"Update":"https://core.telegram.org/bots/api#update"}, + ), + tg_doc: "https://core.telegram.org/bots/api#setwebhook", + tg_category: "Getting updates", + params: [ + Param( + name: "url", + ty: String, + descr: Doc(md: "HTTPS url to send updates to. Use an empty string to remove webhook integration"), + ), + Param( + name: "certificate", + ty: Option(RawTy("InputFile")), + descr: Doc( + md: "Upload your public key certificate so that the root certificate in use can be checked. See our [self-signed guide] for details.", + md_links: {"self-signed guide":"https://core.telegram.org/bots/self-signed"}, + ) + ), + Param( + name: "ip_address", + ty: Option(String), + descr: Doc(md: "The fixed IP address which will be used to send webhook requests instead of the IP address resolved through DNS"), + ), + Param( + name: "max_connections", + ty: Option(u8), + descr: Doc(md: "Maximum allowed number of simultaneous HTTPS connections to the webhook for update delivery, 1-100. Defaults to 40. Use lower values to limit the load on your bot's server, and higher values to increase your bot's throughput.") + ), + Param( + name: "allowed_updates", + ty: Option(ArrayOf(RawTy("AllowedUpdate"))), + descr: Doc( + md: "A JSON-serialized list of the update types you want your bot to receive. For example, specify [“message”, “edited_channel_post”, “callback_query”] to only receive updates of these types. See [Update] for a complete list of available update types. Specify an empty list to receive all updates regardless of type (default). If not specified, the previous setting will be used.\n\nPlease note that this parameter doesn't affect updates created before the call to the setWebhook, so unwanted updates may be received for a short period of time.", + md_links: {"Update":"https://core.telegram.org/bots/api#update"}, + ), + ), + Param( + name: "drop_pending_updates", + ty: Option(bool), + descr: Doc(md: "Pass _True_ to drop all pending updates") + ), + Param( + name: "secret_token", + ty: Option(String), + descr: Doc(md: "A secret token to be sent in a header “X-Telegram-Bot-Api-Secret-Token” in every webhook request, 1-256 characters. Only characters `A-Z`, `a-z`, `0-9`, `_` and `-` are allowed. The header is useful to ensure that the request comes from a webhook set by you."), + ), + ], + notes: [ + ( + md: "You will not be able to receive updates using [getUpdates] for as long as an outgoing webhook is set up.", + md_links: {"getUpdates": "https://core.telegram.org/bots/api#getupdates"}, + ), + ( + md: "To use a self-signed certificate, you need to upload your [public key certificate] using certificate parameter. Please upload as InputFile, sending a String will not work.", + md_links: {"public key certificate":"https://core.telegram.org/bots/self-signed"} + ), + (md: "Ports currently supported for Webhooks: **443**, **80**, **88**, **8443**."), + ( + md: "If you're having any trouble setting up webhooks, please check out this [amazing guide to Webhooks].", + md_links: {"amazing guide to Webhooks": "https://core.telegram.org/bots/webhooks"} + ) + ] + ), + Method( + names: ("deleteWebhook", "DeleteWebhook", "delete_webhook"), + return_ty: True, + doc: Doc( + md: "Use this method to remove webhook integration if you decide to switch back to [getUpdates]. Returns True on success. Requires no parameters.", + md_links: {"getUpdates":"https://core.telegram.org/bots/api#getupdates"}, + ), + tg_doc: "https://core.telegram.org/bots/api#deletewebhook", + tg_category: "Getting updates", + params: [ + Param( + name: "drop_pending_updates", + ty: Option(bool), + descr: Doc(md: "Pass _True_ to drop all pending updates"), + ) + ], + ), + Method( + names: ("getWebhookInfo", "GetWebhookInfo", "get_webhook_info"), + return_ty: RawTy("WebhookInfo"), + doc: Doc( + md: "Use this method to get current webhook status. Requires no parameters. On success, returns a [WebhookInfo] object. If the bot is using [getUpdates], will return an object with the _url_ field empty.", + md_links: { + "WebhookInfo": "https://core.telegram.org/bots/api#webhookinfo", + "getUpdates": "https://core.telegram.org/bots/api#getupdates", + } + ), + tg_doc: "https://core.telegram.org/bots/api#getwebhookinfo", + tg_category: "Getting updates", + params: [], + ), + Method( + names: ("getMe", "GetMe", "get_me"), + return_ty: RawTy("Me"), + doc: Doc( + md: "A simple method for testing your bot's auth token. Requires no parameters. Returns basic information about the bot in form of a [User] object.", + md_links: {"User": "https://core.telegram.org/bots/api#user"} + ), + tg_doc: "https://core.telegram.org/bots/api#getme", + tg_category: "Available methods", + params: [], + ), + Method( + names: ("logOut", "LogOut", "log_out"), + return_ty: True, + doc: Doc(md: "Use this method to log out from the cloud Bot API server before launching the bot locally. You **must** log out the bot before running it locally, otherwise there is no guarantee that the bot will receive updates. After a successful call, you can immediately log in on a local server, but will not be able to log in back to the cloud Bot API server for 10 minutes. Returns _True_ on success. Requires no parameters."), + tg_doc: "https://core.telegram.org/bots/api#logout", + tg_category: "Available methods", + params: [], + ), + Method( + names: ("close", "Close", "close"), + return_ty: True, + doc: Doc(md: "Use this method to close the bot instance before moving it from one local server to another. You need to delete the webhook before calling this method to ensure that the bot isn't launched again after server restart. The method will return error 429 in the first 10 minutes after the bot is launched. Returns _True_ on success. Requires no parameters."), + tg_doc: "https://core.telegram.org/bots/api#close", + tg_category: "Available methods", + params: [], + ), + Method( + names: ("sendMessage", "SendMessage", "send_message"), + return_ty: RawTy("Message"), + doc: Doc( + md: "Use this method to send text messages. On success, the sent [Message] is returned.", + md_links: {"Message": "https://core.telegram.org/bots/api#message"}, + ), + tg_doc: "https://core.telegram.org/bots/api#sendmessage", + tg_category: "Available methods", + params: [ + Param( + name: "chat_id", + ty: RawTy("Recipient"), + descr: Doc(md: "Unique identifier for the target chat or username of the target channel (in the format `@channelusername`)") + ), + Param( + name: "text", + ty: String, + descr: Doc(md: "Text of the message to be sent, 1-4096 characters after entities parsing") + ), + Param( + name: "parse_mode", + ty: Option(RawTy("ParseMode")), + descr: Doc( + md: "Mode for parsing entities in the message text. See [formatting options] for more details.", + md_links: {"formatting options": "https://core.telegram.org/bots/api#formatting-options"} + ) + ), + Param( + name: "entities", + ty: Option(ArrayOf(RawTy("MessageEntity"))), + descr: Doc(md: "List of special entities that appear in the message text, which can be specified instead of _parse\\_mode_"), + ), + Param( + name: "disable_web_page_preview", + ty: Option(bool), + descr: Doc(md: "Disables link previews for links in this message") + ), + Param( + name: "disable_notification", + ty: Option(bool), + descr: Doc( + md: "Sends the message [silently]. Users will receive a notification with no sound.", + md_links: {"silently": "https://telegram.org/blog/channels-2-0#silent-messages"} + ) + ), + Param( + name: "protect_content", + ty: Option(bool), + descr: Doc(md: "Protects the contents of sent messages from forwarding and saving"), + ), + Param( + name: "reply_to_message_id", + ty: Option(RawTy("MessageId")), + descr: Doc(md: "If the message is a reply, ID of the original message") + ), + Param( + name: "allow_sending_without_reply", + ty: Option(bool), + descr: Doc(md: "Pass _True_, if the message should be sent even if the specified replied-to message is not found") + ), + Param( + name: "reply_markup", + ty: Option(RawTy("ReplyMarkup")), + descr: Doc( + md: "Additional interface options. A JSON-serialized object for an [inline keyboard], [custom reply keyboard], instructions to remove reply keyboard or to force a reply from the user.", + md_links: { + "inline keyboard": "https://core.telegram.org/bots#inline-keyboards-and-on-the-fly-updating", + "custom reply keyboard": "https://core.telegram.org/bots#keyboards", + } + ), + ), + ], + ), + Method( + names: ("forwardMessage", "ForwardMessage", "forward_message"), + return_ty: RawTy("Message"), + doc: Doc( + md: "Use this method to forward messages of any kind. On success, the sent [Message] is returned.", + md_links: {"Message": "https://core.telegram.org/bots/api#message"}, + ), + tg_doc: "https://core.telegram.org/bots/api#forwardmessage", + tg_category: "Available methods", + params: [ + Param( + name: "chat_id", + ty: RawTy("Recipient"), + descr: Doc(md: "Unique identifier for the target chat or username of the target channel (in the format `@channelusername`)"), + ), + Param( + name: "from_chat_id", + ty: RawTy("Recipient"), + descr: Doc(md: "Unique identifier for the chat where the original message was sent (or channel username in the format `@channelusername`)"), + ), + Param( + name: "disable_notification", + ty: Option(bool), + descr: Doc( + md: "Sends the message [silently]. Users will receive a notification with no sound.", + md_links: {"silently": "https://telegram.org/blog/channels-2-0#silent-messages"} + ) + ), + Param( + name: "protect_content", + ty: Option(bool), + descr: Doc(md: "Protects the contents of sent messages from forwarding and saving"), + ), + Param( + name: "message_id", + ty: RawTy("MessageId"), + descr: Doc(md: "Message identifier in the chat specified in _from\\_chat\\_id_") + ), + ], + ), + Method( + names: ("copyMessage", "CopyMessage", "copy_message"), + return_ty: RawTy("MessageId"), + doc: Doc( + md: "Use this method to copy messages of any kind. The method is analogous to the method forwardMessage, but the copied message doesn't have a link to the original message. Returns the [MessageId] of the sent message on success.", + md_links: {"MessageId": "https://core.telegram.org/bots/api#messageid"}, + ), + tg_doc: "https://core.telegram.org/bots/api#copymessage", + tg_category: "Available methods", + params: [ + Param( + name: "chat_id", + ty: RawTy("Recipient"), + descr: Doc(md: "Unique identifier for the target chat or username of the target channel (in the format `@channelusername`)"), + ), + Param( + name: "from_chat_id", + ty: RawTy("Recipient"), + descr: Doc(md: "Unique identifier for the chat where the original message was sent (or channel username in the format `@channelusername`)"), + ), + Param( + name: "message_id", + ty: RawTy("MessageId"), + descr: Doc(md: "Message identifier in the chat specified in _from\\_chat\\_id_") + ), + Param( + name: "caption", + ty: Option(String), + descr: Doc(md: "New caption for media, 0-1024 characters after entities parsing. If not specified, the original caption is kept"), + ), + Param( + name: "parse_mode", + ty: Option(RawTy("ParseMode")), + descr: Doc( + md: "Mode for parsing entities in the photo caption. See [formatting options] for more details.", + md_links: {"formatting options": "https://core.telegram.org/bots/api#formatting-options"} + ) + ), + Param( + name: "caption_entities", + ty: Option(ArrayOf(RawTy("MessageEntity"))), + descr: Doc(md: "List of special entities that appear in the new caption, which can be specified instead of _parse\\_mode_"), + ), + Param( + name: "disable_notification", + ty: Option(bool), + descr: Doc( + md: "Sends the message [silently]. Users will receive a notification with no sound.", + md_links: {"silently": "https://telegram.org/blog/channels-2-0#silent-messages"} + ) + ), + Param( + name: "protect_content", + ty: Option(bool), + descr: Doc(md: "Protects the contents of sent messages from forwarding and saving"), + ), + Param( + name: "reply_to_message_id", + ty: Option(RawTy("MessageId")), + descr: Doc(md: "If the message is a reply, ID of the original message") + ), + Param( + name: "allow_sending_without_reply", + ty: Option(bool), + descr: Doc(md: "Pass _True_, if the message should be sent even if the specified replied-to message is not found") + ), + Param( + name: "reply_markup", + ty: Option(RawTy("ReplyMarkup")), + descr: Doc( + md: "Additional interface options. A JSON-serialized object for an [inline keyboard], [custom reply keyboard], instructions to remove reply keyboard or to force a reply from the user.", + md_links: { + "inline keyboard": "https://core.telegram.org/bots#inline-keyboards-and-on-the-fly-updating", + "custom reply keyboard": "https://core.telegram.org/bots#keyboards", + }, + ), + ), + ], + ), + Method( + names: ("sendPhoto", "SendPhoto", "send_photo"), + return_ty: RawTy("Message"), + doc: Doc( + md: "Use this method to send photos. On success, the sent [Message] is returned.", + md_links: {"Message": "https://core.telegram.org/bots/api#message"}, + ), + tg_doc: "https://core.telegram.org/bots/api#sendphoto", + tg_category: "Available methods", + params: [ + Param( + name: "chat_id", + ty: RawTy("Recipient"), + descr: Doc(md: "Unique identifier for the target chat or username of the target channel (in the format `@channelusername`)") + ), + Param( + name: "photo", + ty: RawTy("InputFile"), + descr: Doc( + md: "Photo to send. Pass a file_id as String to send a photo that exists on the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get a photo from the Internet, or upload a new photo using multipart/form-data. [More info on Sending Files »]", + md_links: {"More info on Sending Files »": "https://core.telegram.org/bots/api#sending-files"}, + ) + ), + Param( + name: "caption", + ty: Option(String), + descr: Doc(md: "Photo caption (may also be used when resending photos by _file\\_id_), 0-1024 characters after entities parsing") + ), + Param( + name: "parse_mode", + ty: Option(RawTy("ParseMode")), + descr: Doc( + md: "Mode for parsing entities in the photo caption. See [formatting options] for more details.", + md_links: {"formatting options": "https://core.telegram.org/bots/api#formatting-options"} + ) + ), + Param( + name: "caption_entities", + ty: Option(ArrayOf(RawTy("MessageEntity"))), + descr: Doc(md: "List of special entities that appear in the photo caption, which can be specified instead of _parse\\_mode_"), + ), + Param( + name: "disable_notification", + ty: Option(bool), + descr: Doc( + md: "Sends the message [silently]. Users will receive a notification with no sound.", + md_links: {"silently": "https://telegram.org/blog/channels-2-0#silent-messages"} + ) + ), + Param( + name: "protect_content", + ty: Option(bool), + descr: Doc(md: "Protects the contents of sent messages from forwarding and saving"), + ), + Param( + name: "reply_to_message_id", + ty: Option(RawTy("MessageId")), + descr: Doc(md: "If the message is a reply, ID of the original message") + ), + Param( + name: "allow_sending_without_reply", + ty: Option(bool), + descr: Doc(md: "Pass _True_, if the message should be sent even if the specified replied-to message is not found") + ), + Param( + name: "reply_markup", + ty: Option(RawTy("ReplyMarkup")), + descr: Doc( + md: "Additional interface options. A JSON-serialized object for an [inline keyboard], [custom reply keyboard], instructions to remove reply keyboard or to force a reply from the user.", + md_links: { + "inline keyboard": "https://core.telegram.org/bots#inline-keyboards-and-on-the-fly-updating", + "custom reply keyboard": "https://core.telegram.org/bots#keyboards", + }, + ), + ), + ], + ), + Method( + names: ("sendAudio", "SendAudio", "send_audio"), + return_ty: RawTy("Message"), + doc: Doc( + md: "Use this method to send audio files, if you want Telegram clients to display them in the music player. Your audio must be in the .MP3 or .M4A format. On success, the sent [Message] is returned. Bots can currently send audio files of up to 50 MB in size, this limit may be changed in the future.\n\nFor sending voice messages, use the [sendVoice] method instead.", + md_links: { + "Message": "https://core.telegram.org/bots/api#message", + "sendVoice": "https://core.telegram.org/bots/api#sendvoice", + }, + ), + tg_doc: "https://core.telegram.org/bots/api#sendaudio", + tg_category: "Available methods", + params: [ + Param( + name: "chat_id", + ty: RawTy("Recipient"), + descr: Doc(md: "Unique identifier for the target chat or username of the target channel (in the format `@channelusername`)"), + ), + Param( + name: "audio", + ty: RawTy("InputFile"), + descr: Doc( + md: "Audio file to send. Pass a file_id as String to send an audio file that exists on the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get an audio file from the Internet, or upload a new one using multipart/form-data. [More info on Sending Files »]", + md_links: {"More info on Sending Files »": "https://core.telegram.org/bots/api#sending-files"}, + ), + ), + Param( + name: "caption", + ty: Option(String), + descr: Doc(md: "Audio caption, 0-1024 characters after entities parsing"), + ), + Param( + name: "parse_mode", + ty: Option(RawTy("ParseMode")), + descr: Doc( + md: "Mode for parsing entities in the audio caption. See [formatting options] for more details.", + md_links: {"formatting options": "https://core.telegram.org/bots/api#formatting-options"}, + ), + ), + Param( + name: "caption_entities", + ty: Option(ArrayOf(RawTy("MessageEntity"))), + descr: Doc(md: "List of special entities that appear in the photo caption, which can be specified instead of _parse\\_mode_"), + ), + Param( + name: "duration", + ty: Option(u32), + descr: Doc(md: "Duration of the audio in seconds"), + ), + Param( + name: "performer", + ty: Option(String), + descr: Doc(md: "Performer"), + ), + Param( + name: "title", + ty: Option(String), + descr: Doc(md: "Track name"), + ), + Param( + name: "thumb", + ty: Option(RawTy("InputFile")), + descr: Doc( + md: "Thumbnail of the file sent; can be ignored if thumbnail generation for the file is supported server-side. The thumbnail should be in JPEG format and less than 200 kB in size. A thumbnail's width and height should not exceed 320. Ignored if the file is not uploaded using multipart/form-data. Thumbnails can't be reused and can be only uploaded as a new file, so you can pass “attach://” if the thumbnail was uploaded using multipart/form-data under . [More info on Sending Files »]", + md_links: {"More info on Sending Files »": "https://core.telegram.org/bots/api#sending-files"}, + ), + ), + Param( + name: "disable_notification", + ty: Option(bool), + descr: Doc( + md: "Sends the message [silently]. Users will receive a notification with no sound.", + md_links: {"silently": "https://telegram.org/blog/channels-2-0#silent-messages"}, + ) + ), + Param( + name: "protect_content", + ty: Option(bool), + descr: Doc(md: "Protects the contents of sent messages from forwarding and saving"), + ), + Param( + name: "reply_to_message_id", + ty: Option(RawTy("MessageId")), + descr: Doc(md: "If the message is a reply, ID of the original message") + ), + Param( + name: "allow_sending_without_reply", + ty: Option(bool), + descr: Doc(md: "Pass _True_, if the message should be sent even if the specified replied-to message is not found") + ), + Param( + name: "reply_markup", + ty: Option(RawTy("ReplyMarkup")), + descr: Doc( + md: "Additional interface options. A JSON-serialized object for an [inline keyboard], [custom reply keyboard], instructions to remove reply keyboard or to force a reply from the user.", + md_links: { + "inline keyboard": "https://core.telegram.org/bots#inline-keyboards-and-on-the-fly-updating", + "custom reply keyboard": "https://core.telegram.org/bots#keyboards", + } + ), + ), + ], + ), + Method( + names: ("sendDocument", "SendDocument", "send_document"), + return_ty: RawTy("Message"), + doc: Doc( + md: "Use this method to send general files. On success, the sent [Message] is returned. Bots can currently send files of any type of up to 50 MB in size, this limit may be changed in the future.", + md_links: { "Message": "https://core.telegram.org/bots/api#message" }, + ), + tg_doc: "https://core.telegram.org/bots/api#senddocument", + tg_category: "Available methods", + params: [ + Param( + name: "chat_id", + ty: RawTy("Recipient"), + descr: Doc(md: "Unique identifier for the target chat or username of the target channel (in the format `@channelusername`)"), + ), + Param( + name: "document", + ty: RawTy("InputFile"), + descr: Doc( + md: "File to send. Pass a file_id as String to send a file that exists on the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get a file from the Internet, or upload a new one using multipart/form-data. [More info on Sending Files »]", + md_links: {"More info on Sending Files »": "https://core.telegram.org/bots/api#sending-files"}, + ), + ), + Param( + name: "thumb", + ty: Option(RawTy("InputFile")), + descr: Doc( + md: "Thumbnail of the file sent; can be ignored if thumbnail generation for the file is supported server-side. The thumbnail should be in JPEG format and less than 200 kB in size. A thumbnail's width and height should not exceed 320. Ignored if the file is not uploaded using multipart/form-data. Thumbnails can't be reused and can be only uploaded as a new file, so you can pass “attach://” if the thumbnail was uploaded using multipart/form-data under . [More info on Sending Files »]", + md_links: {"More info on Sending Files »": "https://core.telegram.org/bots/api#sending-files"}, + ), + ), + Param( + name: "caption", + ty: Option(String), + descr: Doc(md: "Document caption (may also be used when resending documents by _file\\_id_), 0-1024 characters after entities parsing"), + ), + Param( + name: "parse_mode", + ty: Option(RawTy("ParseMode")), + descr: Doc( + md: "Mode for parsing entities in the audio caption. See [formatting options] for more details.", + md_links: {"formatting options": "https://core.telegram.org/bots/api#formatting-options"}, + ), + ), + Param( + name: "caption_entities", + ty: Option(ArrayOf(RawTy("MessageEntity"))), + descr: Doc(md: "List of special entities that appear in the photo caption, which can be specified instead of _parse\\_mode_"), + ), + Param( + name: "disable_content_type_detection", + ty: Option(bool), + descr: Doc(md: "Disables automatic server-side content type detection for files uploaded using multipart/form-data."), + ), + Param( + name: "disable_notification", + ty: Option(bool), + descr: Doc( + md: "Sends the message [silently]. Users will receive a notification with no sound.", + md_links: {"silently": "https://telegram.org/blog/channels-2-0#silent-messages"}, + ) + ), + Param( + name: "protect_content", + ty: Option(bool), + descr: Doc(md: "Protects the contents of sent messages from forwarding and saving"), + ), + Param( + name: "reply_to_message_id", + ty: Option(RawTy("MessageId")), + descr: Doc(md: "If the message is a reply, ID of the original message") + ), + Param( + name: "allow_sending_without_reply", + ty: Option(bool), + descr: Doc(md: "Pass _True_, if the message should be sent even if the specified replied-to message is not found") + ), + Param( + name: "reply_markup", + ty: Option(RawTy("ReplyMarkup")), + descr: Doc( + md: "Additional interface options. A JSON-serialized object for an [inline keyboard], [custom reply keyboard], instructions to remove reply keyboard or to force a reply from the user.", + md_links: { + "inline keyboard": "https://core.telegram.org/bots#inline-keyboards-and-on-the-fly-updating", + "custom reply keyboard": "https://core.telegram.org/bots#keyboards", + } + ), + ), + ], + ), + Method( + names: ("sendVideo", "SendVideo", "send_video"), + return_ty: RawTy("Message"), + doc: Doc( + md: "Use this method to send video files, Telegram clients support mp4 videos (other formats may be sent as [Document]). On success, the sent [Message] is returned. Bots can currently send video files of up to 50 MB in size, this limit may be changed in the future.", + md_links: { + "Document": "https://core.telegram.org/bots/api#document", + "Message": "https://core.telegram.org/bots/api#message", + }, + ), + tg_doc: "https://core.telegram.org/bots/api#sendvideo", + tg_category: "Available methods", + params: [ + Param( + name: "chat_id", + ty: RawTy("Recipient"), + descr: Doc(md: "Unique identifier for the target chat or username of the target channel (in the format `@channelusername`)"), + ), + Param( + name: "video", + ty: RawTy("InputFile"), + descr: Doc( + md: "Video to send. Pass a file_id as String to send a video that exists on the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get a video from the Internet, or upload a new video using multipart/form-data. [More info on Sending Files »]", + md_links: {"More info on Sending Files »": "https://core.telegram.org/bots/api#sending-files"}, + ), + ), + Param( + name: "duration", + ty: Option(u32), + descr: Doc(md: "Duration of the video in seconds"), + ), + Param( + name: "width", + ty: Option(u32), + descr: Doc(md: "Video width"), + ), + Param( + name: "height", + ty: Option(u32), + descr: Doc(md: "Video height"), + ), + Param( + name: "thumb", + ty: Option(RawTy("InputFile")), + descr: Doc( + md: "Thumbnail of the file sent; can be ignored if thumbnail generation for the file is supported server-side. The thumbnail should be in JPEG format and less than 200 kB in size. A thumbnail's width and height should not exceed 320. Ignored if the file is not uploaded using multipart/form-data. Thumbnails can't be reused and can be only uploaded as a new file, so you can pass “attach://” if the thumbnail was uploaded using multipart/form-data under . [More info on Sending Files »]", + md_links: {"More info on Sending Files »": "https://core.telegram.org/bots/api#sending-files"}, + ), + ), + Param( + name: "caption", + ty: Option(String), + descr: Doc(md: "Video caption (may also be used when resending videos by _file\\_id_), 0-1024 characters after entities parsing"), + ), + Param( + name: "parse_mode", + ty: Option(RawTy("ParseMode")), + descr: Doc( + md: "Mode for parsing entities in the video caption. See [formatting options] for more details.", + md_links: {"formatting options": "https://core.telegram.org/bots/api#formatting-options"}, + ), + ), + Param( + name: "caption_entities", + ty: Option(ArrayOf(RawTy("MessageEntity"))), + descr: Doc(md: "List of special entities that appear in the caption, which can be specified instead of _parse\\_mode_"), + ), + Param( + name: "supports_streaming", + ty: Option(bool), + descr: Doc(md: "Pass _True_, if the uploaded video is suitable for streaming"), + ), + Param( + name: "disable_notification", + ty: Option(bool), + descr: Doc( + md: "Sends the message [silently]. Users will receive a notification with no sound.", + md_links: {"silently": "https://telegram.org/blog/channels-2-0#silent-messages"}, + ) + ), + Param( + name: "protect_content", + ty: Option(bool), + descr: Doc(md: "Protects the contents of sent messages from forwarding and saving"), + ), + Param( + name: "reply_to_message_id", + ty: Option(RawTy("MessageId")), + descr: Doc(md: "If the message is a reply, ID of the original message") + ), + Param( + name: "allow_sending_without_reply", + ty: Option(bool), + descr: Doc(md: "Pass _True_, if the message should be sent even if the specified replied-to message is not found") + ), + Param( + name: "reply_markup", + ty: Option(RawTy("ReplyMarkup")), + descr: Doc( + md: "Additional interface options. A JSON-serialized object for an [inline keyboard], [custom reply keyboard], instructions to remove reply keyboard or to force a reply from the user.", + md_links: { + "inline keyboard": "https://core.telegram.org/bots#inline-keyboards-and-on-the-fly-updating", + "custom reply keyboard": "https://core.telegram.org/bots#keyboards", + } + ), + + ), + ], + ), + Method( + names: ("sendAnimation", "SendAnimation", "send_animation"), + return_ty: RawTy("Message"), + doc: Doc( + md: "Use this method to send animation files (GIF or H.264/MPEG-4 AVC video without sound). On success, the sent [Message] is returned. Bots can currently send animation files of up to 50 MB in size, this limit may be changed in the future.", + md_links: { "Message": "https://core.telegram.org/bots/api#message" }, + ), + tg_doc: "https://core.telegram.org/bots/api#sendanimation", + tg_category: "Available methods", + params: [ + Param( + name: "chat_id", + ty: RawTy("Recipient"), + descr: Doc(md: "Unique identifier for the target chat or username of the target channel (in the format `@channelusername`)"), + ), + Param( + name: "animation", + ty: RawTy("InputFile"), + descr: Doc( + md: "Animation to send. Pass a file_id as String to send a video that exists on the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get a video from the Internet, or upload a new video using multipart/form-data. [More info on Sending Files »]", + md_links: {"More info on Sending Files »": "https://core.telegram.org/bots/api#sending-files"}, + ), + ), + Param( + name: "duration", + ty: Option(u32), + descr: Doc(md: "Duration of the animation in seconds"), + ), + Param( + name: "width", + ty: Option(u32), + descr: Doc(md: "Animation width"), + ), + Param( + name: "height", + ty: Option(u32), + descr: Doc(md: "Animation height"), + ), + Param( + name: "thumb", + ty: Option(RawTy("InputFile")), + descr: Doc( + md: "Thumbnail of the file sent; can be ignored if thumbnail generation for the file is supported server-side. The thumbnail should be in JPEG format and less than 200 kB in size. A thumbnail's width and height should not exceed 320. Ignored if the file is not uploaded using multipart/form-data. Thumbnails can't be reused and can be only uploaded as a new file, so you can pass “attach://” if the thumbnail was uploaded using multipart/form-data under . [More info on Sending Files »]", + md_links: {"More info on Sending Files »": "https://core.telegram.org/bots/api#sending-files"}, + ), + ), + Param( + name: "caption", + ty: Option(String), + descr: Doc(md: "Animation caption (may also be used when resending videos by _file\\_id_), 0-1024 characters after entities parsing"), + ), + Param( + name: "parse_mode", + ty: Option(RawTy("ParseMode")), + descr: Doc( + md: "Mode for parsing entities in the animation caption. See [formatting options] for more details.", + md_links: {"formatting options": "https://core.telegram.org/bots/api#formatting-options"}, + ), + ), + Param( + name: "caption_entities", + ty: Option(ArrayOf(RawTy("MessageEntity"))), + descr: Doc(md: "List of special entities that appear in the photo caption, which can be specified instead of _parse\\_mode_"), + ), + Param( + name: "disable_notification", + ty: Option(bool), + descr: Doc( + md: "Sends the message [silently]. Users will receive a notification with no sound.", + md_links: {"silently": "https://telegram.org/blog/channels-2-0#silent-messages"}, + ) + ), + Param( + name: "protect_content", + ty: Option(bool), + descr: Doc(md: "Protects the contents of sent messages from forwarding and saving"), + ), + Param( + name: "reply_to_message_id", + ty: Option(RawTy("MessageId")), + descr: Doc(md: "If the message is a reply, ID of the original message") + ), + Param( + name: "allow_sending_without_reply", + ty: Option(bool), + descr: Doc(md: "Pass _True_, if the message should be sent even if the specified replied-to message is not found") + ), + Param( + name: "reply_markup", + ty: Option(RawTy("ReplyMarkup")), + descr: Doc( + md: "Additional interface options. A JSON-serialized object for an [inline keyboard], [custom reply keyboard], instructions to remove reply keyboard or to force a reply from the user.", + md_links: { + "inline keyboard": "https://core.telegram.org/bots#inline-keyboards-and-on-the-fly-updating", + "custom reply keyboard": "https://core.telegram.org/bots#keyboards", + } + ), + ), + ], + ), + Method( + names: ("sendVoice", "SendVoice", "send_voice"), + return_ty: RawTy("Message"), + doc: Doc( + md: "Use this method to send audio files, if you want Telegram clients to display the file as a playable voice message. For this to work, your audio must be in an .OGG file encoded with OPUS (other formats may be sent as [Audio] or [Document]). On success, the sent [Message] is returned. Bots can currently send voice messages of up to 50 MB in size, this limit may be changed in the future.", + md_links: { + "Audio": "https://core.telegram.org/bots/api#audio", + "Document": "https://core.telegram.org/bots/api#document", + "Message": "https://core.telegram.org/bots/api#message", + }, + ), + tg_doc: "https://core.telegram.org/bots/api#sendaudio", + tg_category: "Available methods", + params: [ + Param( + name: "chat_id", + ty: RawTy("Recipient"), + descr: Doc(md: "Unique identifier for the target chat or username of the target channel (in the format `@channelusername`)"), + ), + Param( + name: "voice", + ty: RawTy("InputFile"), + descr: Doc( + md: "Audio file to send. Pass a file_id as String to send an audio file that exists on the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get an audio file from the Internet, or upload a new one using multipart/form-data. [More info on Sending Files »]", + md_links: {"More info on Sending Files »": "https://core.telegram.org/bots/api#sending-files"}, + ), + ), + Param( + name: "caption", + ty: Option(String), + descr: Doc(md: "Voice message caption, 0-1024 characters after entities parsing"), + ), + Param( + name: "parse_mode", + ty: Option(RawTy("ParseMode")), + descr: Doc( + md: "Mode for parsing entities in the voice message caption. See [formatting options] for more details.", + md_links: {"formatting options": "https://core.telegram.org/bots/api#formatting-options"}, + ), + ), + Param( + name: "caption_entities", + ty: Option(ArrayOf(RawTy("MessageEntity"))), + descr: Doc(md: "List of special entities that appear in the photo caption, which can be specified instead of _parse\\_mode_"), + ), + Param( + name: "duration", + ty: Option(u32), + descr: Doc(md: "Duration of the voice message in seconds"), + ), + Param( + name: "disable_notification", + ty: Option(bool), + descr: Doc( + md: "Sends the message [silently]. Users will receive a notification with no sound.", + md_links: {"silently": "https://telegram.org/blog/channels-2-0#silent-messages"}, + ) + ), + Param( + name: "reply_to_message_id", + ty: Option(RawTy("MessageId")), + descr: Doc(md: "If the message is a reply, ID of the original message") + ), + Param( + name: "allow_sending_without_reply", + ty: Option(bool), + descr: Doc(md: "Pass _True_, if the message should be sent even if the specified replied-to message is not found") + ), + Param( + name: "reply_markup", + ty: Option(RawTy("ReplyMarkup")), + descr: Doc( + md: "Additional interface options. A JSON-serialized object for an [inline keyboard], [custom reply keyboard], instructions to remove reply keyboard or to force a reply from the user.", + md_links: { + "inline keyboard": "https://core.telegram.org/bots#inline-keyboards-and-on-the-fly-updating", + "custom reply keyboard": "https://core.telegram.org/bots#keyboards", + } + ), + ), + ], + ), + Method( + names: ("sendVideoNote", "SendVideoNote", "send_video_note"), + return_ty: RawTy("Message"), + doc: Doc( + md: "As of [v.4.0], Telegram clients support rounded square mp4 videos of up to 1 minute long. Use this method to send video messages. On success, the sent [Message] is returned.", + md_links: { + "v.4.0": "https://core.telegram.org/bots/api#document", + "Message": "https://core.telegram.org/bots/api#message", + }, + ), + tg_doc: "https://core.telegram.org/bots/api#sendvideonote", + tg_category: "Available methods", + params: [ + Param( + name: "chat_id", + ty: RawTy("Recipient"), + descr: Doc(md: "Unique identifier for the target chat or username of the target channel (in the format `@channelusername`)"), + ), + Param( + name: "video_note", + ty: RawTy("InputFile"), + descr: Doc( + md: "Video note to send. Pass a file_id as String to send a video note that exists on the Telegram servers (recommended) or upload a new video using multipart/form-data. [More info on Sending Files »]. Sending video notes by a URL is currently unsupported", + md_links: {"More info on Sending Files »": "https://core.telegram.org/bots/api#sending-files"}, + ), + ), + Param( + name: "duration", + ty: Option(u32), + descr: Doc(md: "Duration of the video in seconds"), + ), + Param( + name: "length", + ty: Option(u32), + descr: Doc(md: "Video width and height, i.e. diameter of the video message"), + ), + Param( + name: "thumb", + ty: Option(RawTy("InputFile")), + descr: Doc( + md: "Thumbnail of the file sent; can be ignored if thumbnail generation for the file is supported server-side. The thumbnail should be in JPEG format and less than 200 kB in size. A thumbnail's width and height should not exceed 320. Ignored if the file is not uploaded using multipart/form-data. Thumbnails can't be reused and can be only uploaded as a new file, so you can pass “attach://” if the thumbnail was uploaded using multipart/form-data under . [More info on Sending Files »]", + md_links: {"More info on Sending Files »": "https://core.telegram.org/bots/api#sending-files"}, + ), + ), + Param( + name: "disable_notification", + ty: Option(bool), + descr: Doc( + md: "Sends the message [silently]. Users will receive a notification with no sound.", + md_links: {"silently": "https://telegram.org/blog/channels-2-0#silent-messages"}, + ) + ), + Param( + name: "protect_content", + ty: Option(bool), + descr: Doc(md: "Protects the contents of sent messages from forwarding and saving"), + ), + Param( + name: "reply_to_message_id", + ty: Option(RawTy("MessageId")), + descr: Doc(md: "If the message is a reply, ID of the original message") + ), + Param( + name: "allow_sending_without_reply", + ty: Option(bool), + descr: Doc(md: "Pass _True_, if the message should be sent even if the specified replied-to message is not found") + ), + Param( + name: "reply_markup", + ty: Option(RawTy("ReplyMarkup")), + descr: Doc( + md: "Additional interface options. A JSON-serialized object for an [inline keyboard], [custom reply keyboard], instructions to remove reply keyboard or to force a reply from the user.", + md_links: { + "inline keyboard": "https://core.telegram.org/bots#inline-keyboards-and-on-the-fly-updating", + "custom reply keyboard": "https://core.telegram.org/bots#keyboards", + } + ), + + ), + ], + ), + Method( + names: ("sendMediaGroup", "SendMediaGroup", "send_media_group"), + return_ty: ArrayOf(RawTy("Message")), + doc: Doc( + md: "Use this method to send a group of photos, videos, documents or audios as an album. Documents and audio files can be only grouped in an album with messages of the same type. On success, an array of [Message]s that were sent is returned.", + md_links: {"Message": "https://core.telegram.org/bots/api#message"}, + ), + tg_doc: "https://core.telegram.org/bots/api#sendmediagroup", + tg_category: "Available methods", + params: [ + Param( + name: "chat_id", + ty: RawTy("Recipient"), + descr: Doc(md: "Unique identifier for the target chat or username of the target channel (in the format `@channelusername`)") + ), + Param( + name: "media", + ty: ArrayOf(RawTy("InputMedia")), + descr: Doc(md: "A JSON-serialized array describing messages to be sent, must include 2-10 items") + ), + Param( + name: "disable_notification", + ty: Option(bool), + descr: Doc( + md: "Sends the message [silently]. Users will receive a notification with no sound.", + md_links: {"silently": "https://telegram.org/blog/channels-2-0#silent-messages"} + ) + ), + Param( + name: "protect_content", + ty: Option(bool), + descr: Doc(md: "Protects the contents of sent messages from forwarding and saving"), + ), + Param( + name: "reply_to_message_id", + ty: Option(RawTy("MessageId")), + descr: Doc(md: "If the message is a reply, ID of the original message") + ), + Param( + name: "allow_sending_without_reply", + ty: Option(bool), + descr: Doc(md: "Pass _True_, if the message should be sent even if the specified replied-to message is not found") + ), + ], + ), + Method( + names: ("sendLocation", "SendLocation", "send_location"), + return_ty: RawTy("Message"), + doc: Doc( + md: "Use this method to send point on the map. On success, the sent [Message] is returned.", + md_links: {"Message": "https://core.telegram.org/bots/api#message"}, + ), + tg_doc: "https://core.telegram.org/bots/api#sendlocation", + tg_category: "Available methods", + params: [ + Param( + name: "chat_id", + ty: RawTy("Recipient"), + descr: Doc(md: "Unique identifier for the target chat or username of the target channel (in the format `@channelusername`)"), + ), + Param( + name: "latitude", + ty: f64, + descr: Doc(md: "Latitude of the location"), + ), + Param( + name: "longitude", + ty: f64, + descr: Doc(md: "Longitude of the location"), + ), + Param( + name: "horizontal_accuracy", + ty: Option(f64), + descr: Doc(md: "The radius of uncertainty for the location, measured in meters; 0-1500"), + ), + Param( + name: "live_period", + ty: Option(u32), + descr: Doc( + md: "Period in seconds for which the location will be updated (see [Live Locations], should be between 60 and 86400.", + md_links: {"Live Locations": "https://telegram.org/blog/live-locations"} + ) + ), + Param( + name: "heading", + ty: Option(u16), + descr: Doc(md: "For live locations, a direction in which the user is moving, in degrees. Must be between 1 and 360 if specified.") + ), + Param( + name: "proximity_alert_radius", + ty: Option(u32), + descr: Doc(md: "For live locations, a maximum distance for proximity alerts about approaching another chat member, in meters. Must be between 1 and 100000 if specified.") + ), + Param( + name: "disable_notification", + ty: Option(bool), + descr: Doc( + md: "Sends the message [silently]. Users will receive a notification with no sound.", + md_links: {"silently": "https://telegram.org/blog/channels-2-0#silent-messages"} + ) + ), + Param( + name: "protect_content", + ty: Option(bool), + descr: Doc(md: "Protects the contents of sent messages from forwarding and saving"), + ), + Param( + name: "reply_to_message_id", + ty: Option(RawTy("MessageId")), + descr: Doc(md: "If the message is a reply, ID of the original message") + ), + Param( + name: "allow_sending_without_reply", + ty: Option(bool), + descr: Doc(md: "Pass _True_, if the message should be sent even if the specified replied-to message is not found") + ), + Param( + name: "reply_markup", + ty: Option(RawTy("ReplyMarkup")), + descr: Doc( + md: "Additional interface options. A JSON-serialized object for an [inline keyboard], [custom reply keyboard], instructions to remove reply keyboard or to force a reply from the user.", + md_links: { + "inline keyboard": "https://core.telegram.org/bots#inline-keyboards-and-on-the-fly-updating", + "custom reply keyboard": "https://core.telegram.org/bots#keyboards", + } + ), + ), + ], + ), + Method( + names: ("editMessageLiveLocation", "EditMessageLiveLocation", "edit_message_live_location"), + return_ty: RawTy("Message"), + doc: Doc( + md: "Use this method to edit live location messages. A location can be edited until its live_period expires or editing is explicitly disabled by a call to [stopMessageLiveLocation]. On success, the edited Message is returned.", + md_links: {"stopMessageLiveLocation": "https://core.telegram.org/bots/api#stopmessagelivelocation"}, + ), + tg_doc: "https://core.telegram.org/bots/api#editmessagelivelocation", + tg_category: "Available methods", + params: [ + Param( + name: "chat_id", + ty: RawTy("Recipient"), + descr: Doc(md: "Unique identifier for the target chat or username of the target channel (in the format `@channelusername`)") + ), + Param( + name: "message_id", + ty: RawTy("MessageId"), + descr: Doc(md: "Identifier of the message to edit") + ), + Param( + name: "latitude", + ty: f64, + descr: Doc(md: "Latitude of new location"), + ), + Param( + name: "longitude", + ty: f64, + descr: Doc(md: "Longitude of new location"), + ), + Param( + name: "horizontal_accuracy", + ty: Option(f64), + descr: Doc(md: "The radius of uncertainty for the location, measured in meters; 0-1500"), + ), + Param( + name: "heading", + ty: Option(u16), + descr: Doc(md: "For live locations, a direction in which the user is moving, in degrees. Must be between 1 and 360 if specified.") + ), + Param( + name: "proximity_alert_radius", + ty: Option(u32), + descr: Doc(md: "For live locations, a maximum distance for proximity alerts about approaching another chat member, in meters. Must be between 1 and 100000 if specified.") + ), + Param( + name: "reply_markup", + ty: Option(RawTy("ReplyMarkup")), + descr: Doc( + md: "Additional interface options. A JSON-serialized object for an [inline keyboard], [custom reply keyboard], instructions to remove reply keyboard or to force a reply from the user.", + md_links: { + "inline keyboard": "https://core.telegram.org/bots#inline-keyboards-and-on-the-fly-updating", + "custom reply keyboard": "https://core.telegram.org/bots#keyboards", + } + ), + ), + ], + sibling: Some("editMessageLiveLocationInline"), + ), + Method( + names: ("editMessageLiveLocationInline", "EditMessageLiveLocationInline", "edit_message_live_location_inline"), + return_ty: RawTy("Message"), + doc: Doc( + md: "Use this method to edit live location messages. A location can be edited until its live_period expires or editing is explicitly disabled by a call to [stopMessageLiveLocation]. On success, True is returned.", + md_links: {"stopMessageLiveLocation": "https://core.telegram.org/bots/api#stopmessagelivelocation"}, + ), + tg_doc: "https://core.telegram.org/bots/api#editmessagelivelocation", + tg_category: "Available methods", + params: [ + Param( + name: "inline_message_id", + ty: String, + descr: Doc(md: "Identifier of the inline message"), + ), + Param( + name: "latitude", + ty: f64, + descr: Doc(md: "Latitude of new location"), + ), + Param( + name: "longitude", + ty: f64, + descr: Doc(md: "Longitude of new location"), + ), + Param( + name: "horizontal_accuracy", + ty: Option(f64), + descr: Doc(md: "The radius of uncertainty for the location, measured in meters; 0-1500"), + ), + Param( + name: "heading", + ty: Option(u16), + descr: Doc(md: "For live locations, a direction in which the user is moving, in degrees. Must be between 1 and 360 if specified.") + ), + Param( + name: "proximity_alert_radius", + ty: Option(u32), + descr: Doc(md: "For live locations, a maximum distance for proximity alerts about approaching another chat member, in meters. Must be between 1 and 100000 if specified.") + ), + Param( + name: "reply_markup", + ty: Option(RawTy("ReplyMarkup")), + descr: Doc( + md: "Additional interface options. A JSON-serialized object for an [inline keyboard], [custom reply keyboard], instructions to remove reply keyboard or to force a reply from the user.", + md_links: { + "inline keyboard": "https://core.telegram.org/bots#inline-keyboards-and-on-the-fly-updating", + "custom reply keyboard": "https://core.telegram.org/bots#keyboards", + } + ), + ), + ], + sibling: Some("editMessageLiveLocation"), + ), + Method( + names: ("stopMessageLiveLocation", "StopMessageLiveLocation", "stop_message_live_location"), + return_ty: RawTy("Message"), + doc: Doc( + md: "Use this method to edit live location messages. A location can be edited until its live_period expires or editing is explicitly disabled by a call to [stopMessageLiveLocation]. On success, the edited Message is returned.", + md_links: { + "Message": "https://core.telegram.org/bots/api#message", + "stopMessageLiveLocation": "https://core.telegram.org/bots/api#stopmessagelivelocation", + }, + ), + tg_doc: "https://core.telegram.org/bots/api#editmessagelivelocation", + tg_category: "Available methods", + params: [ + Param( + name: "chat_id", + ty: RawTy("Recipient"), + descr: Doc(md: "Unique identifier for the target chat or username of the target channel (in the format `@channelusername`)") + ), + Param( + name: "message_id", + ty: RawTy("MessageId"), + descr: Doc(md: "Identifier of the message to edit") + ), + Param( + name: "latitude", + ty: f64, + descr: Doc(md: "Latitude of new location"), + ), + Param( + name: "longitude", + ty: f64, + descr: Doc(md: "Longitude of new location"), + ), + Param( + name: "reply_markup", + ty: Option(RawTy("ReplyMarkup")), + descr: Doc( + md: "Additional interface options. A JSON-serialized object for an [inline keyboard], [custom reply keyboard], instructions to remove reply keyboard or to force a reply from the user.", + md_links: { + "inline keyboard": "https://core.telegram.org/bots#inline-keyboards-and-on-the-fly-updating", + "custom reply keyboard": "https://core.telegram.org/bots#keyboards", + } + ), + ), + ], + sibling: Some("stopMessageLiveLocationInline"), + ), + Method( + names: ("stopMessageLiveLocationInline", "StopMessageLiveLocationInline", "stop_message_live_location_inline"), + return_ty: RawTy("Message"), + doc: Doc( + md: "Use this method to edit live location messages. A location can be edited until its live_period expires or editing is explicitly disabled by a call to [stopMessageLiveLocation]. On success, True is returned.", + md_links: {"stopMessageLiveLocation": "https://core.telegram.org/bots/api#stopmessagelivelocation"}, + ), + tg_doc: "https://core.telegram.org/bots/api#editmessagelivelocation", + tg_category: "Available methods", + params: [ + Param( + name: "inline_message_id", + ty: String, + descr: Doc(md: "Identifier of the inline message"), + ), + Param( + name: "latitude", + ty: f64, + descr: Doc(md: "Latitude of new location"), + ), + Param( + name: "longitude", + ty: f64, + descr: Doc(md: "Longitude of new location"), + ), + Param( + name: "reply_markup", + ty: Option(RawTy("ReplyMarkup")), + descr: Doc( + md: "Additional interface options. A JSON-serialized object for an [inline keyboard], [custom reply keyboard], instructions to remove reply keyboard or to force a reply from the user.", + md_links: { + "inline keyboard": "https://core.telegram.org/bots#inline-keyboards-and-on-the-fly-updating", + "custom reply keyboard": "https://core.telegram.org/bots#keyboards", + } + ), + ), + ], + sibling: Some("stopMessageLiveLocation"), + ), + Method( + names: ("sendVenue", "SendVenue", "send_venue"), + return_ty: RawTy("Message"), + doc: Doc( + md: "Use this method to send information about a venue. On success, the sent [Message] is returned.", + md_links: {"Message": "https://core.telegram.org/bots/api#message"}, + ), + tg_doc: "https://core.telegram.org/bots/api#sendvenue", + tg_category: "Available methods", + params: [ + Param( + name: "chat_id", + ty: RawTy("Recipient"), + descr: Doc(md: "Unique identifier for the target chat or username of the target channel (in the format `@channelusername`)") + ), + Param( + name: "latitude", + ty: f64, + descr: Doc(md: "Latitude of new location"), + ), + Param( + name: "longitude", + ty: f64, + descr: Doc(md: "Longitude of new location"), + ), + Param( + name: "title", + ty: String, + descr: Doc(md: "Name of the venue") + ), + Param( + name: "address", + ty: String, + descr: Doc(md: "Address of the venue") + ), + Param( + name: "foursquare_id", + ty: Option(String), + descr: Doc(md: "Foursquare identifier of the venue") + ), + Param( + name: "foursquare_type", + ty: Option(String), + descr: Doc(md: "Foursquare type of the venue, if known. (For example, “arts_entertainment/default”, “arts_entertainment/aquarium” or “food/icecream”.)") + ), + Param( + name: "google_place_id", + ty: Option(String), + descr: Doc(md: "Google Places identifier of the venue") + ), + Param( + name: "google_place_type", + ty: Option(String), + descr: Doc( + md: "Google Places type of the venue. (See [supported types].)", + md_links: {"supported types":"https://developers.google.com/places/web-service/supported_types"}, + ), + ), + Param( + name: "disable_notification", + ty: Option(bool), + descr: Doc( + md: "Sends the message [silently]. Users will receive a notification with no sound.", + md_links: {"silently": "https://telegram.org/blog/channels-2-0#silent-messages"} + ) + ), + Param( + name: "protect_content", + ty: Option(bool), + descr: Doc(md: "Protects the contents of sent messages from forwarding and saving"), + ), + Param( + name: "reply_to_message_id", + ty: Option(RawTy("MessageId")), + descr: Doc(md: "If the message is a reply, ID of the original message") + ), + Param( + name: "allow_sending_without_reply", + ty: Option(bool), + descr: Doc(md: "Pass _True_, if the message should be sent even if the specified replied-to message is not found") + ), + Param( + name: "reply_markup", + ty: Option(RawTy("ReplyMarkup")), + descr: Doc( + md: "Additional interface options. A JSON-serialized object for an [inline keyboard], [custom reply keyboard], instructions to remove reply keyboard or to force a reply from the user.", + md_links: { + "inline keyboard": "https://core.telegram.org/bots#inline-keyboards-and-on-the-fly-updating", + "custom reply keyboard": "https://core.telegram.org/bots#keyboards", + } + ), + ), + ], + ), + Method( + names: ("sendContact", "SendContact", "send_contact"), + return_ty: RawTy("Message"), + doc: Doc( + md: "Use this method to send phone contacts. On success, the sent [Message] is returned.", + md_links: {"Message": "https://core.telegram.org/bots/api#message"}, + ), + tg_doc: "https://core.telegram.org/bots/api#sendcontact", + tg_category: "Available methods", + params: [ + Param( + name: "chat_id", + ty: RawTy("Recipient"), + descr: Doc(md: "Unique identifier for the target chat or username of the target channel (in the format `@channelusername`)") + ), + Param( + name: "phone_number", + ty: String, + descr: Doc(md: "Contact's phone number"), + ), + Param( + name: "first_name", + ty: String, + descr: Doc(md: "Contact's first name"), + ), + Param( + name: "last_name", + ty: Option(String), + descr: Doc(md: "Contact's last name") + ), + Param( + name: "vcard", + ty: Option(String), + descr: Doc( + md: "Additional data about the contact in the form of a [vCard], 0-2048 bytes", + md_links: {"vCard": "https://en.wikipedia.org/wiki/VCard"} + ) + ), + Param( + name: "disable_notification", + ty: Option(bool), + descr: Doc( + md: "Sends the message [silently]. Users will receive a notification with no sound.", + md_links: {"silently": "https://telegram.org/blog/channels-2-0#silent-messages"} + ) + ), + Param( + name: "protect_content", + ty: Option(bool), + descr: Doc(md: "Protects the contents of sent messages from forwarding and saving"), + ), + Param( + name: "reply_to_message_id", + ty: Option(RawTy("MessageId")), + descr: Doc(md: "If the message is a reply, ID of the original message") + ), + Param( + name: "allow_sending_without_reply", + ty: Option(bool), + descr: Doc(md: "Pass _True_, if the message should be sent even if the specified replied-to message is not found") + ), + Param( + name: "reply_markup", + ty: Option(RawTy("ReplyMarkup")), + descr: Doc( + md: "Additional interface options. A JSON-serialized object for an [inline keyboard], [custom reply keyboard], instructions to remove reply keyboard or to force a reply from the user.", + md_links: { + "inline keyboard": "https://core.telegram.org/bots#inline-keyboards-and-on-the-fly-updating", + "custom reply keyboard": "https://core.telegram.org/bots#keyboards", + } + ), + ), + ], + ), + Method( + names: ("sendPoll", "SendPoll", "send_poll"), + return_ty: RawTy("Message"), + doc: Doc( + md: "Use this method to send phone contacts. On success, the sent [Message] is returned.", + md_links: {"Message": "https://core.telegram.org/bots/api#message"}, + ), + tg_doc: "https://core.telegram.org/bots/api#sendpoll", + tg_category: "Available methods", + params: [ + Param( + name: "chat_id", + ty: RawTy("Recipient"), + descr: Doc(md: "Unique identifier for the target chat or username of the target channel (in the format `@channelusername`)") + ), + Param( + name: "question", + ty: String, + descr: Doc(md: "Poll question, 1-300 characters"), + ), + Param( + name: "options", + ty: ArrayOf(String), + descr: Doc(md: "A JSON-serialized list of answer options, 2-10 strings 1-100 characters each"), + ), + Param( + name: "is_anonymous", + ty: Option(bool), + descr: Doc(md: "True, if the poll needs to be anonymous, defaults to True") + ), + Param( + name: "type", + ty: Option(RawTy("PollType")), + descr: Doc(md: "Poll type, “quiz” or “regular”, defaults to “regular”") + ), + Param( + name: "allows_multiple_answers", + ty: Option(bool), + descr: Doc(md: "True, if the poll allows multiple answers, ignored for polls in quiz mode, defaults to False") + ), + Param( + name: "correct_option_id", + ty: Option(u8), + descr: Doc(md: "0-based identifier of the correct answer option, required for polls in quiz mode") + ), + Param( + name: "explanation", + ty: Option(String), + descr: Doc(md: "Text that is shown when a user chooses an incorrect answer or taps on the lamp icon in a quiz-style poll, 0-200 characters with at most 2 line feeds after entities parsing") + ), + Param( + name: "explanation_parse_mode", + ty: Option(RawTy("ParseMode")), + descr: Doc( + md: "Mode for parsing entities in the message text. See [formatting options] for more details.", + md_links: {"formatting options": "https://core.telegram.org/bots/api#formatting-options"} + ) + ), + Param( + name: "explanation_entities", + ty: Option(ArrayOf(RawTy("MessageEntity"))), + descr: Doc(md: "List of special entities that appear in the poll explanation, which can be specified instead of _parse\\_mode_"), + ), + Param( + name: "open_period", + ty: Option(u16), + descr: Doc(md: "Amount of time in seconds the poll will be active after creation, 5-600. Can't be used together with close_date.") + ), + Param( + name: "close_date", + ty: Option(u64), + descr: Doc(md: "Point in time (Unix timestamp) when the poll will be automatically closed. Must be at least 5 and no more than 600 seconds in the future. Can't be used together with open_period.") + ), + Param( + name: "is_closed", + ty: Option(bool), + descr: Doc(md: "Pass True, if the poll needs to be immediately closed. This can be useful for poll preview.") + ), + Param( + name: "disable_notification", + ty: Option(bool), + descr: Doc( + md: "Sends the message [silently]. Users will receive a notification with no sound.", + md_links: {"silently": "https://telegram.org/blog/channels-2-0#silent-messages"} + ) + ), + Param( + name: "protect_content", + ty: Option(bool), + descr: Doc(md: "Protects the contents of sent messages from forwarding and saving"), + ), + Param( + name: "reply_to_message_id", + ty: Option(RawTy("MessageId")), + descr: Doc(md: "If the message is a reply, ID of the original message") + ), + Param( + name: "allow_sending_without_reply", + ty: Option(bool), + descr: Doc(md: "Pass _True_, if the message should be sent even if the specified replied-to message is not found") + ), + Param( + name: "reply_markup", + ty: Option(RawTy("ReplyMarkup")), + descr: Doc( + md: "Additional interface options. A JSON-serialized object for an [inline keyboard], [custom reply keyboard], instructions to remove reply keyboard or to force a reply from the user.", + md_links: { + "inline keyboard": "https://core.telegram.org/bots#inline-keyboards-and-on-the-fly-updating", + "custom reply keyboard": "https://core.telegram.org/bots#keyboards", + } + ), + ), + ], + ), + Method( + names: ("sendDice", "SendDice", "send_dice"), + return_ty: RawTy("Message"), + doc: Doc( + md: "Use this method to send an animated emoji that will display a random value. On success, the sent [Message] is returned.", + md_links: {"Message": "https://core.telegram.org/bots/api#message"}, + ), + tg_doc: "https://core.telegram.org/bots/api#senddice", + tg_category: "Available methods", + params: [ + Param( + name: "chat_id", + ty: RawTy("Recipient"), + descr: Doc(md: "Unique identifier for the target chat or username of the target channel (in the format `@channelusername`)") + ), + Param( + name: "emoji", + ty: Option(RawTy("DiceEmoji")), + descr: Doc(md: "Emoji on which the dice throw animation is based. Currently, must be one of “🎲”, “🎯”, “🏀”, “⚽”, “🎳”, or “🎰”. Dice can have values 1-6 for “🎲”, “🎯” and “🎳”, values 1-5 for “🏀” and “⚽”, and values 1-64 for “🎰”. Defaults to “🎲”"), + ), + Param( + name: "disable_notification", + ty: Option(bool), + descr: Doc( + md: "Sends the message [silently]. Users will receive a notification with no sound.", + md_links: {"silently": "https://telegram.org/blog/channels-2-0#silent-messages"} + ) + ), + Param( + name: "protect_content", + ty: Option(bool), + descr: Doc(md: "Protects the contents of sent messages from forwarding and saving"), + ), + Param( + name: "reply_to_message_id", + ty: Option(RawTy("MessageId")), + descr: Doc(md: "If the message is a reply, ID of the original message") + ), + Param( + name: "allow_sending_without_reply", + ty: Option(bool), + descr: Doc(md: "Pass _True_, if the message should be sent even if the specified replied-to message is not found") + ), + Param( + name: "reply_markup", + ty: Option(RawTy("ReplyMarkup")), + descr: Doc( + md: "Additional interface options. A JSON-serialized object for an [inline keyboard], [custom reply keyboard], instructions to remove reply keyboard or to force a reply from the user.", + md_links: { + "inline keyboard": "https://core.telegram.org/bots#inline-keyboards-and-on-the-fly-updating", + "custom reply keyboard": "https://core.telegram.org/bots#keyboards", + } + ), + ), + ], + ), + Method( + names: ("sendChatAction", "SendChatAction", "send_chat_action"), + return_ty: True, + doc: Doc( + md: "Use this method when you need to tell the user that something is happening on the bot's side. The status is set for 5 seconds or less (when a message arrives from your bot, Telegram clients clear its typing status). Returns True on success.\n\n> Example: The [ImageBot] needs some time to process a request and upload the image. Instead of sending a text message along the lines of “Retrieving image, please wait…”, the bot may use sendChatAction with action = upload_photo. The user will see a “sending photo” status for the bot.\n\nWe only recommend using this method when a response from the bot will take a **noticeable** amount of time to arrive.", + md_links: {"ImageBot": "https://t.me/imagebot"}, + ), + tg_doc: "https://core.telegram.org/bots/api#sendchataction", + tg_category: "Available methods", + params: [ + Param( + name: "chat_id", + ty: RawTy("Recipient"), + descr: Doc(md: "Unique identifier for the target chat or username of the target channel (in the format `@channelusername`)") + ), + Param( + name: "action", + ty: RawTy("ChatAction"), + descr: Doc( + md: "Type of action to broadcast. Choose one, depending on what the user is about to receive: typing for [text messages], upload_photo for [photos], record_video or upload_video for [videos], record_audio or upload_audio for [audio files], upload_document for [general files], choose_sticker for [stickers], find_location for [location data], record_video_note or upload_video_note for [video notes].", + md_links: { + "text messages": "https://core.telegram.org/bots/api#sendmessage", + "photos": "https://core.telegram.org/bots/api#sendphoto", + "videos": "https://core.telegram.org/bots/api#sendvideo", + "audio files": "https://core.telegram.org/bots/api#sendaudio", + "general files": "https://core.telegram.org/bots/api#senddocument", + "stickers": "https://core.telegram.org/bots/api#sendsticker", + "location data": "https://core.telegram.org/bots/api#sendlocation", + "video notes": "https://core.telegram.org/bots/api#sendvideonote", + } + ), + ), + ], + ), + Method( + names: ("getUserProfilePhotos", "GetUserProfilePhotos", "get_user_profile_photos"), + return_ty: RawTy("UserProfilePhotos"), + doc: Doc( + md: "Use this method to get a list of profile pictures for a user. Returns a [UserProfilePhotos] object.", + md_links: {"UserProfilePhotos": "https://core.telegram.org/bots/api#userprofilephotos"}, + ), + tg_doc: "https://core.telegram.org/bots/api#getuserprofilephotos", + tg_category: "Available methods", + params: [ + Param( + name: "user_id", + ty: RawTy("UserId"), + descr: Doc(md: "Unique identifier of the target user") + ), + Param( + name: "offset", + ty: Option(u32), + descr: Doc(md: "Sequential number of the first photo to be returned. By default, all photos are returned.") + ), + Param( + name: "limit", + ty: Option(u8), + descr: Doc(md: "Limits the number of photos to be retrieved. Values between 1-100 are accepted. Defaults to 100.") + ), + ], + ), + Method( + names: ("getFile", "GetFile", "get_file"), + return_ty: RawTy("File"), + doc: Doc( + md: "Use this method to get basic info about a file and prepare it for downloading. For the moment, bots can download files of up to 20MB in size. On success, a [File] object is returned. The file can then be downloaded via the link `https://api.telegram.org/file/bot/`, where `` is taken from the response. It is guaranteed that the link will be valid for at least 1 hour. When the link expires, a new one can be requested by calling [getFile] again.", + md_links: { + "File": "https://core.telegram.org/bots/api#file", + "getFile": "https://core.telegram.org/bots/api#getfile", + }, + ), + tg_doc: "https://core.telegram.org/bots/api#getuserprofilephotos", + tg_category: "Available methods", + params: [ + Param( + name: "file_id", + ty: String, + descr: Doc(md: "File identifier to get info about") + ), + ], + ), + Method( + names: ("banChatMember", "BanChatMember", "ban_chat_member"), + return_ty: True, + doc: Doc( + md: "Use this method to ban a user in a group, a supergroup or a channel. In the case of supergroups and channels, the user will not be able to return to the chat on their own using invite links, etc., unless [unbanned] first. The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. Returns _True_ on success.", + md_links: {"unbanned": "https://core.telegram.org/bots/api#unbanchatmember"}, + ), + tg_doc: "https://core.telegram.org/bots/api#kickchatmember", + tg_category: "Available methods", + params: [ + Param( + name: "chat_id", + ty: RawTy("Recipient"), + descr: Doc(md: "Unique identifier for the target chat or username of the target channel (in the format `@channelusername`)") + ), + Param( + name: "user_id", + ty: RawTy("UserId"), + descr: Doc(md: "Unique identifier of the target user") + ), + Param( + name: "until_date", + ty: Option(u64), + descr: Doc(md: "Date when the user will be unbanned, unix time. If user is banned for more than 366 days or less than 30 seconds from the current time they are considered to be banned forever") + ), + Param( + name: "revoke_messages", + ty: Option(bool), + descr: Doc(md: "Pass True to delete all messages from the chat for the user that is being removed. If False, the user will be able to see messages in the group that were sent before the user was removed. Always True for supergroups and channels.") + ), + ], + ), + // deprecated + Method( + names: ("kickChatMember", "KickChatMember", "kick_chat_member"), + return_ty: True, + doc: Doc( + md: "Use this method to kick a user from a group, a supergroup or a channel. In the case of supergroups and channels, the user will not be able to return to the group on their own using invite links, etc., unless [unbanned] first. The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. Returns _True_ on success.", + md_links: {"unbanned": "https://core.telegram.org/bots/api#unbanchatmember"}, + ), + tg_doc: "https://core.telegram.org/bots/api#kickchatmember", + tg_category: "Available methods", + params: [ + Param( + name: "chat_id", + ty: RawTy("Recipient"), + descr: Doc(md: "Unique identifier for the target chat or username of the target channel (in the format `@channelusername`)") + ), + Param( + name: "user_id", + ty: RawTy("UserId"), + descr: Doc(md: "Unique identifier of the target user") + ), + Param( + name: "until_date", + ty: Option(u64), + descr: Doc(md: "Date when the user will be unbanned, unix time. If user is banned for more than 366 days or less than 30 seconds from the current time they are considered to be banned forever") + ), + Param( + name: "revoke_messages", + ty: Option(bool), + descr: Doc(md: "Pass True to delete all messages from the chat for the user that is being removed. If False, the user will be able to see messages in the group that were sent before the user was removed. Always True for supergroups and channels.") + ), + ], + ), + + Method( + names: ("unbanChatMember", "UnbanChatMember", "unban_chat_member"), + return_ty: True, + doc: Doc(md: "Use this method to unban a previously kicked user in a supergroup or channel. The user will **not** return to the group or channel automatically, but will be able to join via link, etc. The bot must be an administrator for this to work. By default, this method guarantees that after the call the user is not a member of the chat, but will be able to join it. So if the user is a member of the chat they will also be **removed** from the chat. If you don't want this, use the parameter _only\\_if\\_banned_. Returns _True_ on success."), + tg_doc: "https://core.telegram.org/bots/api#unbanchatmember", + tg_category: "Available methods", + params: [ + Param( + name: "chat_id", + ty: RawTy("Recipient"), + descr: Doc(md: "Unique identifier for the target chat or username of the target channel (in the format `@channelusername`)") + ), + Param( + name: "user_id", + ty: RawTy("UserId"), + descr: Doc(md: "Unique identifier of the target user") + ), + Param( + name: "only_if_banned", + ty: Option(bool), + descr: Doc(md: "Do nothing if the user is not banned"), + ) + ], + ), + Method( + names: ("restrictChatMember", "RestrictChatMember", "restrict_chat_member"), + return_ty: True, + doc: Doc(md: "Use this method to restrict a user in a supergroup. The bot must be an administrator in the supergroup for this to work and must have the appropriate admin rights. Pass _True_ for all permissions to lift restrictions from a user. Returns _True_ on success."), + tg_doc: "https://core.telegram.org/bots/api#restrictchatmember", + tg_category: "Available methods", + params: [ + Param( + name: "chat_id", + ty: RawTy("Recipient"), + descr: Doc(md: "Unique identifier for the target chat or username of the target channel (in the format `@channelusername`)") + ), + Param( + name: "user_id", + ty: RawTy("UserId"), + descr: Doc(md: "Unique identifier of the target user") + ), + Param( + name: "permissions", + ty: RawTy("ChatPermissions"), + descr: Doc(md: "A JSON-serialized object for new user permissions") + ), + Param( + name: "until_date", + ty: Option(u64), + descr: Doc(md: "Date when the user will be unbanned, unix time. If user is banned for more than 366 days or less than 30 seconds from the current time they are considered to be banned forever") + ), + ], + ), + Method( + names: ("promoteChatMember", "PromoteChatMember", "promote_chat_member"), + return_ty: True, + doc: Doc(md: "Use this method to promote or demote a user in a supergroup or a channel. The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. Pass _False_ for all boolean parameters to demote a user. Returns _True_ on success."), + tg_doc: "https://core.telegram.org/bots/api#promotechatmember", + tg_category: "Available methods", + params: [ + Param( + name: "chat_id", + ty: RawTy("Recipient"), + descr: Doc(md: "Unique identifier for the target chat or username of the target channel (in the format `@channelusername`)") + ), + Param( + name: "user_id", + ty: RawTy("UserId"), + descr: Doc(md: "Unique identifier of the target user") + ), + Param( + name: "is_anonymous", + ty: Option(bool), + descr: Doc(md: "Pass True, if the administrator's presence in the chat is hidden") + ), + Param( + name: "can_manage_chat", + ty: Option(bool), + descr: Doc(md: "Pass True, if the administrator can access the chat event log, chat statistics, message statistics in channels, see channel members, see anonymous administrators in supergroups and ignore slow mode. Implied by any other administrator privilege"), + ), + Param( + name: "can_change_info", + ty: Option(bool), + descr: Doc(md: "Pass True, if the administrator can change chat title, photo and other settings") + ), + Param( + name: "can_post_messages", + ty: Option(bool), + descr: Doc(md: "Pass True, if the administrator can create channel posts, channels only") + ), + Param( + name: "can_edit_messages", + ty: Option(bool), + descr: Doc(md: "Pass True, if the administrator can edit messages of other users and can pin messages, channels only") + ), + Param( + name: "can_delete_messages", + ty: Option(bool), + descr: Doc(md: "Pass True, if the administrator can delete messages of other users") + ), + Param( + name: "can_manage_video_chats", + ty: Option(bool), + descr: Doc(md: "Pass True, if the administrator can manage video chats, supergroups only") + ), + Param( + name: "can_invite_users", + ty: Option(bool), + descr: Doc(md: "Pass True, if the administrator can invite new users to the chat") + ), + Param( + name: "can_restrict_members", + ty: Option(bool), + descr: Doc(md: "Pass True, if the administrator can restrict, ban or unban chat members") + ), + Param( + name: "can_pin_messages", + ty: Option(bool), + descr: Doc(md: "Pass True, if the administrator can pin messages, supergroups only") + ), + Param( + name: "can_promote_members", + ty: Option(bool), + descr: Doc(md: "Pass True, if the administrator can add new administrators with a subset of their own privileges or demote administrators that he has promoted, directly or indirectly (promoted by administrators that were appointed by him)") + ), + ], + ), + Method( + names: ("setChatAdministratorCustomTitle", "SetChatAdministratorCustomTitle", "set_chat_administrator_custom_title"), + return_ty: True, + doc: Doc(md: "Use this method to set a custom title for an administrator in a supergroup promoted by the bot. Returns _True_on success."), + tg_doc: "https://core.telegram.org/bots/api#setchatadministratorcustomtitle", + tg_category: "Available methods", + params: [ + Param( + name: "chat_id", + ty: RawTy("Recipient"), + descr: Doc(md: "Unique identifier for the target chat or username of the target channel (in the format `@channelusername`)") + ), + Param( + name: "user_id", + ty: RawTy("UserId"), + descr: Doc(md: "Unique identifier of the target user") + ), + Param( + name: "custom_title", + ty: String, + descr: Doc(md: "New custom title for the administrator; 0-16 characters, emoji are not allowed") + ), + ], + ), + Method( + names: ("banChatSenderChat", "BanChatSenderChat", "ban_chat_sender_chat"), + return_ty: True, + doc: Doc(md: "Use this method to ban a channel chat in a supergroup or a channel. The owner of the chat will not be able to send messages and join live streams on behalf of the chat, unless it is unbanned first. The bot must be an administrator in the supergroup or channel for this to work and must have the appropriate administrator rights."), + tg_doc: "https://core.telegram.org/bots/api#banchatsenderchat", + tg_category: "Available methods", + params: [ + Param( + name: "chat_id", + ty: RawTy("Recipient"), + descr: Doc(md: "Unique identifier for the target chat or username of the target channel (in the format `@channelusername`)") + ), + Param( + name: "sender_chat_id", + ty: RawTy("ChatId"), + descr: Doc(md: "Unique identifier of the target sender chat") + ), + ], + ), + Method( + names: ("unbanChatSenderChat", "UnbanChatSenderChat", "unban_chat_sender_chat"), + return_ty: True, + doc: Doc(md: "Use this method to unban a previously banned channel chat in a supergroup or channel. The bot must be an administrator for this to work and must have the appropriate administrator rights."), + tg_doc: "https://core.telegram.org/bots/api#unbanchatsenderchat", + tg_category: "Available methods", + params: [ + Param( + name: "chat_id", + ty: RawTy("Recipient"), + descr: Doc(md: "Unique identifier for the target chat or username of the target channel (in the format `@channelusername`)") + ), + Param( + name: "sender_chat_id", + ty: RawTy("ChatId"), + descr: Doc(md: "Unique identifier of the target sender chat") + ), + ], + ), + Method( + names: ("setChatPermissions", "SetChatPermissions", "set_chat_permissions"), + return_ty: True, + doc: Doc(md: "Use this method to set default chat permissions for all members. The bot must be an administrator in the group or a supergroup for this to work and must have the _can_restrict_members_ admin rights. Returns _True_ on success."), + tg_doc: "https://core.telegram.org/bots/api#setchatpermissions", + tg_category: "Available methods", + params: [ + Param( + name: "chat_id", + ty: RawTy("Recipient"), + descr: Doc(md: "Unique identifier for the target chat or username of the target channel (in the format `@channelusername`)") + ), + Param( + name: "permissions", + ty: RawTy("ChatPermissions"), + descr: Doc(md: "New default chat permissions") + ), + ], + ), + Method( + names: ("exportChatInviteLink", "ExportChatInviteLink", "export_chat_invite_link"), + return_ty: String, + doc: Doc(md: "Use this method to generate a new invite link for a chat; any previously generated link is revoked. The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. Returns the new invite link as String on success.\n\n> Note: Each administrator in a chat generates their own invite links. Bots can't use invite links generated by other administrators. If you want your bot to work with invite links, it will need to generate its own link using exportChatInviteLink — after this the link will become available to the bot via the getChat method. If your bot needs to generate a new invite link replacing its previous one, use exportChatInviteLink again."), + tg_doc: "https://core.telegram.org/bots/api#exportchatinvitelink", + tg_category: "Available methods", + params: [ + Param( + name: "chat_id", + ty: RawTy("Recipient"), + descr: Doc(md: "Unique identifier for the target chat or username of the target channel (in the format `@channelusername`)") + ), + ], + ), + Method( + names: ("createChatInviteLink", "CreateChatInviteLink", "create_chat_invite_link"), + return_ty: RawTy("ChatInviteLink"), + doc: Doc( + md: "Use this method to create an additional invite link for a chat. The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. The link can be revoked using the method [revokeChatInviteLink]. Returns the new invite link as [ChatInviteLink] object.", + md_links: { + "revokeChatInviteLink": "https://core.telegram.org/bots/api#revokechatinvitelink", + "ChatInviteLink": "https://core.telegram.org/bots/api#chatinvitelink", + } + ), + tg_doc: "https://core.telegram.org/bots/api#createchatinvitelink", + tg_category: "Available methods", + params: [ + Param( + name: "chat_id", + ty: RawTy("Recipient"), + descr: Doc(md: "Unique identifier for the target chat or username of the target channel (in the format `@channelusername`)") + ), + Param( + name: "name", + ty: Option(String), + descr: Doc(md: "Invite link name; 0-32 characters") + ), + Param( + name: "expire_date", + ty: Option(i64), + descr: Doc(md: "Point in time (Unix timestamp) when the link will expire") + ), + Param( + name: "member_limit", + ty: Option(u32), + descr: Doc(md: "Maximum number of users that can be members of the chat simultaneously after joining the chat via this invite link; 1-99999") + ), + Param( + name: "creates_join_request", + ty: Option(bool) , + descr: Doc(md: "True, if users joining the chat via the link need to be approved by chat administrators. If True, member_limit can't be specified") + ), + ], + ), + Method( + names: ("editChatInviteLink", "EditChatInviteLink", "edit_chat_invite_link"), + return_ty: String, + doc: Doc( + md: "Use this method to edit a non-primary invite link created by the bot. The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. Returns the edited invite link as a [ChatInviteLink] object.", + md_links: {"ChatInviteLink": "https://core.telegram.org/bots/api#chatinvitelink"} + ), + tg_doc: "https://core.telegram.org/bots/api#editchatinvitelink", + tg_category: "Available methods", + params: [ + Param( + name: "chat_id", + ty: RawTy("Recipient"), + descr: Doc(md: "Unique identifier for the target chat or username of the target channel (in the format `@channelusername`)") + ), + Param( + name: "invite_link", + ty: String, + descr: Doc(md: "The invite link to edit") + ), + Param( + name: "name", + ty: Option(String), + descr: Doc(md: "Invite link name; 0-32 characters") + ), + Param( + name: "expire_date", + ty: Option(i64), + descr: Doc(md: "Point in time (Unix timestamp) when the link will expire") + ), + Param( + name: "member_limit", + ty: Option(u32), + descr: Doc(md: "Maximum number of users that can be members of the chat simultaneously after joining the chat via this invite link; 1-99999") + ), + Param( + name: "creates_join_request", + ty: Option(bool) , + descr: Doc(md: "True, if users joining the chat via the link need to be approved by chat administrators. If True, member_limit can't be specified") + ), + ], + ), + Method( + names: ("revokeChatInviteLink", "RevokeChatInviteLink", "revoke_chat_invite_link"), + return_ty: String, + doc: Doc( + md: "Use this method to revoke an invite link created by the bot. If the primary link is revoked, a new link is automatically generated. The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. Returns the revoked invite link as [ChatInviteLink] object.", + md_links: {"ChatInviteLink": "https://core.telegram.org/bots/api#chatinvitelink"} + ), + tg_doc: "https://core.telegram.org/bots/api#editchatinvitelink", + tg_category: "Available methods", + params: [ + Param( + name: "chat_id", + ty: RawTy("Recipient"), + descr: Doc(md: "Unique identifier for the target chat or username of the target channel (in the format `@channelusername`)") + ), + Param( + name: "invite_link", + ty: String, + descr: Doc(md: "The invite link to revoke") + ), + ], + ), + Method( + names: ("approveChatJoinRequest", "ApproveChatJoinRequest", "approve_chat_join_request"), + return_ty: True, + doc: Doc(md: "Use this method to approve a chat join request. The bot must be an administrator in the chat for this to work and must have the _can_invite_users_ administrator right. Returns _True_ on success."), + tg_doc: "https://core.telegram.org/bots/api#approvechatjoinrequest", + tg_category: "Available methods", + params: [ + Param( + name: "chat_id", + ty: RawTy("Recipient"), + descr: Doc(md: "Unique identifier for the target chat or username of the target channel (in the format `@channelusername`)") + ), + Param( + name: "user_id", + ty: RawTy("UserId"), + descr: Doc(md: "Unique identifier of the target user") + ), + ], + ), + Method( + names: ("declineChatJoinRequest", "DeclineChatJoinRequest", "decline_chat_join_request"), + return_ty: True, + doc: Doc(md: "Use this method to decline a chat join request. The bot must be an administrator in the chat for this to work and must have the _can_invite_users_ administrator right. Returns _True_ on success."), + tg_doc: "https://core.telegram.org/bots/api#declinechatjoinrequest", + tg_category: "Available methods", + params: [ + Param( + name: "chat_id", + ty: RawTy("Recipient"), + descr: Doc(md: "Unique identifier for the target chat or username of the target channel (in the format `@channelusername`)") + ), + Param( + name: "user_id", + ty: RawTy("UserId"), + descr: Doc(md: "Unique identifier of the target user") + ), + ], + ), + Method( + names: ("setChatPhoto", "SetChatPhoto", "set_chat_photo"), + return_ty: True, + doc: Doc(md: "Use this method to set a new profile photo for the chat. Photos can't be changed for private chats. The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. Returns _True_ on success."), + tg_doc: "https://core.telegram.org/bots/api#setchatphoto", + tg_category: "Available methods", + params: [ + Param( + name: "chat_id", + ty: RawTy("Recipient"), + descr: Doc(md: "Unique identifier for the target chat or username of the target channel (in the format `@channelusername`)") + ), + Param( + name: "photo", + ty: RawTy("InputFile"), + descr: Doc(md: "New chat photo, uploaded using multipart/form-data") + ), + ], + ), + Method( + names: ("deleteChatPhoto", "DeleteChatPhoto", "delete_chat_photo"), + return_ty: String, + doc: Doc(md: "Use this method to delete a chat photo. Photos can't be changed for private chats. The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. Returns True on success."), + tg_doc: "https://core.telegram.org/bots/api#deletechatphoto", + tg_category: "Available methods", + params: [ + Param( + name: "chat_id", + ty: RawTy("Recipient"), + descr: Doc(md: "Unique identifier for the target chat or username of the target channel (in the format `@channelusername`)") + ), + ], + ), + Method( + names: ("setChatTitle", "SetChatTitle", "set_chat_title"), + return_ty: True, + doc: Doc(md: "Use this method to change the title of a chat. Titles can't be changed for private chats. The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. Returns _True_ on success."), + tg_doc: "https://core.telegram.org/bots/api#setchattitle", + tg_category: "Available methods", + params: [ + Param( + name: "chat_id", + ty: RawTy("Recipient"), + descr: Doc(md: "Unique identifier for the target chat or username of the target channel (in the format `@channelusername`)") + ), + Param( + name: "title", + ty: String, + descr: Doc(md: "New chat title, 1-255 characters") + ), + ], + ), + Method( + names: ("setChatDescription", "SetChatDescription", "set_chat_description"), + return_ty: True, + doc: Doc(md: "Use this method to change the description of a group, a supergroup or a channel. The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. Returns _True_ on success."), + tg_doc: "https://core.telegram.org/bots/api#setchatdescription", + tg_category: "Available methods", + params: [ + Param( + name: "chat_id", + ty: RawTy("Recipient"), + descr: Doc(md: "Unique identifier for the target chat or username of the target channel (in the format `@channelusername`)") + ), + Param( + name: "description", + ty: Option(String), + descr: Doc(md: "New chat description, 0-255 characters") + ), + ], + ), + Method( + names: ("pinChatMessage", "PinChatMessage", "pin_chat_message"), + return_ty: True, + doc: Doc(md: "Use this method to pin a message in a group, a supergroup, or a channel. The bot must be an administrator in the chat for this to work and must have the 'can_pin_messages' admin right in the supergroup or 'can_edit_messages' admin right in the channel. Returns _True_ on success."), + tg_doc: "https://core.telegram.org/bots/api#pinchatmessage", + tg_category: "Available methods", + params: [ + Param( + name: "chat_id", + ty: RawTy("Recipient"), + descr: Doc(md: "Unique identifier for the target chat or username of the target channel (in the format `@channelusername`)") + ), + Param( + name: "message_id", + ty: RawTy("MessageId"), + descr: Doc(md: "Identifier of a message to pin"), + ), + Param( + name: "disable_notification", + ty: Option(bool), + descr: Doc(md: "Pass True, if it is not necessary to send a notification to all chat members about the new pinned message. Notifications are always disabled in channels.") + ), + ], + ), + Method( + names: ("unpinChatMessage", "UnpinChatMessage", "unpin_chat_message"), + return_ty: True, + doc: Doc(md: "Use this method to remove a message from the list of pinned messages in a chat. If the chat is not a private chat, the bot must be an administrator in the chat for this to work and must have the 'can_pin_messages' admin right in a supergroup or 'can_edit_messages' admin right in a channel. Returns _True_ on success."), + tg_doc: "https://core.telegram.org/bots/api#unpinchatmessage", + tg_category: "Available methods", + params: [ + Param( + name: "chat_id", + ty: RawTy("Recipient"), + descr: Doc(md: "Unique identifier for the target chat or username of the target channel (in the format `@channelusername`)") + ), + Param( + name: "message_id", + ty: Option(RawTy("MessageId")), + descr: Doc(md: "Identifier of a message to unpin. If not specified, the most recent pinned message (by sending date) will be unpinned.") + ), + ], + ), + Method( + names: ("unpinAllChatMessages", "UnpinAllChatMessages", "unpin_all_chat_messages"), + return_ty: True, + doc: Doc(md: "Use this method to clear the list of pinned messages in a chat. If the chat is not a private chat, the bot must be an administrator in the chat for this to work and must have the 'can_pin_messages' admin right in a supergroup or 'can_edit_messages' admin right in a channel. Returns _True_ on success."), + tg_doc: "https://core.telegram.org/bots/api#unpinallchatmessages", + tg_category: "Available methods", + params: [ + Param( + name: "chat_id", + ty: RawTy("Recipient"), + descr: Doc(md: "Unique identifier for the target chat or username of the target channel (in the format `@channelusername`)") + ), + ], + ), + Method( + names: ("leaveChat", "LeaveChat", "leave_chat"), + return_ty: True, + doc: Doc(md: "Use this method for your bot to leave a group, supergroup or channel. Returns _True_ on success."), + tg_doc: "https://core.telegram.org/bots/api#leavechat", + tg_category: "Available methods", + params: [ + Param( + name: "chat_id", + ty: RawTy("Recipient"), + descr: Doc(md: "Unique identifier for the target chat or username of the target channel (in the format `@channelusername`)") + ), + ], + ), + Method( + names: ("getChat", "GetChat", "get_chat"), + return_ty: RawTy("Chat"), + doc: Doc( + md: "Use this method to get up to date information about the chat (current name of the user for one-on-one conversations, current username of a user, group or channel, etc.). Returns a [Chat] object on success.", + md_links: {"Chat": "https://core.telegram.org/bots/api#chat"}, + ), + tg_doc: "https://core.telegram.org/bots/api#getchat", + tg_category: "Available methods", + params: [ + Param( + name: "chat_id", + ty: RawTy("Recipient"), + descr: Doc(md: "Unique identifier for the target chat or username of the target channel (in the format `@channelusername`)") + ), + ], + ), + Method( + names: ("getChatAdministrators", "GetChatAdministrators", "get_chat_administrators"), + return_ty: ArrayOf(RawTy("ChatMember")), + doc: Doc( + md: "Use this method to get a list of administrators in a chat. On success, returns an Array of [ChatMember] objects that contains information about all chat administrators except other bots. If the chat is a group or a supergroup and no administrators were appointed, only the creator will be returned.", + md_links: {"ChatMember": "https://core.telegram.org/bots/api#chatmember"}, + ), + tg_doc: "https://core.telegram.org/bots/api#getchatadministrators", + tg_category: "Available methods", + params: [ + Param( + name: "chat_id", + ty: RawTy("Recipient"), + descr: Doc(md: "Unique identifier for the target chat or username of the target channel (in the format `@channelusername`)") + ), + ], + ), + Method( + names: ("getChatMemberCount", "GetChatMemberCount", "get_chat_member_count"), + return_ty: u32, + doc: Doc(md: "Use this method to get the number of members in a chat. Returns _Int_ on success."), + tg_doc: "https://core.telegram.org/bots/api#getchatmemberscount", + tg_category: "Available methods", + params: [ + Param( + name: "chat_id", + ty: RawTy("Recipient"), + descr: Doc(md: "Unique identifier for the target chat or username of the target channel (in the format `@channelusername`)") + ), + ], + ), + // deprecated + Method( + names: ("getChatMembersCount", "GetChatMembersCount", "get_chat_members_count"), + return_ty: u32, + doc: Doc(md: "Use this method to get the number of members in a chat. Returns _Int_ on success."), + tg_doc: "https://core.telegram.org/bots/api#getchatmemberscount", + tg_category: "Available methods", + params: [ + Param( + name: "chat_id", + ty: RawTy("Recipient"), + descr: Doc(md: "Unique identifier for the target chat or username of the target channel (in the format `@channelusername`)") + ), + ], + ), + Method( + names: ("getChatMember", "GetChatMember", "get_chat_member"), + return_ty: RawTy("ChatMember"), + doc: Doc( + md: "Use this method to get information about a member of a chat. Returns a [ChatMember] object on success.", + md_links: {"ChatMember": "https://core.telegram.org/bots/api#chatmember"} + ), + tg_doc: "https://core.telegram.org/bots/api#getchatmember", + tg_category: "Available methods", + params: [ + Param( + name: "chat_id", + ty: RawTy("Recipient"), + descr: Doc(md: "Unique identifier for the target chat or username of the target channel (in the format `@channelusername`)") + ), + Param( + name: "user_id", + ty: RawTy("UserId"), + descr: Doc(md: "Unique identifier of the target user") + ), + ], + ), + Method( + names: ("setChatStickerSet", "SetChatStickerSet", "set_chat_sticker_set"), + return_ty: True, + doc: Doc(md: "Use this method to set a new group sticker set for a supergroup. The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. Use the field _can\\_set\\_sticker\\_set_ optionally returned in getChat requests to check if the bot can use this method. Returns _True_ on success."), + tg_doc: "https://core.telegram.org/bots/api#set_chat_sticker_set", + tg_category: "Available methods", + params: [ + Param( + name: "chat_id", + ty: RawTy("Recipient"), + descr: Doc(md: "Unique identifier for the target chat or username of the target channel (in the format `@channelusername`)") + ), + Param( + name: "sticker_set_name", + ty: String, + descr: Doc(md: "Name of the sticker set to be set as the group sticker set") + ), + ], + ), + Method( + names: ("deleteChatStickerSet", "DeleteChatStickerSet", "delete_chat_sticker_set"), + return_ty: True, + doc: Doc( + md: "Use this method to delete a group sticker set from a supergroup. The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. Use the field `can_set_sticker_set` optionally returned in [getChat] requests to check if the bot can use this method. Returns _True_ on success.", + md_links: {"getChat": "https://core.telegram.org/bots/api#getchat"} + ), + tg_doc: "https://core.telegram.org/bots/api#deletechatstickerset", + tg_category: "Available methods", + params: [ + Param( + name: "chat_id", + ty: RawTy("Recipient"), + descr: Doc(md: "Unique identifier for the target chat or username of the target channel (in the format `@channelusername`)") + ), + ], + ), + Method( + names: ("answerCallbackQuery", "AnswerCallbackQuery", "answer_callback_query"), + return_ty: True, + doc: Doc( + md: "Use this method to send answers to callback queries sent from [inline keyboards]. The answer will be displayed to the user as a notification at the top of the chat screen or as an alert. On success, True is returned.\n\n>Alternatively, the user can be redirected to the specified Game URL. For this option to work, you must first create a game for your bot via [@Botfather] and accept the terms. Otherwise, you may use links like `t.me/your_bot?start=XXXX` that open your bot with a parameter.", + md_links: { + "inline keyboards": "https://core.telegram.org/bots#inline-keyboards-and-on-the-fly-updating", + "@Botfather": "https://t.me/botfather", + } + ), + tg_doc: "https://core.telegram.org/bots/api#answercallbackquery", + tg_category: "Available methods", + params: [ + Param( + name: "callback_query_id", + ty: String, + descr: Doc(md: "Unique identifier for the query to be answered"), + ), + Param( + name: "text", + ty: Option(String), + descr: Doc(md: "Text of the notification. If not specified, nothing will be shown to the user, 0-200 characters"), + ), + Param( + name: "show_alert", + ty: Option(bool), + descr: Doc(md: "If true, an alert will be shown by the client instead of a notification at the top of the chat screen. Defaults to false."), + ), + Param( + name: "url", + ty: Option(String), + descr: Doc( + md: "URL that will be opened by the user's client. If you have created a [Game] and accepted the conditions via [@Botfather], specify the URL that opens your game — note that this will only work if the query comes from a _[callback\\_game]_ button.\n\nOtherwise, you may use links like `t.me/your\\_bot?start=XXXX` that open your bot with a parameter.", + md_links: { + "Game": "https://core.telegram.org/bots/api#game", + "@Botfather": "https://t.me/botfather", + "callback_game": "https://core.telegram.org/bots/api#inlinekeyboardbutton", + }, + ), + ), + Param( + name: "cache_time", + ty: Option(u32), + descr: Doc(md: "The maximum amount of time in seconds that the result of the callback query may be cached client-side. Telegram apps will support caching starting in version 3.14. Defaults to 0."), + ), + ], + ), + Method( + names: ("setMyCommands", "SetMyCommands", "set_my_commands"), + return_ty: True, + doc: Doc(md: "Use this method to change the list of the bot's commands. Returns _True_ on success."), + tg_doc: "https://core.telegram.org/bots/api#setmycommands", + tg_category: "Available methods", + params: [ + Param( + name: "commands", + ty: ArrayOf(RawTy("BotCommand")), + descr: Doc(md: "A JSON-serialized list of bot commands to be set as the list of the bot's commands. At most 100 commands can be specified.") + ), + Param( + name: "scope", + ty: Option(RawTy("BotCommandScope")), + descr: Doc(md: "A JSON-serialized object, describing scope of users for which the commands are relevant. Defaults to BotCommandScopeDefault.") + ), + Param( + name: "language_code", + ty: Option(String), + descr: Doc(md: "A two-letter ISO 639-1 language code. If empty, commands will be applied to all users from the given scope, for whose language there are no dedicated commands") + ), + ], + ), + Method( + names: ("getMyCommands", "GetMyCommands", "get_my_commands"), + return_ty: ArrayOf(RawTy("BotCommand")), + doc: Doc( + md: "Use this method to get the current list of the bot's commands. Requires no parameters. Returns Array of [BotCommand] on success.", + md_links: {"BotCommand": "https://core.telegram.org/bots/api#botcommand"}, + ), + tg_doc: "https://core.telegram.org/bots/api#getmycommands", + tg_category: "Available methods", + params: [ + Param( + name: "scope", + ty: Option(RawTy("BotCommandScope")), + descr: Doc(md: "A JSON-serialized object, describing scope of users for which the commands are relevant. Defaults to BotCommandScopeDefault.") + ), + Param( + name: "language_code", + ty: Option(String), + descr: Doc(md: "A two-letter ISO 639-1 language code. If empty, commands will be applied to all users from the given scope, for whose language there are no dedicated commands") + ), + ], + ), + Method( + names: ("setChatMenuButton", "SetChatMenuButton", "set_chat_menu_button"), + return_ty: True, + doc: Doc(md: "Use this method to change the bot's menu button in a private chat, or the default menu button."), + tg_doc: "https://core.telegram.org/bots/api#setchatmenubutton", + tg_category: "Available methods", + params: [ + Param( + name: "chat_id", + ty: Option(RawTy("ChatId")), + descr: Doc(md: "Unique identifier for the target private chat. If not specified, default bot's menu button will be changed."), + ), + Param( + name: "menu_button", + ty: Option(RawTy("MenuButton")), + descr: Doc(md: "An object for the new bot's menu button. Defaults to MenuButtonDefault"), + ) + ] + ), + Method( + names: ("getChatMenuButton", "GetChatMenuButton", "get_chat_menu_button"), + return_ty: RawTy("MenuButton"), + doc: Doc(md: "Use this method to get the current value of the bot's menu button in a private chat, or the default menu button."), + tg_doc: "https://core.telegram.org/bots/api#getchatmenubutton", + tg_category: "Available methods", + params: [ + Param( + name: "chat_id", + ty: Option(RawTy("ChatId")), + descr: Doc(md: "Unique identifier for the target private chat. If not specified, default bot's menu button will be returned"), + ), + ] + ), + Method( + names: ("setMyDefaultAdministratorRights", "SetMyDefaultAdministratorRights", "set_my_default_administrator_rights"), + return_ty: True, + doc: Doc(md: "Use this method to change the default administrator rights requested by the bot when it's added as an administrator to groups or channels. These rights will be suggested to users, but they are are free to modify the list before adding the bot."), + tg_doc: "https://core.telegram.org/bots/api#setmydefaultadministratorrights", + tg_category: "Available methods", + params: [ + Param( + name: "rights", + ty: Option(RawTy("ChatAdministratorRights")), + descr: Doc(md: "A JSON-serialized object describing new default administrator rights. If not specified, the default administrator rights will be cleared."), + ), + Param( + name: "for_channels", + ty: Option(bool), + descr: Doc(md: "Pass _True_ to change the default administrator rights of the bot in channels. Otherwise, the default administrator rights of the bot for groups and supergroups will be changed."), + ) + ] + ), + Method( + names: ("getMyDefaultAdministratorRights", "GetMyDefaultAdministratorRights", "get_my_default_administrator_rights"), + return_ty: RawTy("ChatAdministratorRights"), + doc: Doc(md: "Use this method to get the current value of the bot's menu button in a private chat, or the default menu button."), + tg_doc: "https://core.telegram.org/bots/api#setmydefaultadministratorrights", + tg_category: "Available methods", + params: [ + Param( + name: "for_channels", + ty: Option(bool), + descr: Doc(md: "Pass _True_ to get default administrator rights of the bot in channels. Otherwise, default administrator rights of the bot for groups and supergroups will be returned."), + ) + ] + ), + Method( + names: ("deleteMyCommands", "DeleteMyCommands", "delete_my_commands"), + return_ty: True, + doc: Doc( + md: "Use this method to delete the list of the bot's commands for the given scope and user language. After deletion, [higher level commands] will be shown to affected users. Returns _True_ on success.", + md_links: {"higher level commands":"https://core.telegram.org/bots/api#determining-list-of-commands"}, + ), + tg_doc: "https://core.telegram.org/bots/api#deletemycommands", + tg_category: "Available methods", + params: [ + Param( + name: "scope", + ty: Option(RawTy("BotCommandScope")), + descr: Doc(md: "A JSON-serialized object, describing scope of users for which the commands are relevant. Defaults to BotCommandScopeDefault.") + ), + Param( + name: "language_code", + ty: Option(String), + descr: Doc(md: "A two-letter ISO 639-1 language code. If empty, commands will be applied to all users from the given scope, for whose language there are no dedicated commands") + ), + ], + ), + Method( + names: ("answerInlineQuery", "AnswerInlineQuery", "answer_inline_query"), + return_ty: True, + doc: Doc(md: "Use this method to send answers to an inline query. On success, _True_ is returned. No more than **50** results per query are allowed."), + tg_doc: "https://core.telegram.org/bots/api#answerinlinequery", + tg_category: "Inline Mode", + params: [ + Param( + name: "inline_query_id", + ty: String, + descr: Doc(md: "Unique identifier for the answered query") + ), + Param( + name: "results", + ty: ArrayOf(RawTy("InlineQueryResult")), + descr: Doc(md: "A JSON-serialized array of results for the inline query") + ), + Param( + name: "cache_time", + ty: Option(u32), + descr: Doc(md: "The maximum amount of time in seconds that the result of the inline query may be cached on the server. Defaults to 300.") + ), + Param( + name: "is_personal", + ty: Option(bool), + descr: Doc(md: "Pass _True_, if results may be cached on the server side only for the user that sent the query. By default, results may be returned to any user who sends the same query") + ), + Param( + name: "next_offset", + ty: Option(String), + descr: Doc(md: "Pass the offset that a client should send in the next query with the same text to receive more results. Pass an empty string if there are no more results or if you don't support pagination. Offset length can't exceed 64 bytes.") + ), + Param( + name: "switch_pm_text", + ty: Option(String), + descr: Doc(md: "If passed, clients will display a button with specified text that switches the user to a private chat with the bot and sends the bot a start message with the parameter switch_pm_parameter") + ), + Param( + name: "switch_pm_parameter", + ty: Option(String), + descr: Doc( + md: "[Deep-linking] parameter for the /start message sent to the bot when user presses the switch button. 1-64 characters, only `A-Z`, `a-z`, `0-9`, `_` and `-` are allowed.\n\n_Example_: An inline bot that sends YouTube videos can ask the user to connect the bot to their YouTube account to adapt search results accordingly. To do this, it displays a 'Connect your YouTube account' button above the results, or even before showing any. The user presses the button, switches to a private chat with the bot and, in doing so, passes a start parameter that instructs the bot to return an oauth link. Once done, the bot can offer a [switch_inline] button so that the user can easily return to the chat where they wanted to use the bot's inline capabilities.", + md_links: { + "Deep-linking": "https://core.telegram.org/bots#deep-linking", + "switch_inline": "https://core.telegram.org/bots/api#inlinekeyboardmarkup", + }, + ), + ), + ], + ), + Method( + names: ("answerWebAppQuery", "AnswerWebAppQuery", "answer_web_app_query"), + return_ty: RawTy("SentWebAppMessage"), + doc: Doc( + md: "Use this method to set the result of an interaction with a [Web App] and send a corresponding message on behalf of the user to the chat from which the query originated.", + md_links: {"Web App": "https://core.telegram.org/bots/webapps"} + ), + tg_doc: "https://core.telegram.org/bots/api#answerwebappquery", + tg_category: "Inline Mode", + params: [ + Param( + name: "web_app_query_id", + ty: String, + descr: Doc(md: "Unique identifier for the query to be answered") + ), + Param( + name :"result", + ty: RawTy("InlineQueryResult"), + descr: Doc(md: "A JSON-serialized object describing the message to be sent"), + ), + ] + ), + Method( + names: ("editMessageText", "EditMessageText", "edit_message_text"), + return_ty: RawTy("Message"), + doc: Doc( + md: "Use this method to edit text and [games] messages. On success, the edited Message is returned.", + md_links: {"games": "https://core.telegram.org/bots/api#games"}, + ), + tg_doc: "https://core.telegram.org/bots/api#editmessagetext", + tg_category: "Updating messages", + sibling: Some("editMessageTextInline"), + params: [ + Param( + name: "chat_id", + ty: RawTy("Recipient"), + descr: Doc(md: "Unique identifier for the target chat or username of the target channel (in the format `@channelusername`).") + ), + Param( + name: "message_id", + ty: RawTy("MessageId"), + descr: Doc(md: "Identifier of the message to edit") + ), + Param( + name: "text", + ty: String, + descr: Doc(md: "New text of the message, 1-4096 characters after entities parsing") + ), + Param( + name: "parse_mode", + ty: Option(RawTy("ParseMode")), + descr: Doc( + md: "Mode for parsing entities in the message text. See [formatting options] for more details.", + md_links: {"formatting options": "https://core.telegram.org/bots/api#formatting-options"} + ) + ), + Param( + name: "entities", + ty: Option(ArrayOf(RawTy("MessageEntity"))), + descr: Doc(md: "List of special entities that appear in message text, which can be specified instead of _parse\\_mode_"), + ), + Param( + name: "disable_web_page_preview", + ty: Option(bool), + descr: Doc(md: "Disables link previews for links in this message") + ), + Param( + name: "reply_markup", + ty: Option(RawTy("InlineKeyboardMarkup")), + descr: Doc( + md: "A JSON-serialized object for an [inline keyboard].", + md_links: {"inline keyboard": "https://core.telegram.org/bots#inline-keyboards-and-on-the-fly-updating"}, + ), + ), + ], + ), + Method( + names: ("editMessageTextInline", "EditMessageTextInline", "edit_message_text_inline"), + return_ty: True, + doc: Doc( + md: "Use this method to edit text and [games] messages. On success, _True_ is returned.", + md_links: {"games": "https://core.telegram.org/bots/api#games"}, + ), + tg_doc: "https://core.telegram.org/bots/api#editmessagetext", + tg_category: "Updating messages", + sibling: Some("editMessageText"), + params: [ + Param( + name: "inline_message_id", + ty: String, + descr: Doc(md: "Identifier of the inline message") + ), + Param( + name: "text", + ty: String, + descr: Doc(md: "New text of the message, 1-4096 characters after entities parsing") + ), + Param( + name: "parse_mode", + ty: Option(RawTy("ParseMode")), + descr: Doc( + md: "Mode for parsing entities in the message text. See [formatting options] for more details.", + md_links: {"formatting options": "https://core.telegram.org/bots/api#formatting-options"} + ) + ), + Param( + name: "entities", + ty: Option(ArrayOf(RawTy("MessageEntity"))), + descr: Doc(md: "List of special entities that appear in message text, which can be specified instead of _parse\\_mode_"), + ), + Param( + name: "disable_web_page_preview", + ty: Option(bool), + descr: Doc(md: "Disables link previews for links in this message") + ), + Param( + name: "reply_markup", + ty: Option(RawTy("InlineKeyboardMarkup")), + descr: Doc( + md: "A JSON-serialized object for an [inline keyboard].", + md_links: {"inline keyboard": "https://core.telegram.org/bots#inline-keyboards-and-on-the-fly-updating"} + ), + ), + ], + ), + Method( + names: ("editMessageCaption", "EditMessageCaption", "edit_message_caption"), + return_ty: RawTy("Message"), + doc: Doc(md: "Use this method to edit captions of messages. On success, the edited Message is returned."), + tg_doc: "https://core.telegram.org/bots/api#editmessagecaption", + tg_category: "Updating messages", + sibling: Some("editMessageCaptionInline"), + params: [ + Param( + name: "chat_id", + ty: RawTy("Recipient"), + descr: Doc(md: "Unique identifier for the target chat or username of the target channel (in the format `@channelusername`).") + ), + Param( + name: "message_id", + ty: RawTy("MessageId"), + descr: Doc(md: "Identifier of the message to edit") + ), + Param( + name: "caption", + ty: Option(String), + descr: Doc(md: "New caption of the message, 0-1024 characters after entities parsing") + ), + Param( + name: "parse_mode", + ty: Option(RawTy("ParseMode")), + descr: Doc( + md: "Mode for parsing entities in the message text. See [formatting options] for more details.", + md_links: {"formatting options": "https://core.telegram.org/bots/api#formatting-options"} + ) + ), + Param( + name: "caption_entities", + ty: Option(ArrayOf(RawTy("MessageEntity"))), + descr: Doc(md: "List of special entities that appear in the caption, which can be specified instead of _parse\\_mode_"), + ), + Param( + name: "reply_markup", + ty: Option(RawTy("InlineKeyboardMarkup")), + descr: Doc( + md: "A JSON-serialized object for an [inline keyboard].", + md_links: {"inline keyboard": "https://core.telegram.org/bots#inline-keyboards-and-on-the-fly-updating",} + ), + ), + ], + ), + Method( + names: ("editMessageCaptionInline", "EditMessageCaptionInline", "edit_message_caption_inline"), + return_ty: True, + doc: Doc(md: "Use this method to edit captions of messages. On success, _True_ is returned."), + tg_doc: "https://core.telegram.org/bots/api#editmessagetext", + tg_category: "Updating messages", + sibling: Some("editMessageCaption"), + params: [ + Param( + name: "inline_message_id", + ty: String, + descr: Doc(md: "Identifier of the inline message") + ), + Param( + name: "caption", + ty: Option(String), + descr: Doc(md: "New caption of the message, 0-1024 characters after entities parsing") + ), + Param( + name: "parse_mode", + ty: Option(RawTy("ParseMode")), + descr: Doc( + md: "Mode for parsing entities in the message text. See [formatting options] for more details.", + md_links: {"formatting options": "https://core.telegram.org/bots/api#formatting-options"} + ) + ), + Param( + name: "caption_entities", + ty: Option(ArrayOf(RawTy("MessageEntity"))), + descr: Doc(md: "List of special entities that appear in the caption, which can be specified instead of _parse\\_mode_"), + ), + + Param( + name: "reply_markup", + ty: Option(RawTy("InlineKeyboardMarkup")), + descr: Doc( + md: "A JSON-serialized object for an [inline keyboard].", + md_links: {"inline keyboard": "https://core.telegram.org/bots#inline-keyboards-and-on-the-fly-updating"} + ), + ), + ], + ), + Method( + names: ("editMessageMedia", "EditMessageMedia", "edit_message_media"), + return_ty: RawTy("Message"), + doc: Doc(md: "Use this method to edit animation, audio, document, photo, or video messages. If a message is a part of a message album, then it can be edited only to a photo or a video. Otherwise, message type can be changed arbitrarily. When inline message is edited, new file can't be uploaded. Use previously uploaded file via its file_id or specify a URL. On success, the edited Message is returned."), + tg_doc: "https://core.telegram.org/bots/api#editmessagemedia", + tg_category: "Updating messages", + sibling: Some("editMessageMediaInline"), + params: [ + Param( + name: "chat_id", + ty: RawTy("Recipient"), + descr: Doc(md: "Unique identifier for the target chat or username of the target channel (in the format `@channelusername`).") + ), + Param( + name: "message_id", + ty: RawTy("MessageId"), + descr: Doc(md: "Identifier of the message to edit") + ), + Param( + name: "media", + ty: RawTy("InputMedia"), + descr: Doc(md: "A JSON-serialized object for a new media content of the message") + ), + Param( + name: "reply_markup", + ty: Option(RawTy("InlineKeyboardMarkup")), + descr: Doc( + md: "A JSON-serialized object for an [inline keyboard].", + md_links: {"inline keyboard": "https://core.telegram.org/bots#inline-keyboards-and-on-the-fly-updating"} + ), + ), + ], + ), + Method( + names: ("editMessageMediaInline", "EditMessageMediaInline", "edit_message_media_inline"), + return_ty: True, + doc: Doc(md: "Use this method to edit animation, audio, document, photo, or video messages. If a message is a part of a message album, then it can be edited only to a photo or a video. Otherwise, message type can be changed arbitrarily. When inline message is edited, new file can't be uploaded. Use previously uploaded file via its file_id or specify a URL. On success, _True_ is returned."), + tg_doc: "https://core.telegram.org/bots/api#editmessagemediainline", + tg_category: "Updating messages", + sibling: Some("editMessageMedia"), + params: [ + Param( + name: "inline_message_id", + ty: String, + descr: Doc(md: "Identifier of the inline message") + ), + Param( + name: "media", + ty: RawTy("InputMedia"), + descr: Doc(md: "A JSON-serialized object for a new media content of the message") + ), + Param( + name: "reply_markup", + ty: Option(RawTy("InlineKeyboardMarkup")), + descr: Doc( + md: "A JSON-serialized object for an [inline keyboard].", + md_links: {"inline keyboard": "https://core.telegram.org/bots#inline-keyboards-and-on-the-fly-updating",} + ), + ), + ], + ), + Method( + names: ("editMessageReplyMarkup", "EditMessageReplyMarkup", "edit_message_reply_markup"), + return_ty: RawTy("Message"), + doc: Doc(md: "Use this method to edit only the reply markup of messages. On success, the edited Message is returned."), + tg_doc: "https://core.telegram.org/bots/api#editmessagereplymarkup", + tg_category: "Updating messages", + sibling: Some("editMessageMediaInline"), + params: [ + Param( + name: "chat_id", + ty: RawTy("Recipient"), + descr: Doc(md: "Unique identifier for the target chat or username of the target channel (in the format `@channelusername`).") + ), + Param( + name: "message_id", + ty: RawTy("MessageId"), + descr: Doc(md: "Identifier of the message to edit") + ), + Param( + name: "reply_markup", + ty: Option(RawTy("InlineKeyboardMarkup")), + descr: Doc( + md: "A JSON-serialized object for an [inline keyboard].", + md_links: {"inline keyboard": "https://core.telegram.org/bots#inline-keyboards-and-on-the-fly-updating",} + ), + ), + ], + ), + Method( + names: ("editMessageReplyMarkupInline", "EditMessageReplyMarkupInline", "edit_message_reply_markup_inline"), + return_ty: True, + doc: Doc(md: "Use this method to edit only the reply markup of messages. On success, _True_ is returned."), + tg_doc: "https://core.telegram.org/bots/api#editmessagereplymarkup", + tg_category: "Updating messages", + sibling: Some("editMessageReplyMarkup"), + params: [ + Param( + name: "inline_message_id", + ty: String, + descr: Doc(md: "Identifier of the inline message") + ), + Param( + name: "reply_markup", + ty: Option(RawTy("InlineKeyboardMarkup")), + descr: Doc( + md: "A JSON-serialized object for an [inline keyboard].", + md_links: {"inline keyboard": "https://core.telegram.org/bots#inline-keyboards-and-on-the-fly-updating",} + ), + ), + ], + ), + Method( + names: ("stopPoll", "StopPoll", "stop_poll"), + return_ty: RawTy("Poll"), + doc: Doc(md: "Use this method to stop a poll which was sent by the bot. On success, the stopped Poll with the final results is returned."), + tg_doc: "https://core.telegram.org/bots/api#stoppoll", + tg_category: "Updating messages", + params: [ + Param( + name: "chat_id", + ty: RawTy("Recipient"), + descr: Doc(md: "Unique identifier for the target chat or username of the target channel (in the format `@channelusername`).") + ), + Param( + name: "message_id", + ty: RawTy("MessageId"), + descr: Doc(md: "Identifier of the message to edit") + ), + Param( + name: "reply_markup", + ty: Option(RawTy("InlineKeyboardMarkup")), + descr: Doc( + md: "A JSON-serialized object for an [inline keyboard].", + md_links: {"inline keyboard": "https://core.telegram.org/bots#inline-keyboards-and-on-the-fly-updating",} + ), + ), + ], + ), + Method( + names: ("deleteMessage", "DeleteMessage", "delete_message"), + return_ty: True, + doc: Doc(md: "Use this method to delete a message, including service messages, with the following limitations:\n- A message can only be deleted if it was sent less than 48 hours ago.\n- A dice message in a private chat can only be deleted if it was sent more than 24 hours ago.\n- Bots can delete outgoing messages in private chats, groups, and supergroups.\n- Bots can delete incoming messages in private chats.\n- Bots granted can_post_messages permissions can delete outgoing messages in channels.\n- If the bot is an administrator of a group, it can delete any message there.\n- If the bot has can_delete_messages permission in a supergroup or a channel, it can delete any message there.\n\nReturns True on success."), + tg_doc: "https://core.telegram.org/bots/api#delete_message", + tg_category: "Updating messages", + params: [ + Param( + name: "chat_id", + ty: RawTy("Recipient"), + descr: Doc(md: "Unique identifier for the target chat or username of the target channel (in the format `@channelusername`).") + ), + Param( + name: "message_id", + ty: RawTy("MessageId"), + descr: Doc(md: "Identifier of the message to delete") + ), + ], + ), + Method( + names: ("sendSticker", "SendSticker", "send_sticker"), + return_ty: RawTy("Message"), + doc: Doc( + md: "Use this method to send static .WEBP or [animated] .TGS stickers. On success, the sent Message is returned.", + md_links: {"animated": "https://telegram.org/blog/animated-stickers"} + ), + tg_doc: "https://core.telegram.org/bots/api#sendsticker", + tg_category: "Stickers", + params: [ + Param( + name: "chat_id", + ty: RawTy("Recipient"), + descr: Doc(md: "Unique identifier for the target chat or username of the target channel (in the format `@channelusername`).") + ), + Param( + name: "sticker", + ty: RawTy("InputFile"), + descr: Doc( + md: "Sticker to send. Pass a file_id as String to send a photo that exists on the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get a photo from the Internet, or upload a new photo using multipart/form-data. [More info on Sending Files »]", + md_links: {"More info on Sending Files »": "https://core.telegram.org/bots/api#sending-files"}, + ) + ), + Param( + name: "disable_notification", + ty: Option(bool), + descr: Doc( + md: "Sends the message [silently]. Users will receive a notification with no sound.", + md_links: {"silently": "https://telegram.org/blog/channels-2-0#silent-messages"} + ) + ), + Param( + name: "protect_content", + ty: Option(bool), + descr: Doc(md: "Protects the contents of sent messages from forwarding and saving"), + ), + Param( + name: "reply_to_message_id", + ty: Option(i32), + descr: Doc(md: "If the message is a reply, ID of the original message") + ), + Param( + name: "allow_sending_without_reply", + ty: Option(bool), + descr: Doc(md: "Pass _True_, if the message should be sent even if the specified replied-to message is not found") + ), + Param( + name: "reply_markup", + ty: Option(RawTy("ReplyMarkup")), + descr: Doc( + md: "Additional interface options. A JSON-serialized object for an [inline keyboard], [custom reply keyboard], instructions to remove reply keyboard or to force a reply from the user.", + md_links: { + "inline keyboard": "https://core.telegram.org/bots#inline-keyboards-and-on-the-fly-updating", + "custom reply keyboard": "https://core.telegram.org/bots#keyboards", + } + ), + ), + ], + ), + Method( + names: ("getStickerSet", "GetStickerSet", "get_sticker_set"), + return_ty: RawTy("StickerSet"), + doc: Doc(md: "Use this method to get a sticker set. On success, a StickerSet object is returned."), + tg_doc: "https://core.telegram.org/bots/api#getstickerset", + tg_category: "Stickers", + params: [ + Param( + name: "name", + ty: String, + descr: Doc(md: "Name of the sticker set"), + ), + ], + ), + Method( + names: ("getCustomEmojiStickers", "GetCustomEmojiStickers", "get_custom_emoji_stickers"), + return_ty: ArrayOf(RawTy("Sticker")), + doc: Doc(md: "Use this method to get information about custom emoji stickers by their identifiers. Returns an Array of Sticker objects."), + tg_doc: "https://core.telegram.org/bots/api#getcustomemojistickers", + tg_category: "Stickers", + params: [ + Param( + name: "custom_emoji_ids", + ty: ArrayOf(String), + descr: Doc(md: "List of custom emoji identifiers. At most 200 custom emoji identifiers can be specified."), + ), + ], + ), + Method( + names: ("uploadStickerFile", "UploadStickerFile", "upload_sticker_file"), + return_ty: RawTy("FileMeta"), + doc: Doc(md: "Use this method to upload a .PNG file with a sticker for later use in _createNewStickerSet_ and _addStickerToSet_ methods (can be used multiple times). Returns the uploaded File on success."), + tg_doc: "https://core.telegram.org/bots/api#uploadstickerfile", + tg_category: "Stickers", + params: [ + Param( + name: "user_id", + ty: RawTy("UserId"), + descr: Doc(md: "User identifier of sticker file owner"), + ), + Param( + name: "png_sticker", + ty: RawTy("InputFile"), + descr: Doc( + md: "PNG image with the sticker, must be up to 512 kilobytes in size, dimensions must not exceed 512px, and either width or height must be exactly 512px. [More info on Sending Files »]", + md_links: {"More info on Sending Files »": "https://core.telegram.org/bots/api#sending-files"}, + ), + ), + ], + ), + Method( + names: ("createNewStickerSet", "CreateNewStickerSet", "create_new_sticker_set"), + return_ty: True, + doc: Doc(md: "Use this method to create a new sticker set owned by a user. The bot will be able to edit the sticker set thus created. You must use exactly one of the fields _png\\_sticker_ or _tgs\\_sticker_. Returns _True_ on success."), + tg_doc: "https://core.telegram.org/bots/api#createstickerset", + tg_category: "Stickers", + params: [ + Param( + name: "user_id", + ty: RawTy("UserId"), + descr: Doc(md: "User identifier of sticker file owner"), + ), + Param( + name: "name", + ty: String, + descr: Doc(md: "Short name of sticker set, to be used in `t.me/addstickers/` URLs (e.g., _animals_). Can contain only english letters, digits and underscores. Must begin with a letter, can't contain consecutive underscores and must end in _“\\_by\\_”. _ is case insensitive. 1-64 characters."), + ), + Param( + name: "title", + ty: String, + descr: Doc(md: "Sticker set title, 1-64 characters"), + ), + Param( + name: "sticker", + ty: RawTy("InputSticker"), + descr: Doc( + md: "**PNG** image, **TGS** animation or **WEBM** video with the sticker, must be up to 512 kilobytes in size, dimensions must not exceed 512px, and either width or height must be exactly 512px. Pass a _file\\_id_ as a String to send a file that already exists on the Telegram servers, pass an HTTP URL as a String for Telegram to get a file from the Internet, or upload a new one using multipart/form-data. [More info on Sending Files »]", + md_links: {"More info on Sending Files »": "https://core.telegram.org/bots/api#sending-files"}, + ), + ), + Param( + name: "emojis", + ty: String, + descr: Doc(md: "One or more emoji corresponding to the sticker"), + ), + Param( + name: "sticker_type", + ty: Option(RawTy("StickerType")), + descr: Doc(md: "Type of stickers in the set, pass “regular” or “mask”. Custom emoji sticker sets can't be created via the Bot API at the moment. By default, a regular sticker set is created."), + ), + Param( + name: "mask_position", + ty: Option(RawTy("MaskPosition")), + descr: Doc(md: "A JSON-serialized object for position where the mask should be placed on faces"), + ), + ], + ), + Method( + names: ("addStickerToSet", "AddStickerToSet", "add_sticker_to_set"), + return_ty: True, + doc: Doc(md: "Use this method to add a new sticker to a set created by the bot. You **must** use exactly one of the fields _png\\_sticker_ or _tgs\\_sticker_. Animated stickers can be added to animated sticker sets and only to them. Animated sticker sets can have up to 50 stickers. Static sticker sets can have up to 120 stickers. Returns _True_ on success."), + tg_doc: "https://core.telegram.org/bots/api#addstickertoset", + tg_category: "Stickers", + params: [ + Param( + name: "user_id", + ty: RawTy("UserId"), + descr: Doc(md: "User identifier of sticker file owner"), + ), + Param( + name: "name", + ty: String, + descr: Doc(md: "Sticker set name"), + ), + Param( + name: "sticker", + ty: RawTy("InputSticker"), + descr: Doc( + md: "**PNG** or **TGS** image with the sticker, must be up to 512 kilobytes in size, dimensions must not exceed 512px, and either width or height must be exactly 512px. Pass a _file\\_id_ as a String to send a file that already exists on the Telegram servers, pass an HTTP URL as a String for Telegram to get a file from the Internet, or upload a new one using multipart/form-data. [More info on Sending Files »]", + md_links: {"More info on Sending Files »": "https://core.telegram.org/bots/api#sending-files"}, + ), + ), + Param( + name: "emojis", + ty: String, + descr: Doc(md: "One or more emoji corresponding to the sticker"), + ), + Param( + name: "mask_position", + ty: Option(RawTy("MaskPosition")), + descr: Doc(md: "A JSON-serialized object for position where the mask should be placed on faces"), + ), + ], + ), + Method( + names: ("setStickerPositionInSet", "SetStickerPositionInSet", "set_sticker_position_in_set"), + return_ty: True, + doc: Doc(md: "Use this method to move a sticker in a set created by the bot to a specific position. Returns _True_ on success."), + tg_doc: "https://core.telegram.org/bots/api#setstickerpositioninset", + tg_category: "Stickers", + params: [ + Param( + name: "sticker", + ty: String, + descr: Doc(md: "File identifier of the sticker"), + ), + Param( + name: "position", + ty: u32, + descr: Doc(md: "New sticker position in the set, zero-based"), + ), + ], + ), + Method( + names: ("deleteStickerFromSet", "DeleteStickerFromSet", "delete_sticker_from_set"), + return_ty: True, + doc: Doc(md: "Use this method to delete a sticker from a set created by the bot. Returns _True_ on success."), + tg_doc: "https://core.telegram.org/bots/api#deletestickerfromset", + tg_category: "Stickers", + params: [ + Param( + name: "sticker", + ty: String, + descr: Doc(md: "File identifier of the sticker"), + ), + ], + ), + Method( + names: ("setStickerSetThumb", "SetStickerSetThumb", "set_sticker_set_thumb"), + return_ty: True, + doc: Doc(md: "Use this method to set the thumbnail of a sticker set. Animated thumbnails can be set for animated sticker sets only. Returns _True_ on success."), + tg_doc: "https://core.telegram.org/bots/api#setstickersetthumb", + tg_category: "Stickers", + params: [ + Param( + name: "name", + ty: String, + descr: Doc(md: "Name of the sticker set"), + ), + Param( + name: "user_id", + ty: RawTy("UserId"), + descr: Doc(md: "User identifier of sticker file owner"), + ), + Param( + name: "thumb", + ty: Option(RawTy("InputFile")), + descr: Doc( + md: "A **PNG** image with the thumbnail, must be up to 128 kilobytes in size and have width and height exactly 100px, or a **TGS** animation with the thumbnail up to 32 kilobytes in size; see https://core.telegram.org/animated_stickers#technical-requirements for animated sticker technical requirements. Pass a _file\\_id_ as a String to send a file that already exists on the Telegram servers, pass an HTTP URL as a String for Telegram to get a file from the Internet, or upload a new one using multipart/form-data. [More info on Sending Files »]. Animated sticker set thumbnail can't be uploaded via HTTP URL.", + md_links: {"More info on Sending Files »": "https://core.telegram.org/bots/api#sending-files"}, + ), + ), + ], + ), + Method( + names: ("sendInvoice", "SendInvoice", "send_invoice"), + return_ty: RawTy("Message"), + doc: Doc( + md: "Use this method to send invoices. On success, the sent [Message] is returned.", + md_links: {"Message":"https://core.telegram.org/bots/api#message"} + ), + tg_doc: "https://core.telegram.org/bots/api#sendinvoice", + tg_category: "Payments", + params: [ + Param( + name: "chat_id", + ty: RawTy("Recipient"), + descr: Doc(md: "Unique identifier for the target private chat"), + ), + Param( + name: "title", + ty: String, + descr: Doc(md: "Product name, 1-32 characters"), + ), + Param( + name: "description", + ty: String, + descr: Doc(md: "Product description, 1-255 characters"), + ), + Param( + name: "payload", + ty: String, + descr: Doc(md: "Bot-defined invoice payload, 1-128 bytes. This will not be displayed to the user, use for your internal processes."), + ), + Param( + name: "provider_token", + ty: String, + descr: Doc( + md: "Payments provider token, obtained via [Botfather]", + md_links: {"Botfather":"https://t.me/botfather"} + ), + ), + Param( + name: "currency", + ty: String, + descr: Doc(md: "Three-letter ISO 4217 currency code, see more on currencies"), + ), + Param( + name: "prices", + ty: ArrayOf(RawTy("LabeledPrice")), + descr: Doc(md: "Price breakdown, a JSON-serialized list of components (e.g. product price, tax, discount, delivery cost, delivery tax, bonus, etc.)"), + ), + Param( + name: "max_tip_amount", + ty: Option(u32), + descr: Doc( + md: "The maximum accepted amount for tips in the smallest units of the currency (integer, **not** float/double). For example, for a maximum tip of `US$ 1.45` pass `max_tip_amount = 145`. See the exp parameter in [`currencies.json`], it shows the number of digits past the decimal point for each currency (2 for the majority of currencies). Defaults to 0", + md_links: {"`currencies.json`":"https://core.telegram.org/bots/payments/currencies.json"} + ), + ), + Param( + name: "suggested_tip_amounts", + ty: Option(ArrayOf(u32)), + descr: Doc(md: "A JSON-serialized array of suggested amounts of tips in the smallest units of the currency (integer, **not** float/double). At most 4 suggested tip amounts can be specified. The suggested tip amounts must be positive, passed in a strictly increased order and must not exceed _max_tip_amount_."), + ), + Param( + name: "start_parameter", + ty: Option(String), + descr: Doc(md: "Unique deep-linking parameter. If left empty, **forwarded copies** of the sent message will have a Pay button, allowing multiple users to pay directly from the forwarded message, using the same invoice. If non-empty, forwarded copies of the sent message will have a URL button with a deep link to the bot (instead of a Pay button), with the value used as the start parameter"), + ), + Param( + name: "provider_data", + ty: Option(String), + descr: Doc(md: "A JSON-serialized data about the invoice, which will be shared with the payment provider. A detailed description of required fields should be provided by the payment provider.") + ), + Param( + name: "photo_url", + ty: Option(String), + descr: Doc(md: "URL of the product photo for the invoice. Can be a photo of the goods or a marketing image for a service. People like it better when they see what they are paying for.") + ), + Param( + name: "photo_size", + ty: Option(String), + descr: Doc(md: "Photo size in bytes") + ), + Param( + name: "photo_width", + ty: Option(String), + descr: Doc(md: "Photo width") + ), + Param( + name: "photo_height", + ty: Option(String), + descr: Doc(md: "Photo height") + ), + Param( + name: "need_name", + ty: Option(bool), + descr: Doc(md: "Pass _True_, if you require the user's full name to complete the order") + ), + Param( + name: "need_phone_number", + ty: Option(bool), + descr: Doc(md: "Pass _True_, if you require the user's phone number to complete the order") + ), + Param( + name: "need_email", + ty: Option(bool), + descr: Doc(md: "Pass _True_, if you require the user's email address to complete the order") + ), + Param( + name: "need_shipping_address", + ty: Option(bool), + descr: Doc(md: "Pass _True_, if you require the user's shipping address to complete the order") + ), + Param( + name: "send_phone_number_to_provider", + ty: Option(bool), + descr: Doc(md: "Pass _True_, if user's phone number should be sent to provider") + ), + Param( + name: "send_email_to_provider", + ty: Option(bool), + descr: Doc(md: "Pass _True_, if user's email address should be sent to provider") + ), + Param( + name: "is_flexible", + ty: Option(bool), + descr: Doc(md: "Pass _True_, if the final price depends on the shipping method") + ), + Param( + name: "disable_notification", + ty: Option(bool), + descr: Doc( + md: "Sends the message [silently]. Users will receive a notification with no sound.", + md_links: {"silently": "https://telegram.org/blog/channels-2-0#silent-messages"} + ) + ), + Param( + name: "protect_content", + ty: Option(bool), + descr: Doc(md: "Protects the contents of sent messages from forwarding and saving"), + ), + Param( + name: "reply_to_message_id", + ty: Option(i32), + descr: Doc(md: "If the message is a reply, ID of the original message") + ), + Param( + name: "allow_sending_without_reply", + ty: Option(bool), + descr: Doc(md: "Pass _True_, if the message should be sent even if the specified replied-to message is not found") + ), + Param( + name: "reply_markup", + ty: Option(RawTy("InlineKeyboardMarkup")), + descr: Doc( + md: "A JSON-serialized object for an [inline keyboard]. If empty, one 'Pay `total price`' button will be shown. If not empty, the first button must be a Pay button.", + md_links: {"inline keyboard": "https://core.telegram.org/bots#inline-keyboards-and-on-the-fly-updating"} + ), + ), + ], + ), + Method( + names: ("createInvoiceLink", "CreateInvoiceLink", "create_invoice_link"), + return_ty: String, + doc: Doc(md: "Use this method to create a link for an invoice. Returns the created invoice link as String on success."), + tg_doc: "https://core.telegram.org/bots/api#createinvoicelink", + tg_category: "Payments", + params: [ + Param( + name: "title", + ty: String, + descr: Doc(md: "Product name, 1-32 characters"), + ), + Param( + name: "description", + ty: String, + descr: Doc(md: "Product description, 1-255 characters"), + ), + Param( + name: "payload", + ty: String, + descr: Doc(md: "Bot-defined invoice payload, 1-128 bytes. This will not be displayed to the user, use for your internal processes."), + ), + Param( + name: "provider_token", + ty: String, + descr: Doc( + md: "Payments provider token, obtained via [Botfather]", + md_links: {"Botfather":"https://t.me/botfather"} + ), + ), + Param( + name: "currency", + ty: String, + descr: Doc(md: "Three-letter ISO 4217 currency code, see more on currencies"), + ), + Param( + name: "prices", + ty: ArrayOf(RawTy("LabeledPrice")), + descr: Doc(md: "Price breakdown, a JSON-serialized list of components (e.g. product price, tax, discount, delivery cost, delivery tax, bonus, etc.)"), + ), + Param( + name: "max_tip_amount", + ty: Option(u32), + descr: Doc( + md: "The maximum accepted amount for tips in the smallest units of the currency (integer, **not** float/double). For example, for a maximum tip of `US$ 1.45` pass `max_tip_amount = 145`. See the exp parameter in [`currencies.json`], it shows the number of digits past the decimal point for each currency (2 for the majority of currencies). Defaults to 0", + md_links: {"`currencies.json`":"https://core.telegram.org/bots/payments/currencies.json"} + ), + ), + Param( + name: "suggested_tip_amounts", + ty: Option(ArrayOf(u32)), + descr: Doc(md: "A JSON-serialized array of suggested amounts of tips in the smallest units of the currency (integer, **not** float/double). At most 4 suggested tip amounts can be specified. The suggested tip amounts must be positive, passed in a strictly increased order and must not exceed _max_tip_amount_."), + ), + Param( + name: "provider_data", + ty: Option(String), + descr: Doc(md: "A JSON-serialized data about the invoice, which will be shared with the payment provider. A detailed description of required fields should be provided by the payment provider.") + ), + Param( + name: "photo_url", + ty: Option(String), + descr: Doc(md: "URL of the product photo for the invoice. Can be a photo of the goods or a marketing image for a service. People like it better when they see what they are paying for.") + ), + Param( + name: "photo_size", + ty: Option(String), + descr: Doc(md: "Photo size in bytes") + ), + Param( + name: "photo_width", + ty: Option(String), + descr: Doc(md: "Photo width") + ), + Param( + name: "photo_height", + ty: Option(String), + descr: Doc(md: "Photo height") + ), + Param( + name: "need_name", + ty: Option(bool), + descr: Doc(md: "Pass _True_, if you require the user's full name to complete the order") + ), + Param( + name: "need_phone_number", + ty: Option(bool), + descr: Doc(md: "Pass _True_, if you require the user's phone number to complete the order") + ), + Param( + name: "need_email", + ty: Option(bool), + descr: Doc(md: "Pass _True_, if you require the user's email address to complete the order") + ), + Param( + name: "need_shipping_address", + ty: Option(bool), + descr: Doc(md: "Pass _True_, if you require the user's shipping address to complete the order") + ), + Param( + name: "send_phone_number_to_provider", + ty: Option(bool), + descr: Doc(md: "Pass _True_, if user's phone number should be sent to provider") + ), + Param( + name: "send_email_to_provider", + ty: Option(bool), + descr: Doc(md: "Pass _True_, if user's email address should be sent to provider") + ), + Param( + name: "is_flexible", + ty: Option(bool), + descr: Doc(md: "Pass _True_, if the final price depends on the shipping method") + ), + ], + ), + Method( + names: ("answerShippingQuery", "AnswerShippingQuery", "answer_shipping_query"), + return_ty: True, + doc: Doc( + md: "If you sent an invoice requesting a shipping address and the parameter _is\\_flexible_ was specified, the Bot API will send an [Update] with a shipping_query field to the bot. Use this method to reply to shipping queries. On success, True is returned.", + md_links: {"Update":"https://core.telegram.org/bots/api#update"}, + ), + tg_doc: "https://core.telegram.org/bots/api#answershippingquery", + tg_category: "Payments", + params: [ + Param( + name: "shipping_query_id", + ty: String, + descr: Doc(md: "Unique identifier for the query to be answered"), + ), + Param( + name: "ok", + ty: bool, + descr: Doc(md: "Specify True if delivery to the specified address is possible and False if there are any problems (for example, if delivery to the specified address is not possible)"), + ), + Param( + name: "shipping_options", + ty: Option(ArrayOf(RawTy("ShippingOption"))), + descr: Doc(md: "Required if ok is True. A JSON-serialized array of available shipping options."), + ), + Param( + name: "error_message", + ty: Option(String), + descr: Doc(md: "Required if ok is False. Error message in human readable form that explains why it is impossible to complete the order (e.g. 'Sorry, delivery to your desired address is unavailable'). Telegram will display this message to the user."), + ), + ], + ), + Method( + names: ("answerPreCheckoutQuery", "AnswerPreCheckoutQuery", "answer_pre_checkout_query"), + return_ty: True, + doc: Doc( + md: "Once the user has confirmed their payment and shipping details, the Bot API sends the final confirmation in the form of an [Update] with the field pre\\_checkout\\_query. Use this method to respond to such pre-checkout queries. On success, True is returned. **Note:** The Bot API must receive an answer within 10 seconds after the pre-checkout query was sent.", + md_links: {"Update":"https://core.telegram.org/bots/api#update"}, + ), + tg_doc: "https://core.telegram.org/bots/api#answershippingquery", + tg_category: "Payments", + params: [ + Param( + name: "pre_checkout_query_id", + ty: String, + descr: Doc(md: "Unique identifier for the query to be answered"), + ), + Param( + name: "ok", + ty: bool, + descr: Doc(md: "Specify True if everything is alright (goods are available, etc.) and the bot is ready to proceed with the order. Use False if there are any problems."), + ), + Param( + name: "error_message", + ty: Option(String), + descr: Doc(md: "Required if ok is False. Error message in human readable form that explains the reason for failure to proceed with the checkout (e.g. \"Sorry, somebody just bought the last of our amazing black T-shirts while you were busy filling out your payment details. Please choose a different color or garment!\"). Telegram will display this message to the user."), + ), + ], + ), + Method( + names: ("setPassportDataErrors", "SetPassportDataErrors", "set_passport_data_errors"), + return_ty: True, + doc: Doc(md: "Informs a user that some of the Telegram Passport elements they provided contains errors. The user will not be able to re-submit their Passport to you until the errors are fixed (the contents of the field for which you returned the error must change). Returns _True_ on success.\n\nUse this if the data submitted by the user doesn't satisfy the standards your service requires for any reason. For example, if a birthday date seems invalid, a submitted document is blurry, a scan shows evidence of tampering, etc. Supply some details in the error message to make sure the user knows how to correct the issues."), + tg_doc: "https://core.telegram.org/bots/api#setpassportdataerrors", + tg_category: "Telegram Passport", + params: [ + Param( + name: "user_id", + ty: RawTy("UserId"), + descr: Doc(md: "User identifier"), + ), + Param( + name: "errors", + ty: ArrayOf(RawTy("PassportElementError")), + descr: Doc(md: "A JSON-serialized array describing the errors"), + ), + ], + ), + Method( + names: ("sendGame", "SendGame", "send_game"), + return_ty: RawTy("Message"), + doc: Doc( + md: "Use this method to send a game. On success, the sent [Message] is returned.", + md_links: {"Message": "https://core.telegram.org/bots/api#message"} + ), + tg_doc: "https://core.telegram.org/bots/api#sendgame", + tg_category: "Games", + params: [ + Param( + name: "chat_id", + ty: u32, + descr: Doc(md: "Unique identifier for the target chat"), + ), + Param( + name: "game_short_name", + ty: String, + descr: Doc(md: "Short name of the game, serves as the unique identifier for the game. Set up your games via Botfather."), + ), + Param( + name: "disable_notification", + ty: Option(bool), + descr: Doc( + md: "Sends the message [silently]. Users will receive a notification with no sound.", + md_links: {"silently": "https://telegram.org/blog/channels-2-0#silent-messages"} + ) + ), + Param( + name: "protect_content", + ty: Option(bool), + descr: Doc(md: "Protects the contents of sent messages from forwarding and saving"), + ), + Param( + name: "reply_to_message_id", + ty: Option(i32), + descr: Doc(md: "If the message is a reply, ID of the original message") + ), + Param( + name: "allow_sending_without_reply", + ty: Option(bool), + descr: Doc(md: "Pass _True_, if the message should be sent even if the specified replied-to message is not found") + ), + Param( + name: "reply_markup", + ty: Option(RawTy("ReplyMarkup")), + descr: Doc( + md: "A JSON-serialized object for an [inline keyboard]. If empty, one 'Play game_title' button will be shown. If not empty, the first button must launch the game.", + md_links: {"inline keyboard": "https://core.telegram.org/bots#inline-keyboards-and-on-the-fly-updating"}, + ), + ), + ], + ), + Method( + names: ("setGameScore", "SetGameScore", "set_game_score"), + return_ty: RawTy("Message"), + doc: Doc( + md: "Use this method to set the score of the specified user in a game. On success, returns the edited [Message]. Returns an error, if the new score is not greater than the user's current score in the chat and force is False.", + md_links: {"Message": "https://core.telegram.org/bots/api#message"} + ), + tg_doc: "https://core.telegram.org/bots/api#setgamescore", + tg_category: "Games", + params: [ + Param( + name: "user_id", + ty: RawTy("UserId"), + descr: Doc(md: "User identifier"), + ), + Param( + name: "score", + ty: u64, + descr: Doc(md: "New score"), + ), + Param( + name: "force", + ty: Option(bool), + descr: Doc(md: "Pass True, if the high score is allowed to decrease. This can be useful when fixing mistakes or banning cheaters"), + ), + Param( + name: "disable_edit_message", + ty: Option(bool), + descr: Doc(md: "Pass True, if the game message should not be automatically edited to include the current scoreboard"), + ), + Param( + name: "chat_id", + ty: u32, + descr: Doc(md: "Unique identifier for the target chat") + ), + Param( + name: "message_id", + ty: RawTy("MessageId"), + descr: Doc(md: "Identifier of the message to edit") + ), + ], + sibling: Some("setGameScoreInline"), + ), + Method( + names: ("setGameScoreInline", "SetGameScoreInline", "set_game_score_inline"), + return_ty: RawTy("Message"), + doc: Doc(md: "Use this method to set the score of the specified user in a game. On success, returns _True_. Returns an error, if the new score is not greater than the user's current score in the chat and force is False."), + tg_doc: "https://core.telegram.org/bots/api#setgamescore", + tg_category: "Games", + params: [ + Param( + name: "user_id", + ty: RawTy("UserId"), + descr: Doc(md: "User identifier"), + ), + Param( + name: "score", + ty: u64, + descr: Doc(md: "New score"), + ), + Param( + name: "force", + ty: Option(bool), + descr: Doc(md: "Pass True, if the high score is allowed to decrease. This can be useful when fixing mistakes or banning cheaters"), + ), + Param( + name: "disable_edit_message", + ty: Option(bool), + descr: Doc(md: "Pass True, if the game message should not be automatically edited to include the current scoreboard"), + ), + Param( + name: "inline_message_id", + ty: String, + descr: Doc(md: "Identifier of the inline message"), + ), + ], + sibling: Some("setGameScore"), + ), + Method( + names: ("getGameHighScores", "GetGameHighScores", "get_game_high_scores"), + return_ty: True, + doc: Doc( + md: "Use this method to get data for high score tables. Will return the score of the specified user and several of their neighbors in a game. On success, returns an Array of [GameHighScore] objects.\n\n> This method will currently return scores for the target user, plus two of their closest neighbors on each side. Will also return the top three users if the user and his neighbors are not among them. Please note that this behavior is subject to change.", + md_links: {"GameHighScore": "https://core.telegram.org/bots/api#gamehighscore"}, + ), + tg_doc: "https://core.telegram.org/bots/api#getgamehighscores", + tg_category: "Games", + params: [ + Param( + name: "user_id", + ty: RawTy("UserId"), + descr: Doc(md: "User identifier"), + ), + Param( + name: "target", + ty: RawTy("TargetMessage"), + descr: Doc(md: "Target message") + ) + ], + ), + ], + tg_categories: { + "Getting updates": "https://core.telegram.org/bots/api#getting-updates", + "Available methods": "https://core.telegram.org/bots/api#available-methods", + "Inline Mode": "https://core.telegram.org/bots/api#inline-mode", + "Updating messages": "https://core.telegram.org/bots/api#updating-messages", + "Stickers": "https://core.telegram.org/bots/api#stickers", + "Payments": "https://core.telegram.org/bots/api#payments", + "Telegram Passport": "https://core.telegram.org/bots/api#telegram-passport", + "Games": "https://core.telegram.org/bots/api#games", + } +) diff --git a/crates/teloxide-core/src/adaptors.rs b/crates/teloxide-core/src/adaptors.rs new file mode 100644 index 00000000..c54a65ec --- /dev/null +++ b/crates/teloxide-core/src/adaptors.rs @@ -0,0 +1,62 @@ +//! Wrappers altering functionality of a bot. +//! +//! Bot adaptors are very similar to the [`Iterator`] adaptors: they are bots +//! wrapping other bots to alter existing or add new functionality. +//! +//! [`Requester`]: crate::requests::Requester + +/// [`AutoSend`] bot adaptor which used to allow sending a request without +/// calling [`send`]. +/// +/// [`AutoSend`]: auto_send::AutoSend +/// [`send`]: crate::requests::Request::send +#[cfg(feature = "auto_send")] +#[deprecated( + since = "0.8.0", + note = "`AutoSend` is no longer required to `.await` requests and is now noop" +)] +pub mod auto_send; + +/// [`CacheMe`] bot adaptor which caches [`GetMe`] requests. +/// +/// [`CacheMe`]: cache_me::CacheMe +/// [`GetMe`]: crate::payloads::GetMe +#[cfg(feature = "cache_me")] +pub mod cache_me; + +/// [`Trace`] bot adaptor which traces requests. +/// +/// [`Trace`]: trace::Trace +#[cfg(feature = "trace_adaptor")] +pub mod trace; + +/// [`ErasedRequester`] bot adaptor which allows to erase type of +/// [`Requester`]. +/// +/// [`ErasedRequester`]: erased::ErasedRequester +/// [`Requester`]: crate::requests::Requester +#[cfg(feature = "erased")] +pub mod erased; + +/// [`Throttle`] bot adaptor which allows automatically throttle when hitting +/// API limits. +/// +/// [`Throttle`]: throttle::Throttle +#[cfg(feature = "throttle")] +pub mod throttle; + +mod parse_mode; + +#[cfg(feature = "auto_send")] +#[allow(deprecated)] +pub use auto_send::AutoSend; +#[cfg(feature = "cache_me")] +pub use cache_me::CacheMe; +#[cfg(feature = "erased")] +pub use erased::ErasedRequester; +#[cfg(feature = "throttle")] +pub use throttle::Throttle; +#[cfg(feature = "trace_adaptor")] +pub use trace::Trace; + +pub use parse_mode::DefaultParseMode; diff --git a/crates/teloxide-core/src/adaptors/auto_send.rs b/crates/teloxide-core/src/adaptors/auto_send.rs new file mode 100644 index 00000000..607d69c0 --- /dev/null +++ b/crates/teloxide-core/src/adaptors/auto_send.rs @@ -0,0 +1,222 @@ +use std::future::IntoFuture; + +use url::Url; + +use crate::{ + requests::{HasPayload, Output, Request, Requester}, + types::*, +}; + +/// Previously was used to send requests automatically. +/// +/// Before addition of [`IntoFuture`] you could only `.await` [`Future`]s. +/// This adaptor turned requests into futures, allowing to `.await` them, +/// without calling `.send()`. +/// +/// Now, however, all requests are required to implement `IntoFuture`, allowing +/// you to `.await` them directly. This adaptor is noop, and shouldn't be used. +/// +/// [`Future`]: std::future::Future +#[derive(Clone, Debug)] +pub struct AutoSend { + bot: B, +} + +impl AutoSend { + /// Creates new `AutoSend`. + /// + /// Note: it's recommended to use [`RequesterExt::auto_send`] instead. + /// + /// [`RequesterExt::auto_send`]: crate::requests::RequesterExt::auto_send + pub fn new(inner: B) -> AutoSend { + Self { bot: inner } + } + + /// Allows to access the inner bot. + pub fn inner(&self) -> &B { + &self.bot + } + + /// Unwraps the inner bot. + pub fn into_inner(self) -> B { + self.bot + } +} + +macro_rules! f { + ($m:ident $this:ident ($($arg:ident : $T:ty),*)) => { + AutoRequest::new($this.inner().$m($($arg),*)) + }; +} + +macro_rules! fty { + ($T:ident) => { + AutoRequest + }; +} + +impl Requester for AutoSend +where + B: Requester, +{ + type Err = B::Err; + + requester_forward! { + get_me, + log_out, + close, + get_updates, + set_webhook, + delete_webhook, + get_webhook_info, + forward_message, + copy_message, + send_message, + send_photo, + send_audio, + send_document, + send_video, + send_animation, + send_voice, + send_video_note, + send_media_group, + send_location, + edit_message_live_location, + edit_message_live_location_inline, + stop_message_live_location, + stop_message_live_location_inline, + send_venue, + send_contact, + send_poll, + send_dice, + send_chat_action, + get_user_profile_photos, + get_file, + kick_chat_member, + ban_chat_member, + unban_chat_member, + restrict_chat_member, + promote_chat_member, + set_chat_administrator_custom_title, + ban_chat_sender_chat, + unban_chat_sender_chat, + set_chat_permissions, + export_chat_invite_link, + create_chat_invite_link, + edit_chat_invite_link, + revoke_chat_invite_link, + set_chat_photo, + delete_chat_photo, + set_chat_title, + set_chat_description, + pin_chat_message, + unpin_chat_message, + unpin_all_chat_messages, + leave_chat, + get_chat, + get_chat_administrators, + get_chat_members_count, + get_chat_member_count, + get_chat_member, + set_chat_sticker_set, + delete_chat_sticker_set, + answer_callback_query, + set_my_commands, + get_my_commands, + set_chat_menu_button, + get_chat_menu_button, + set_my_default_administrator_rights, + get_my_default_administrator_rights, + delete_my_commands, + answer_inline_query, + answer_web_app_query, + edit_message_text, + edit_message_text_inline, + edit_message_caption, + edit_message_caption_inline, + edit_message_media, + edit_message_media_inline, + edit_message_reply_markup, + edit_message_reply_markup_inline, + stop_poll, + delete_message, + send_sticker, + get_sticker_set, + get_custom_emoji_stickers, + upload_sticker_file, + create_new_sticker_set, + add_sticker_to_set, + set_sticker_position_in_set, + delete_sticker_from_set, + set_sticker_set_thumb, + send_invoice, + create_invoice_link, + answer_shipping_query, + answer_pre_checkout_query, + set_passport_data_errors, + send_game, + set_game_score, + set_game_score_inline, + get_game_high_scores, + approve_chat_join_request, + decline_chat_join_request + => f, fty + } +} + +download_forward! { + 'w + B + AutoSend + { this => this.inner() } +} + +#[must_use = "Futures are lazy and do nothing unless polled or awaited"] +pub struct AutoRequest(R); + +impl AutoRequest +where + R: Request, +{ + pub fn new(inner: R) -> Self { + Self(inner) + } +} + +impl Request for AutoRequest +where + R: Request, +{ + type Err = R::Err; + type Send = R::Send; + type SendRef = R::SendRef; + + fn send(self) -> Self::Send { + self.0.send() + } + + fn send_ref(&self) -> Self::SendRef { + self.0.send_ref() + } +} + +impl IntoFuture for AutoRequest { + type Output = Result, ::Err>; + type IntoFuture = ::Send; + + fn into_future(self) -> Self::IntoFuture { + self.send() + } +} + +impl HasPayload for AutoRequest { + type Payload = R::Payload; + + fn payload_mut(&mut self) -> &mut Self::Payload { + self.0.payload_mut() + } + + fn payload_ref(&self) -> &Self::Payload { + self.0.payload_ref() + } +} diff --git a/crates/teloxide-core/src/adaptors/cache_me.rs b/crates/teloxide-core/src/adaptors/cache_me.rs new file mode 100644 index 00000000..86ee8118 --- /dev/null +++ b/crates/teloxide-core/src/adaptors/cache_me.rs @@ -0,0 +1,297 @@ +use std::{future::IntoFuture, pin::Pin, sync::Arc}; + +use futures::{ + future, + future::{ok, Ready}, + task::{Context, Poll}, + Future, +}; +use once_cell::sync::OnceCell; +use url::Url; + +use crate::{ + payloads::GetMe, + requests::{HasPayload, Request, Requester}, + types::{Me, Recipient, *}, +}; + +/// `get_me` cache. +/// +/// Bot's user is hardly ever changed, so sometimes it's reasonable to cache +/// response from `get_me` method. +#[derive(Clone, Debug)] +pub struct CacheMe { + bot: B, + me: Arc>, +} + +impl CacheMe { + /// Creates new cache. + /// + /// Note: it's recommended to use [`RequesterExt::cache_me`] instead. + /// + /// [`RequesterExt::cache_me`]: crate::requests::RequesterExt::cache_me + pub fn new(bot: B) -> CacheMe { + Self { bot, me: Arc::new(OnceCell::new()) } + } + + /// Allows to access inner bot + pub fn inner(&self) -> &B { + &self.bot + } + + /// Unwraps inner bot + pub fn into_inner(self) -> B { + self.bot + } + + /// Clear cache. + /// + /// Returns cached response from `get_me`, if it was cached. + /// + /// Note: internally this uses [`Arc::make_mut`] so this will **not** + /// clear cache of clones of self. + pub fn clear(&mut self) -> Option { + Arc::make_mut(&mut self.me).take() + } +} + +macro_rules! f { + ($m:ident $this:ident ($($arg:ident : $T:ty),*)) => { + $this.inner().$m($($arg),*) + }; +} + +macro_rules! fty { + ($T:ident) => { + B::$T + }; +} + +impl Requester for CacheMe +where + B: Requester, +{ + type Err = B::Err; + + type GetMe = CachedMeRequest; + + fn get_me(&self) -> Self::GetMe { + match self.me.get() { + Some(me) => CachedMeRequest(Inner::Ready(me.clone()), GetMe::new()), + None => CachedMeRequest( + Inner::Pending(self.bot.get_me(), Arc::clone(&self.me)), + GetMe::new(), + ), + } + } + + requester_forward! { + log_out, + close, + get_updates, + set_webhook, + delete_webhook, + get_webhook_info, + forward_message, + copy_message, + send_message, + send_photo, + send_audio, + send_document, + send_video, + send_animation, + send_voice, + send_video_note, + send_media_group, + send_location, + edit_message_live_location, + edit_message_live_location_inline, + stop_message_live_location, + stop_message_live_location_inline, + send_venue, + send_contact, + send_poll, + send_dice, + send_chat_action, + get_user_profile_photos, + get_file, + kick_chat_member, + ban_chat_member, + unban_chat_member, + restrict_chat_member, + promote_chat_member, + set_chat_administrator_custom_title, + ban_chat_sender_chat, + unban_chat_sender_chat, + set_chat_permissions, + export_chat_invite_link, + create_chat_invite_link, + edit_chat_invite_link, + revoke_chat_invite_link, + set_chat_photo, + delete_chat_photo, + set_chat_title, + set_chat_description, + pin_chat_message, + unpin_chat_message, + unpin_all_chat_messages, + leave_chat, + get_chat, + get_chat_administrators, + get_chat_members_count, + get_chat_member_count, + get_chat_member, + set_chat_sticker_set, + delete_chat_sticker_set, + answer_callback_query, + set_my_commands, + get_my_commands, + set_chat_menu_button, + get_chat_menu_button, + set_my_default_administrator_rights, + get_my_default_administrator_rights, + delete_my_commands, + answer_inline_query, + answer_web_app_query, + edit_message_text, + edit_message_text_inline, + edit_message_caption, + edit_message_caption_inline, + edit_message_media, + edit_message_media_inline, + edit_message_reply_markup, + edit_message_reply_markup_inline, + stop_poll, + delete_message, + send_sticker, + get_sticker_set, + get_custom_emoji_stickers, + upload_sticker_file, + create_new_sticker_set, + add_sticker_to_set, + set_sticker_position_in_set, + delete_sticker_from_set, + set_sticker_set_thumb, + send_invoice, + create_invoice_link, + answer_shipping_query, + answer_pre_checkout_query, + set_passport_data_errors, + send_game, + set_game_score, + set_game_score_inline, + get_game_high_scores, + approve_chat_join_request, + decline_chat_join_request + => f, fty + } +} + +download_forward! { + 'w + B + CacheMe + { this => this.inner() } +} + +#[must_use = "Requests are lazy and do nothing unless sent"] +pub struct CachedMeRequest>(Inner, GetMe); + +enum Inner> { + Ready(Me), + Pending(R, Arc>), +} + +impl Request for CachedMeRequest +where + R: Request, +{ + type Err = R::Err; + type Send = Send; + type SendRef = SendRef; + + fn send(self) -> Self::Send { + let fut = match self.0 { + Inner::Ready(me) => future::Either::Left(ok(me)), + Inner::Pending(req, cell) => future::Either::Right(Init(req.send(), cell)), + }; + Send(fut) + } + + fn send_ref(&self) -> Self::SendRef { + let fut = match &self.0 { + Inner::Ready(me) => future::Either::Left(ok(me.clone())), + Inner::Pending(req, cell) => { + future::Either::Right(Init(req.send_ref(), Arc::clone(cell))) + } + }; + SendRef(fut) + } +} + +impl> HasPayload for CachedMeRequest { + type Payload = GetMe; + + fn payload_mut(&mut self) -> &mut Self::Payload { + &mut self.1 + } + + fn payload_ref(&self) -> &Self::Payload { + &self.1 + } +} + +impl> IntoFuture for CachedMeRequest { + type Output = Result; + type IntoFuture = Send; + + fn into_future(self) -> Self::IntoFuture { + self.send() + } +} + +type ReadyMe = Ready>; + +#[pin_project::pin_project] +pub struct Send>( + #[pin] future::Either, Init>, +); + +impl> Future for Send { + type Output = Result; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let this = self.project(); + this.0.poll(cx) + } +} + +#[pin_project::pin_project] +pub struct SendRef>( + #[pin] future::Either, Init>, +); + +impl> Future for SendRef { + type Output = Result; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let this = self.project(); + this.0.poll(cx) + } +} + +#[pin_project::pin_project] +struct Init(#[pin] F, Arc>); + +impl>, T: Clone, E> Future for Init { + type Output = Result; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let this = self.project(); + match this.0.poll(cx) { + Poll::Ready(Ok(ok)) => Poll::Ready(Ok(this.1.get_or_init(|| ok).clone())), + poll @ Poll::Ready(_) | poll @ Poll::Pending => poll, + } + } +} diff --git a/crates/teloxide-core/src/adaptors/erased.rs b/crates/teloxide-core/src/adaptors/erased.rs new file mode 100644 index 00000000..f5d2d6bf --- /dev/null +++ b/crates/teloxide-core/src/adaptors/erased.rs @@ -0,0 +1,1594 @@ +use std::{future::IntoFuture, sync::Arc}; + +use futures::{future::BoxFuture, FutureExt}; +use reqwest::Url; + +use crate::{ + payloads::*, + requests::{HasPayload, Output, Payload, Request, Requester}, + types::*, +}; + +/// [`Requester`] with erased type. +pub struct ErasedRequester<'a, E> { + inner: Arc + 'a>, +} + +impl<'a, E> ErasedRequester<'a, E> { + /// Erases type of `requester` + /// + /// Note: it's recommended to use [`RequesterExt::erase`] instead. + /// + /// [`RequesterExt::erase`]: crate::requests::RequesterExt::erase + pub fn new(requester: B) -> Self + where + B: Requester + 'a, + { + Self { inner: Arc::new(requester) } + } +} + +impl std::fmt::Debug for ErasedRequester<'_, E> { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + f.debug_struct("ErasedRequester").finish_non_exhaustive() + } +} + +// NB. hand-written impl to avoid `E: Clone` bound +impl Clone for ErasedRequester<'_, E> { + fn clone(&self) -> Self { + Self { inner: Arc::clone(&self.inner) } + } +} + +/// [`Request`] with erased type. +#[must_use = "Requests are lazy and do nothing unless sent"] +pub struct ErasedRequest<'a, T, E> { + inner: Box + 'a>, +} + +// `T: Payload` required b/c of +impl<'a, T: Payload, E> ErasedRequest<'a, T, E> { + pub(crate) fn erase(request: impl Request + 'a) -> Self { + Self { inner: Box::new(request) } + } +} + +impl HasPayload for ErasedRequest<'_, T, E> +where + T: Payload, +{ + type Payload = T; + + fn payload_mut(&mut self) -> &mut Self::Payload { + self.inner.payload_mut() + } + + fn payload_ref(&self) -> &Self::Payload { + self.inner.payload_ref() + } +} + +impl<'a, T, E> Request for ErasedRequest<'a, T, E> +where + T: Payload, + E: std::error::Error + Send, +{ + type Err = E; + + type Send = BoxFuture<'a, Result, Self::Err>>; + + type SendRef = BoxFuture<'a, Result, Self::Err>>; + + fn send(self) -> Self::Send { + self.inner.send_box() + } + + fn send_ref(&self) -> Self::SendRef { + self.inner.send_ref() + } +} + +impl<'a, T, E> IntoFuture for ErasedRequest<'a, T, E> +where + T: Payload, + E: std::error::Error + Send, +{ + type Output = Result, ::Err>; + type IntoFuture = ::Send; + + fn into_future(self) -> Self::IntoFuture { + self.send() + } +} + +/// Object safe version of [`Request`]. +/// +/// TODO(waffle): make [`Request`] object safe and remove this trait (this is a +/// breaking change) +trait ErasableRequest<'a>: HasPayload { + type Err: std::error::Error + Send; + + fn send_box(self: Box) -> BoxFuture<'a, Result, Self::Err>>; + + fn send_ref(&self) -> BoxFuture<'a, Result, Self::Err>>; +} + +impl<'a, R> ErasableRequest<'a> for R +where + R: Request, + ::Send: 'a, + ::SendRef: 'a, +{ + type Err = R::Err; + + fn send_box(self: Box) -> BoxFuture<'a, Result, Self::Err>> { + self.send().boxed() + } + + fn send_ref(&self) -> BoxFuture<'a, Result, Self::Err>> { + Request::send_ref(self).boxed() + } +} + +macro_rules! fty { + ($T:ident) => { + ErasedRequest<'a, $T, Err> + }; +} + +macro_rules! fwd_erased { + ($m:ident $this:ident ($($arg:ident : $T:ty),*)) => { + $this.inner.$m($( fwd_erased!(@convert $m, $arg, $arg : $T) ),*) + }; + + (@convert send_media_group, $arg:ident, media : $T:ty) => { + $arg.into_iter().collect() + }; + (@convert $m:ident, $arg:ident, options : $T:ty) => { + $arg.into_iter().collect() + }; + (@convert $m:ident, $arg:ident, commands : $T:ty) => { + $arg.into_iter().collect() + }; + (@convert $m:ident, $arg:ident, results : $T:ty) => { + $arg.into_iter().collect() + }; + (@convert $m:ident, $arg:ident, prices : $T:ty) => { + $arg.into_iter().collect() + }; + (@convert $m:ident, $arg:ident, errors : $T:ty) => { + $arg.into_iter().collect() + }; + (@convert $m:ident, $arg:ident, custom_emoji_ids : $T:ty) => { + $arg.into_iter().collect() + }; + (@convert $m:ident, $arg:ident, $arg_:ident : $T:ty) => { + $arg.into() + }; +} + +impl<'a, Err> Requester for ErasedRequester<'a, Err> +where + Err: std::error::Error + Send, +{ + type Err = Err; + + requester_forward! { + get_me, + log_out, + close, + get_updates, + set_webhook, + delete_webhook, + get_webhook_info, + forward_message, + copy_message, + send_message, + send_photo, + send_audio, + send_document, + send_video, + send_animation, + send_voice, + send_video_note, + send_media_group, + send_location, + edit_message_live_location, + edit_message_live_location_inline, + stop_message_live_location, + stop_message_live_location_inline, + send_venue, + send_contact, + send_poll, + send_dice, + send_chat_action, + get_user_profile_photos, + get_file, + kick_chat_member, + ban_chat_member, + unban_chat_member, + restrict_chat_member, + promote_chat_member, + set_chat_administrator_custom_title, + ban_chat_sender_chat, + unban_chat_sender_chat, + set_chat_permissions, + export_chat_invite_link, + create_chat_invite_link, + edit_chat_invite_link, + revoke_chat_invite_link, + set_chat_photo, + delete_chat_photo, + set_chat_title, + set_chat_description, + pin_chat_message, + unpin_chat_message, + unpin_all_chat_messages, + leave_chat, + get_chat, + get_chat_administrators, + get_chat_members_count, + get_chat_member_count, + get_chat_member, + set_chat_sticker_set, + delete_chat_sticker_set, + answer_callback_query, + set_my_commands, + get_my_commands, + set_chat_menu_button, + get_chat_menu_button, + set_my_default_administrator_rights, + get_my_default_administrator_rights, + delete_my_commands, + answer_inline_query, + answer_web_app_query, + edit_message_text, + edit_message_text_inline, + edit_message_caption, + edit_message_caption_inline, + edit_message_media, + edit_message_media_inline, + edit_message_reply_markup, + edit_message_reply_markup_inline, + stop_poll, + delete_message, + send_sticker, + get_sticker_set, + get_custom_emoji_stickers, + upload_sticker_file, + create_new_sticker_set, + add_sticker_to_set, + set_sticker_position_in_set, + delete_sticker_from_set, + set_sticker_set_thumb, + send_invoice, + create_invoice_link, + answer_shipping_query, + answer_pre_checkout_query, + set_passport_data_errors, + send_game, + set_game_score, + set_game_score_inline, + get_game_high_scores, + approve_chat_join_request, + decline_chat_join_request + => fwd_erased, fty + } +} + +/// Object safe version of [`Requester`]. +trait ErasableRequester<'a> { + /// Error type returned by all requests. + type Err: std::error::Error + Send; + + fn get_updates(&self) -> ErasedRequest<'a, GetUpdates, Self::Err>; + + fn set_webhook(&self, url: Url) -> ErasedRequest<'a, SetWebhook, Self::Err>; + + fn delete_webhook(&self) -> ErasedRequest<'a, DeleteWebhook, Self::Err>; + + fn get_webhook_info(&self) -> ErasedRequest<'a, GetWebhookInfo, Self::Err>; + + fn get_me(&self) -> ErasedRequest<'a, GetMe, Self::Err>; + + fn log_out(&self) -> ErasedRequest<'a, LogOut, Self::Err>; + + fn close(&self) -> ErasedRequest<'a, Close, Self::Err>; + + fn send_message( + &self, + chat_id: Recipient, + text: String, + ) -> ErasedRequest<'a, SendMessage, Self::Err>; + + fn forward_message( + &self, + chat_id: Recipient, + from_chat_id: Recipient, + message_id: MessageId, + ) -> ErasedRequest<'a, ForwardMessage, Self::Err>; + + fn copy_message( + &self, + chat_id: Recipient, + from_chat_id: Recipient, + message_id: MessageId, + ) -> ErasedRequest<'a, CopyMessage, Self::Err>; + + fn send_photo( + &self, + chat_id: Recipient, + photo: InputFile, + ) -> ErasedRequest<'a, SendPhoto, Self::Err>; + + fn send_audio( + &self, + chat_id: Recipient, + audio: InputFile, + ) -> ErasedRequest<'a, SendAudio, Self::Err>; + + fn send_document( + &self, + chat_id: Recipient, + document: InputFile, + ) -> ErasedRequest<'a, SendDocument, Self::Err>; + + fn send_video( + &self, + chat_id: Recipient, + video: InputFile, + ) -> ErasedRequest<'a, SendVideo, Self::Err>; + + fn send_animation( + &self, + chat_id: Recipient, + animation: InputFile, + ) -> ErasedRequest<'a, SendAnimation, Self::Err>; + + fn send_voice( + &self, + chat_id: Recipient, + voice: InputFile, + ) -> ErasedRequest<'a, SendVoice, Self::Err>; + + fn send_video_note( + &self, + chat_id: Recipient, + video_note: InputFile, + ) -> ErasedRequest<'a, SendVideoNote, Self::Err>; + + fn send_media_group( + &self, + chat_id: Recipient, + media: Vec, + ) -> ErasedRequest<'a, SendMediaGroup, Self::Err>; + + fn send_location( + &self, + chat_id: Recipient, + latitude: f64, + longitude: f64, + ) -> ErasedRequest<'a, SendLocation, Self::Err>; + + fn edit_message_live_location( + &self, + chat_id: Recipient, + message_id: MessageId, + latitude: f64, + longitude: f64, + ) -> ErasedRequest<'a, EditMessageLiveLocation, Self::Err>; + + fn edit_message_live_location_inline( + &self, + inline_message_id: String, + latitude: f64, + longitude: f64, + ) -> ErasedRequest<'a, EditMessageLiveLocationInline, Self::Err>; + + fn stop_message_live_location( + &self, + chat_id: Recipient, + message_id: MessageId, + latitude: f64, + longitude: f64, + ) -> ErasedRequest<'a, StopMessageLiveLocation, Self::Err>; + + fn stop_message_live_location_inline( + &self, + inline_message_id: String, + latitude: f64, + longitude: f64, + ) -> ErasedRequest<'a, StopMessageLiveLocationInline, Self::Err>; + + fn send_venue( + &self, + chat_id: Recipient, + latitude: f64, + longitude: f64, + title: String, + address: String, + ) -> ErasedRequest<'a, SendVenue, Self::Err>; + + fn send_contact( + &self, + chat_id: Recipient, + phone_number: String, + first_name: String, + ) -> ErasedRequest<'a, SendContact, Self::Err>; + + fn send_poll( + &self, + chat_id: Recipient, + question: String, + options: Vec, + ) -> ErasedRequest<'a, SendPoll, Self::Err>; + + fn send_dice(&self, chat_id: Recipient) -> ErasedRequest<'a, SendDice, Self::Err>; + + fn send_chat_action( + &self, + chat_id: Recipient, + action: ChatAction, + ) -> ErasedRequest<'a, SendChatAction, Self::Err>; + + fn get_user_profile_photos( + &self, + user_id: UserId, + ) -> ErasedRequest<'a, GetUserProfilePhotos, Self::Err>; + + fn get_file(&self, file_id: String) -> ErasedRequest<'a, GetFile, Self::Err>; + + fn ban_chat_member( + &self, + chat_id: Recipient, + user_id: UserId, + ) -> ErasedRequest<'a, BanChatMember, Self::Err>; + + fn kick_chat_member( + &self, + chat_id: Recipient, + user_id: UserId, + ) -> ErasedRequest<'a, KickChatMember, Self::Err>; + + fn unban_chat_member( + &self, + chat_id: Recipient, + user_id: UserId, + ) -> ErasedRequest<'a, UnbanChatMember, Self::Err>; + + fn restrict_chat_member( + &self, + chat_id: Recipient, + user_id: UserId, + permissions: ChatPermissions, + ) -> ErasedRequest<'a, RestrictChatMember, Self::Err>; + + fn promote_chat_member( + &self, + chat_id: Recipient, + user_id: UserId, + ) -> ErasedRequest<'a, PromoteChatMember, Self::Err>; + + fn set_chat_administrator_custom_title( + &self, + chat_id: Recipient, + user_id: UserId, + custom_title: String, + ) -> ErasedRequest<'a, SetChatAdministratorCustomTitle, Self::Err>; + + fn ban_chat_sender_chat( + &self, + chat_id: Recipient, + sender_chat_id: ChatId, + ) -> ErasedRequest<'a, BanChatSenderChat, Self::Err>; + + fn unban_chat_sender_chat( + &self, + chat_id: Recipient, + sender_chat_id: ChatId, + ) -> ErasedRequest<'a, UnbanChatSenderChat, Self::Err>; + + fn set_chat_permissions( + &self, + chat_id: Recipient, + permissions: ChatPermissions, + ) -> ErasedRequest<'a, SetChatPermissions, Self::Err>; + + fn export_chat_invite_link( + &self, + chat_id: Recipient, + ) -> ErasedRequest<'a, ExportChatInviteLink, Self::Err>; + + fn create_chat_invite_link( + &self, + chat_id: Recipient, + ) -> ErasedRequest<'a, CreateChatInviteLink, Self::Err>; + + fn edit_chat_invite_link( + &self, + chat_id: Recipient, + invite_link: String, + ) -> ErasedRequest<'a, EditChatInviteLink, Self::Err>; + + fn revoke_chat_invite_link( + &self, + chat_id: Recipient, + invite_link: String, + ) -> ErasedRequest<'a, RevokeChatInviteLink, Self::Err>; + + /// For Telegram documentation see [`ApproveChatJoinRequest`]. + fn approve_chat_join_request( + &self, + chat_id: Recipient, + user_id: UserId, + ) -> ErasedRequest<'a, ApproveChatJoinRequest, Self::Err>; + + /// For Telegram documentation see [`DeclineChatJoinRequest`]. + fn decline_chat_join_request( + &self, + chat_id: Recipient, + user_id: UserId, + ) -> ErasedRequest<'a, DeclineChatJoinRequest, Self::Err>; + + fn set_chat_photo( + &self, + chat_id: Recipient, + photo: InputFile, + ) -> ErasedRequest<'a, SetChatPhoto, Self::Err>; + + fn delete_chat_photo( + &self, + chat_id: Recipient, + ) -> ErasedRequest<'a, DeleteChatPhoto, Self::Err>; + + fn set_chat_title( + &self, + chat_id: Recipient, + title: String, + ) -> ErasedRequest<'a, SetChatTitle, Self::Err>; + + fn set_chat_description( + &self, + chat_id: Recipient, + ) -> ErasedRequest<'a, SetChatDescription, Self::Err>; + + fn pin_chat_message( + &self, + chat_id: Recipient, + message_id: MessageId, + ) -> ErasedRequest<'a, PinChatMessage, Self::Err>; + + fn unpin_chat_message( + &self, + chat_id: Recipient, + ) -> ErasedRequest<'a, UnpinChatMessage, Self::Err>; + + fn unpin_all_chat_messages( + &self, + chat_id: Recipient, + ) -> ErasedRequest<'a, UnpinAllChatMessages, Self::Err>; + + fn leave_chat(&self, chat_id: Recipient) -> ErasedRequest<'a, LeaveChat, Self::Err>; + + fn get_chat(&self, chat_id: Recipient) -> ErasedRequest<'a, GetChat, Self::Err>; + + fn get_chat_administrators( + &self, + chat_id: Recipient, + ) -> ErasedRequest<'a, GetChatAdministrators, Self::Err>; + + fn get_chat_member_count( + &self, + chat_id: Recipient, + ) -> ErasedRequest<'a, GetChatMemberCount, Self::Err>; + + fn get_chat_members_count( + &self, + chat_id: Recipient, + ) -> ErasedRequest<'a, GetChatMembersCount, Self::Err>; + + fn get_chat_member( + &self, + chat_id: Recipient, + user_id: UserId, + ) -> ErasedRequest<'a, GetChatMember, Self::Err>; + + fn set_chat_sticker_set( + &self, + chat_id: Recipient, + sticker_set_name: String, + ) -> ErasedRequest<'a, SetChatStickerSet, Self::Err>; + + fn delete_chat_sticker_set( + &self, + chat_id: Recipient, + ) -> ErasedRequest<'a, DeleteChatStickerSet, Self::Err>; + + fn answer_callback_query( + &self, + callback_query_id: String, + ) -> ErasedRequest<'a, AnswerCallbackQuery, Self::Err>; + + fn set_my_commands( + &self, + commands: Vec, + ) -> ErasedRequest<'a, SetMyCommands, Self::Err>; + + fn get_my_commands(&self) -> ErasedRequest<'a, GetMyCommands, Self::Err>; + + fn set_chat_menu_button(&self) -> ErasedRequest<'a, SetChatMenuButton, Self::Err>; + + fn get_chat_menu_button(&self) -> ErasedRequest<'a, GetChatMenuButton, Self::Err>; + + fn set_my_default_administrator_rights( + &self, + ) -> ErasedRequest<'a, SetMyDefaultAdministratorRights, Self::Err>; + + fn get_my_default_administrator_rights( + &self, + ) -> ErasedRequest<'a, GetMyDefaultAdministratorRights, Self::Err>; + + fn delete_my_commands(&self) -> ErasedRequest<'a, DeleteMyCommands, Self::Err>; + + fn answer_inline_query( + &self, + inline_query_id: String, + results: Vec, + ) -> ErasedRequest<'a, AnswerInlineQuery, Self::Err>; + + fn answer_web_app_query( + &self, + web_app_query_id: String, + result: InlineQueryResult, + ) -> ErasedRequest<'a, AnswerWebAppQuery, Self::Err>; + + fn edit_message_text( + &self, + chat_id: Recipient, + message_id: MessageId, + text: String, + ) -> ErasedRequest<'a, EditMessageText, Self::Err>; + + fn edit_message_text_inline( + &self, + inline_message_id: String, + text: String, + ) -> ErasedRequest<'a, EditMessageTextInline, Self::Err>; + + fn edit_message_caption( + &self, + chat_id: Recipient, + message_id: MessageId, + ) -> ErasedRequest<'a, EditMessageCaption, Self::Err>; + + fn edit_message_caption_inline( + &self, + inline_message_id: String, + ) -> ErasedRequest<'a, EditMessageCaptionInline, Self::Err>; + + fn edit_message_media( + &self, + chat_id: Recipient, + message_id: MessageId, + media: InputMedia, + ) -> ErasedRequest<'a, EditMessageMedia, Self::Err>; + + fn edit_message_media_inline( + &self, + inline_message_id: String, + media: InputMedia, + ) -> ErasedRequest<'a, EditMessageMediaInline, Self::Err>; + + fn edit_message_reply_markup( + &self, + chat_id: Recipient, + message_id: MessageId, + ) -> ErasedRequest<'a, EditMessageReplyMarkup, Self::Err>; + + fn edit_message_reply_markup_inline( + &self, + inline_message_id: String, + ) -> ErasedRequest<'a, EditMessageReplyMarkupInline, Self::Err>; + + fn stop_poll( + &self, + chat_id: Recipient, + message_id: MessageId, + ) -> ErasedRequest<'a, StopPoll, Self::Err>; + + fn delete_message( + &self, + chat_id: Recipient, + message_id: MessageId, + ) -> ErasedRequest<'a, DeleteMessage, Self::Err>; + + fn send_sticker( + &self, + chat_id: Recipient, + sticker: InputFile, + ) -> ErasedRequest<'a, SendSticker, Self::Err>; + + fn get_sticker_set(&self, name: String) -> ErasedRequest<'a, GetStickerSet, Self::Err>; + + fn get_custom_emoji_stickers( + &self, + custom_emoji_ids: Vec, + ) -> ErasedRequest<'a, GetCustomEmojiStickers, Self::Err>; + + fn upload_sticker_file( + &self, + user_id: UserId, + png_sticker: InputFile, + ) -> ErasedRequest<'a, UploadStickerFile, Self::Err>; + + fn create_new_sticker_set( + &self, + user_id: UserId, + name: String, + title: String, + sticker: InputSticker, + emojis: String, + ) -> ErasedRequest<'a, CreateNewStickerSet, Self::Err>; + + fn add_sticker_to_set( + &self, + user_id: UserId, + name: String, + sticker: InputSticker, + emojis: String, + ) -> ErasedRequest<'a, AddStickerToSet, Self::Err>; + + fn set_sticker_position_in_set( + &self, + sticker: String, + position: u32, + ) -> ErasedRequest<'a, SetStickerPositionInSet, Self::Err>; + + fn delete_sticker_from_set( + &self, + sticker: String, + ) -> ErasedRequest<'a, DeleteStickerFromSet, Self::Err>; + + fn set_sticker_set_thumb( + &self, + name: String, + user_id: UserId, + ) -> ErasedRequest<'a, SetStickerSetThumb, Self::Err>; + + // we can't change telegram API + #[allow(clippy::too_many_arguments)] + fn send_invoice( + &self, + chat_id: Recipient, + title: String, + description: String, + payload: String, + provider_token: String, + currency: String, + prices: Vec, + ) -> ErasedRequest<'a, SendInvoice, Self::Err>; + + #[allow(clippy::too_many_arguments)] + fn create_invoice_link( + &self, + title: String, + description: String, + payload: String, + provider_token: String, + currency: String, + prices: Vec, + ) -> ErasedRequest<'a, CreateInvoiceLink, Self::Err>; + + fn answer_shipping_query( + &self, + shipping_query_id: String, + ok: bool, + ) -> ErasedRequest<'a, AnswerShippingQuery, Self::Err>; + + fn answer_pre_checkout_query( + &self, + pre_checkout_query_id: String, + ok: bool, + ) -> ErasedRequest<'a, AnswerPreCheckoutQuery, Self::Err>; + + fn set_passport_data_errors( + &self, + user_id: UserId, + errors: Vec, + ) -> ErasedRequest<'a, SetPassportDataErrors, Self::Err>; + + fn send_game( + &self, + chat_id: u32, + game_short_name: String, + ) -> ErasedRequest<'a, SendGame, Self::Err>; + + fn set_game_score( + &self, + user_id: UserId, + score: u64, + chat_id: u32, + message_id: MessageId, + ) -> ErasedRequest<'a, SetGameScore, Self::Err>; + + fn set_game_score_inline( + &self, + user_id: UserId, + score: u64, + inline_message_id: String, + ) -> ErasedRequest<'a, SetGameScoreInline, Self::Err>; + + fn get_game_high_scores( + &self, + user_id: UserId, + target: TargetMessage, + ) -> ErasedRequest<'a, GetGameHighScores, Self::Err>; +} + +impl<'a, B> ErasableRequester<'a> for B +where + B: Requester + 'a, +{ + type Err = B::Err; + + fn get_updates(&self) -> ErasedRequest<'a, GetUpdates, Self::Err> { + Requester::get_updates(self).erase() + } + + fn set_webhook(&self, url: Url) -> ErasedRequest<'a, SetWebhook, Self::Err> { + Requester::set_webhook(self, url).erase() + } + + fn delete_webhook(&self) -> ErasedRequest<'a, DeleteWebhook, Self::Err> { + Requester::delete_webhook(self).erase() + } + + fn get_webhook_info(&self) -> ErasedRequest<'a, GetWebhookInfo, Self::Err> { + Requester::get_webhook_info(self).erase() + } + + fn get_me(&self) -> ErasedRequest<'a, GetMe, Self::Err> { + Requester::get_me(self).erase() + } + + fn log_out(&self) -> ErasedRequest<'a, LogOut, Self::Err> { + Requester::log_out(self).erase() + } + + fn close(&self) -> ErasedRequest<'a, Close, Self::Err> { + Requester::close(self).erase() + } + + fn send_message( + &self, + chat_id: Recipient, + text: String, + ) -> ErasedRequest<'a, SendMessage, Self::Err> { + Requester::send_message(self, chat_id, text).erase() + } + + fn forward_message( + &self, + chat_id: Recipient, + from_chat_id: Recipient, + message_id: MessageId, + ) -> ErasedRequest<'a, ForwardMessage, Self::Err> { + Requester::forward_message(self, chat_id, from_chat_id, message_id).erase() + } + + fn copy_message( + &self, + chat_id: Recipient, + from_chat_id: Recipient, + message_id: MessageId, + ) -> ErasedRequest<'a, CopyMessage, Self::Err> { + Requester::copy_message(self, chat_id, from_chat_id, message_id).erase() + } + + fn send_photo( + &self, + chat_id: Recipient, + photo: InputFile, + ) -> ErasedRequest<'a, SendPhoto, Self::Err> { + Requester::send_photo(self, chat_id, photo).erase() + } + + fn send_audio( + &self, + chat_id: Recipient, + audio: InputFile, + ) -> ErasedRequest<'a, SendAudio, Self::Err> { + Requester::send_audio(self, chat_id, audio).erase() + } + + fn send_document( + &self, + chat_id: Recipient, + document: InputFile, + ) -> ErasedRequest<'a, SendDocument, Self::Err> { + Requester::send_document(self, chat_id, document).erase() + } + + fn send_video( + &self, + chat_id: Recipient, + video: InputFile, + ) -> ErasedRequest<'a, SendVideo, Self::Err> { + Requester::send_video(self, chat_id, video).erase() + } + + fn send_animation( + &self, + chat_id: Recipient, + animation: InputFile, + ) -> ErasedRequest<'a, SendAnimation, Self::Err> { + Requester::send_animation(self, chat_id, animation).erase() + } + + fn send_voice( + &self, + chat_id: Recipient, + voice: InputFile, + ) -> ErasedRequest<'a, SendVoice, Self::Err> { + Requester::send_voice(self, chat_id, voice).erase() + } + + fn send_video_note( + &self, + chat_id: Recipient, + video_note: InputFile, + ) -> ErasedRequest<'a, SendVideoNote, Self::Err> { + Requester::send_video_note(self, chat_id, video_note).erase() + } + + fn send_media_group( + &self, + chat_id: Recipient, + media: Vec, + ) -> ErasedRequest<'a, SendMediaGroup, Self::Err> { + Requester::send_media_group(self, chat_id, media).erase() + } + + fn send_location( + &self, + chat_id: Recipient, + latitude: f64, + longitude: f64, + ) -> ErasedRequest<'a, SendLocation, Self::Err> { + Requester::send_location(self, chat_id, latitude, longitude).erase() + } + + fn edit_message_live_location( + &self, + chat_id: Recipient, + message_id: MessageId, + latitude: f64, + longitude: f64, + ) -> ErasedRequest<'a, EditMessageLiveLocation, Self::Err> { + Requester::edit_message_live_location(self, chat_id, message_id, latitude, longitude) + .erase() + } + + fn edit_message_live_location_inline( + &self, + inline_message_id: String, + latitude: f64, + longitude: f64, + ) -> ErasedRequest<'a, EditMessageLiveLocationInline, Self::Err> { + Requester::edit_message_live_location_inline(self, inline_message_id, latitude, longitude) + .erase() + } + + fn stop_message_live_location( + &self, + chat_id: Recipient, + message_id: MessageId, + latitude: f64, + longitude: f64, + ) -> ErasedRequest<'a, StopMessageLiveLocation, Self::Err> { + Requester::stop_message_live_location(self, chat_id, message_id, latitude, longitude) + .erase() + } + + fn stop_message_live_location_inline( + &self, + inline_message_id: String, + latitude: f64, + longitude: f64, + ) -> ErasedRequest<'a, StopMessageLiveLocationInline, Self::Err> { + Requester::stop_message_live_location_inline(self, inline_message_id, latitude, longitude) + .erase() + } + + fn send_venue( + &self, + chat_id: Recipient, + latitude: f64, + longitude: f64, + title: String, + address: String, + ) -> ErasedRequest<'a, SendVenue, Self::Err> { + Requester::send_venue(self, chat_id, latitude, longitude, title, address).erase() + } + + fn send_contact( + &self, + chat_id: Recipient, + phone_number: String, + first_name: String, + ) -> ErasedRequest<'a, SendContact, Self::Err> { + Requester::send_contact(self, chat_id, phone_number, first_name).erase() + } + + fn send_poll( + &self, + chat_id: Recipient, + question: String, + options: Vec, + ) -> ErasedRequest<'a, SendPoll, Self::Err> { + Requester::send_poll(self, chat_id, question, options).erase() + } + + fn send_dice(&self, chat_id: Recipient) -> ErasedRequest<'a, SendDice, Self::Err> { + Requester::send_dice(self, chat_id).erase() + } + + fn send_chat_action( + &self, + chat_id: Recipient, + action: ChatAction, + ) -> ErasedRequest<'a, SendChatAction, Self::Err> { + Requester::send_chat_action(self, chat_id, action).erase() + } + + fn get_user_profile_photos( + &self, + user_id: UserId, + ) -> ErasedRequest<'a, GetUserProfilePhotos, Self::Err> { + Requester::get_user_profile_photos(self, user_id).erase() + } + + fn get_file(&self, file_id: String) -> ErasedRequest<'a, GetFile, Self::Err> { + Requester::get_file(self, file_id).erase() + } + + fn ban_chat_member( + &self, + chat_id: Recipient, + user_id: UserId, + ) -> ErasedRequest<'a, BanChatMember, Self::Err> { + Requester::ban_chat_member(self, chat_id, user_id).erase() + } + + fn kick_chat_member( + &self, + chat_id: Recipient, + user_id: UserId, + ) -> ErasedRequest<'a, KickChatMember, Self::Err> { + Requester::kick_chat_member(self, chat_id, user_id).erase() + } + + fn unban_chat_member( + &self, + chat_id: Recipient, + user_id: UserId, + ) -> ErasedRequest<'a, UnbanChatMember, Self::Err> { + Requester::unban_chat_member(self, chat_id, user_id).erase() + } + + fn restrict_chat_member( + &self, + chat_id: Recipient, + user_id: UserId, + permissions: ChatPermissions, + ) -> ErasedRequest<'a, RestrictChatMember, Self::Err> { + Requester::restrict_chat_member(self, chat_id, user_id, permissions).erase() + } + + fn promote_chat_member( + &self, + chat_id: Recipient, + user_id: UserId, + ) -> ErasedRequest<'a, PromoteChatMember, Self::Err> { + Requester::promote_chat_member(self, chat_id, user_id).erase() + } + + fn set_chat_administrator_custom_title( + &self, + chat_id: Recipient, + user_id: UserId, + custom_title: String, + ) -> ErasedRequest<'a, SetChatAdministratorCustomTitle, Self::Err> { + Requester::set_chat_administrator_custom_title(self, chat_id, user_id, custom_title).erase() + } + + fn ban_chat_sender_chat( + &self, + chat_id: Recipient, + sender_chat_id: ChatId, + ) -> ErasedRequest<'a, BanChatSenderChat, Self::Err> { + Requester::ban_chat_sender_chat(self, chat_id, sender_chat_id).erase() + } + + fn unban_chat_sender_chat( + &self, + chat_id: Recipient, + sender_chat_id: ChatId, + ) -> ErasedRequest<'a, UnbanChatSenderChat, Self::Err> { + Requester::unban_chat_sender_chat(self, chat_id, sender_chat_id).erase() + } + + fn set_chat_permissions( + &self, + chat_id: Recipient, + permissions: ChatPermissions, + ) -> ErasedRequest<'a, SetChatPermissions, Self::Err> { + Requester::set_chat_permissions(self, chat_id, permissions).erase() + } + + fn export_chat_invite_link( + &self, + chat_id: Recipient, + ) -> ErasedRequest<'a, ExportChatInviteLink, Self::Err> { + Requester::export_chat_invite_link(self, chat_id).erase() + } + + fn create_chat_invite_link( + &self, + chat_id: Recipient, + ) -> ErasedRequest<'a, CreateChatInviteLink, Self::Err> { + Requester::create_chat_invite_link(self, chat_id).erase() + } + + fn edit_chat_invite_link( + &self, + chat_id: Recipient, + invite_link: String, + ) -> ErasedRequest<'a, EditChatInviteLink, Self::Err> { + Requester::edit_chat_invite_link(self, chat_id, invite_link).erase() + } + + fn revoke_chat_invite_link( + &self, + chat_id: Recipient, + invite_link: String, + ) -> ErasedRequest<'a, RevokeChatInviteLink, Self::Err> { + Requester::revoke_chat_invite_link(self, chat_id, invite_link).erase() + } + + /// For Telegram documentation see [`ApproveChatJoinRequest`]. + fn approve_chat_join_request( + &self, + chat_id: Recipient, + user_id: UserId, + ) -> ErasedRequest<'a, ApproveChatJoinRequest, Self::Err> { + Requester::approve_chat_join_request(self, chat_id, user_id).erase() + } + + /// For Telegram documentation see [`DeclineChatJoinRequest`]. + fn decline_chat_join_request( + &self, + chat_id: Recipient, + user_id: UserId, + ) -> ErasedRequest<'a, DeclineChatJoinRequest, Self::Err> { + Requester::decline_chat_join_request(self, chat_id, user_id).erase() + } + + fn set_chat_photo( + &self, + chat_id: Recipient, + photo: InputFile, + ) -> ErasedRequest<'a, SetChatPhoto, Self::Err> { + Requester::set_chat_photo(self, chat_id, photo).erase() + } + + fn delete_chat_photo( + &self, + chat_id: Recipient, + ) -> ErasedRequest<'a, DeleteChatPhoto, Self::Err> { + Requester::delete_chat_photo(self, chat_id).erase() + } + + fn set_chat_title( + &self, + chat_id: Recipient, + title: String, + ) -> ErasedRequest<'a, SetChatTitle, Self::Err> { + Requester::set_chat_title(self, chat_id, title).erase() + } + + fn set_chat_description( + &self, + chat_id: Recipient, + ) -> ErasedRequest<'a, SetChatDescription, Self::Err> { + Requester::set_chat_description(self, chat_id).erase() + } + + fn pin_chat_message( + &self, + chat_id: Recipient, + message_id: MessageId, + ) -> ErasedRequest<'a, PinChatMessage, Self::Err> { + Requester::pin_chat_message(self, chat_id, message_id).erase() + } + + fn unpin_chat_message( + &self, + chat_id: Recipient, + ) -> ErasedRequest<'a, UnpinChatMessage, Self::Err> { + Requester::unpin_chat_message(self, chat_id).erase() + } + + fn unpin_all_chat_messages( + &self, + chat_id: Recipient, + ) -> ErasedRequest<'a, UnpinAllChatMessages, Self::Err> { + Requester::unpin_all_chat_messages(self, chat_id).erase() + } + + fn leave_chat(&self, chat_id: Recipient) -> ErasedRequest<'a, LeaveChat, Self::Err> { + Requester::leave_chat(self, chat_id).erase() + } + + fn get_chat(&self, chat_id: Recipient) -> ErasedRequest<'a, GetChat, Self::Err> { + Requester::get_chat(self, chat_id).erase() + } + + fn get_chat_administrators( + &self, + chat_id: Recipient, + ) -> ErasedRequest<'a, GetChatAdministrators, Self::Err> { + Requester::get_chat_administrators(self, chat_id).erase() + } + + fn get_chat_member_count( + &self, + chat_id: Recipient, + ) -> ErasedRequest<'a, GetChatMemberCount, Self::Err> { + Requester::get_chat_member_count(self, chat_id).erase() + } + + fn get_chat_members_count( + &self, + chat_id: Recipient, + ) -> ErasedRequest<'a, GetChatMembersCount, Self::Err> { + Requester::get_chat_members_count(self, chat_id).erase() + } + + fn get_chat_member( + &self, + chat_id: Recipient, + user_id: UserId, + ) -> ErasedRequest<'a, GetChatMember, Self::Err> { + Requester::get_chat_member(self, chat_id, user_id).erase() + } + + fn set_chat_sticker_set( + &self, + chat_id: Recipient, + sticker_set_name: String, + ) -> ErasedRequest<'a, SetChatStickerSet, Self::Err> { + Requester::set_chat_sticker_set(self, chat_id, sticker_set_name).erase() + } + + fn delete_chat_sticker_set( + &self, + chat_id: Recipient, + ) -> ErasedRequest<'a, DeleteChatStickerSet, Self::Err> { + Requester::delete_chat_sticker_set(self, chat_id).erase() + } + + fn answer_callback_query( + &self, + callback_query_id: String, + ) -> ErasedRequest<'a, AnswerCallbackQuery, Self::Err> { + Requester::answer_callback_query(self, callback_query_id).erase() + } + + fn set_my_commands( + &self, + commands: Vec, + ) -> ErasedRequest<'a, SetMyCommands, Self::Err> { + Requester::set_my_commands(self, commands).erase() + } + + fn get_my_commands(&self) -> ErasedRequest<'a, GetMyCommands, Self::Err> { + Requester::get_my_commands(self).erase() + } + + fn set_chat_menu_button(&self) -> ErasedRequest<'a, SetChatMenuButton, Self::Err> { + Requester::set_chat_menu_button(self).erase() + } + + fn get_chat_menu_button(&self) -> ErasedRequest<'a, GetChatMenuButton, Self::Err> { + Requester::get_chat_menu_button(self).erase() + } + + fn set_my_default_administrator_rights( + &self, + ) -> ErasedRequest<'a, SetMyDefaultAdministratorRights, Self::Err> { + Requester::set_my_default_administrator_rights(self).erase() + } + + fn get_my_default_administrator_rights( + &self, + ) -> ErasedRequest<'a, GetMyDefaultAdministratorRights, Self::Err> { + Requester::get_my_default_administrator_rights(self).erase() + } + + fn delete_my_commands(&self) -> ErasedRequest<'a, DeleteMyCommands, Self::Err> { + Requester::delete_my_commands(self).erase() + } + + fn answer_inline_query( + &self, + inline_query_id: String, + results: Vec, + ) -> ErasedRequest<'a, AnswerInlineQuery, Self::Err> { + Requester::answer_inline_query(self, inline_query_id, results).erase() + } + + fn answer_web_app_query( + &self, + web_app_query_id: String, + result: InlineQueryResult, + ) -> ErasedRequest<'a, AnswerWebAppQuery, Self::Err> { + Requester::answer_web_app_query(self, web_app_query_id, result).erase() + } + + fn edit_message_text( + &self, + chat_id: Recipient, + message_id: MessageId, + text: String, + ) -> ErasedRequest<'a, EditMessageText, Self::Err> { + Requester::edit_message_text(self, chat_id, message_id, text).erase() + } + + fn edit_message_text_inline( + &self, + inline_message_id: String, + text: String, + ) -> ErasedRequest<'a, EditMessageTextInline, Self::Err> { + Requester::edit_message_text_inline(self, inline_message_id, text).erase() + } + + fn edit_message_caption( + &self, + chat_id: Recipient, + message_id: MessageId, + ) -> ErasedRequest<'a, EditMessageCaption, Self::Err> { + Requester::edit_message_caption(self, chat_id, message_id).erase() + } + + fn edit_message_caption_inline( + &self, + inline_message_id: String, + ) -> ErasedRequest<'a, EditMessageCaptionInline, Self::Err> { + Requester::edit_message_caption_inline(self, inline_message_id).erase() + } + + fn edit_message_media( + &self, + chat_id: Recipient, + message_id: MessageId, + media: InputMedia, + ) -> ErasedRequest<'a, EditMessageMedia, Self::Err> { + Requester::edit_message_media(self, chat_id, message_id, media).erase() + } + + fn edit_message_media_inline( + &self, + inline_message_id: String, + media: InputMedia, + ) -> ErasedRequest<'a, EditMessageMediaInline, Self::Err> { + Requester::edit_message_media_inline(self, inline_message_id, media).erase() + } + + fn edit_message_reply_markup( + &self, + chat_id: Recipient, + message_id: MessageId, + ) -> ErasedRequest<'a, EditMessageReplyMarkup, Self::Err> { + Requester::edit_message_reply_markup(self, chat_id, message_id).erase() + } + + fn edit_message_reply_markup_inline( + &self, + inline_message_id: String, + ) -> ErasedRequest<'a, EditMessageReplyMarkupInline, Self::Err> { + Requester::edit_message_reply_markup_inline(self, inline_message_id).erase() + } + + fn stop_poll( + &self, + chat_id: Recipient, + message_id: MessageId, + ) -> ErasedRequest<'a, StopPoll, Self::Err> { + Requester::stop_poll(self, chat_id, message_id).erase() + } + + fn delete_message( + &self, + chat_id: Recipient, + message_id: MessageId, + ) -> ErasedRequest<'a, DeleteMessage, Self::Err> { + Requester::delete_message(self, chat_id, message_id).erase() + } + + fn send_sticker( + &self, + chat_id: Recipient, + sticker: InputFile, + ) -> ErasedRequest<'a, SendSticker, Self::Err> { + Requester::send_sticker(self, chat_id, sticker).erase() + } + + fn get_sticker_set(&self, name: String) -> ErasedRequest<'a, GetStickerSet, Self::Err> { + Requester::get_sticker_set(self, name).erase() + } + + fn get_custom_emoji_stickers( + &self, + custom_emoji_ids: Vec, + ) -> ErasedRequest<'a, GetCustomEmojiStickers, Self::Err> { + Requester::get_custom_emoji_stickers(self, custom_emoji_ids).erase() + } + + fn upload_sticker_file( + &self, + user_id: UserId, + png_sticker: InputFile, + ) -> ErasedRequest<'a, UploadStickerFile, Self::Err> { + Requester::upload_sticker_file(self, user_id, png_sticker).erase() + } + + fn create_new_sticker_set( + &self, + user_id: UserId, + name: String, + title: String, + sticker: InputSticker, + emojis: String, + ) -> ErasedRequest<'a, CreateNewStickerSet, Self::Err> { + Requester::create_new_sticker_set(self, user_id, name, title, sticker, emojis).erase() + } + + fn add_sticker_to_set( + &self, + user_id: UserId, + name: String, + sticker: InputSticker, + emojis: String, + ) -> ErasedRequest<'a, AddStickerToSet, Self::Err> { + Requester::add_sticker_to_set(self, user_id, name, sticker, emojis).erase() + } + + fn set_sticker_position_in_set( + &self, + sticker: String, + position: u32, + ) -> ErasedRequest<'a, SetStickerPositionInSet, Self::Err> { + Requester::set_sticker_position_in_set(self, sticker, position).erase() + } + + fn delete_sticker_from_set( + &self, + sticker: String, + ) -> ErasedRequest<'a, DeleteStickerFromSet, Self::Err> { + Requester::delete_sticker_from_set(self, sticker).erase() + } + + fn set_sticker_set_thumb( + &self, + name: String, + user_id: UserId, + ) -> ErasedRequest<'a, SetStickerSetThumb, Self::Err> { + Requester::set_sticker_set_thumb(self, name, user_id).erase() + } + + fn send_invoice( + &self, + chat_id: Recipient, + title: String, + description: String, + payload: String, + provider_token: String, + currency: String, + prices: Vec, + ) -> ErasedRequest<'a, SendInvoice, Self::Err> { + Requester::send_invoice( + self, + chat_id, + title, + description, + payload, + provider_token, + currency, + prices, + ) + .erase() + } + + #[allow(clippy::too_many_arguments)] + fn create_invoice_link( + &self, + title: String, + description: String, + payload: String, + provider_token: String, + currency: String, + prices: Vec, + ) -> ErasedRequest<'a, CreateInvoiceLink, Self::Err> { + Requester::create_invoice_link( + self, + title, + description, + payload, + provider_token, + currency, + prices, + ) + .erase() + } + + fn answer_shipping_query( + &self, + shipping_query_id: String, + ok: bool, + ) -> ErasedRequest<'a, AnswerShippingQuery, Self::Err> { + Requester::answer_shipping_query(self, shipping_query_id, ok).erase() + } + + fn answer_pre_checkout_query( + &self, + pre_checkout_query_id: String, + ok: bool, + ) -> ErasedRequest<'a, AnswerPreCheckoutQuery, Self::Err> { + Requester::answer_pre_checkout_query(self, pre_checkout_query_id, ok).erase() + } + + fn set_passport_data_errors( + &self, + user_id: UserId, + errors: Vec, + ) -> ErasedRequest<'a, SetPassportDataErrors, Self::Err> { + Requester::set_passport_data_errors(self, user_id, errors).erase() + } + + fn send_game( + &self, + chat_id: u32, + game_short_name: String, + ) -> ErasedRequest<'a, SendGame, Self::Err> { + Requester::send_game(self, chat_id, game_short_name).erase() + } + + fn set_game_score( + &self, + user_id: UserId, + score: u64, + chat_id: u32, + message_id: MessageId, + ) -> ErasedRequest<'a, SetGameScore, Self::Err> { + Requester::set_game_score(self, user_id, score, chat_id, message_id).erase() + } + + fn set_game_score_inline( + &self, + user_id: UserId, + score: u64, + inline_message_id: String, + ) -> ErasedRequest<'a, SetGameScoreInline, Self::Err> { + Requester::set_game_score_inline(self, user_id, score, inline_message_id).erase() + } + + fn get_game_high_scores( + &self, + user_id: UserId, + target: TargetMessage, + ) -> ErasedRequest<'a, GetGameHighScores, Self::Err> { + Requester::get_game_high_scores(self, user_id, target).erase() + } +} diff --git a/crates/teloxide-core/src/adaptors/parse_mode.rs b/crates/teloxide-core/src/adaptors/parse_mode.rs new file mode 100644 index 00000000..cd6e6870 --- /dev/null +++ b/crates/teloxide-core/src/adaptors/parse_mode.rs @@ -0,0 +1,191 @@ +use url::Url; + +use crate::{ + prelude::Requester, + requests::HasPayload, + types::{InputFile, ParseMode, Recipient, *}, +}; + +/// Default parse mode adaptor, see +/// [`RequesterExt::parse_mode`](crate::requests::RequesterExt::parse_mode). +#[derive(Clone, Debug)] +pub struct DefaultParseMode { + bot: B, + mode: ParseMode, +} + +impl DefaultParseMode { + /// Creates new [`DefaultParseMode`]. + /// + /// Note: it's recommended to use [`RequesterExt::parse_mode`] instead. + /// + /// [`RequesterExt::parse_mode`]: crate::requests::RequesterExt::parse_mode + pub fn new(bot: B, parse_mode: ParseMode) -> Self { + Self { bot, mode: parse_mode } + } + + /// Allows to access the inner bot. + pub fn inner(&self) -> &B { + &self.bot + } + + /// Unwraps the inner bot. + pub fn into_inner(self) -> B { + self.bot + } + + /// Returns currently used [`ParseMode`]. + pub fn parse_mode(&self) -> ParseMode { + self.mode + } +} + +macro_rules! f { + ($m:ident $this:ident ($($arg:ident : $T:ty),*)) => { + { + let mut req = $this.inner().$m($($arg),*); + req.payload_mut().parse_mode = Some($this.mode); + req + } + }; +} + +macro_rules! fty { + ($T:ident) => { + B::$T + }; +} + +macro_rules! fid { + ($m:ident $this:ident ($($arg:ident : $T:ty),*)) => { + $this.inner().$m($($arg),*) + }; +} + +impl Requester for DefaultParseMode { + type Err = B::Err; + + requester_forward! { + send_message, + send_photo, + send_video, + send_audio, + send_document, + send_animation, + send_voice, + edit_message_text, + edit_message_text_inline, + edit_message_caption, + edit_message_caption_inline => f, fty + } + + type SendPoll = B::SendPoll; + + fn send_poll(&self, chat_id: C, question: Q, options: O) -> Self::SendPoll + where + C: Into, + Q: Into, + O: IntoIterator, + { + let mut req = self.inner().send_poll(chat_id, question, options); + req.payload_mut().explanation_parse_mode = Some(self.mode); + req + } + + requester_forward! { + get_me, + log_out, + close, + get_updates, + set_webhook, + delete_webhook, + get_webhook_info, + forward_message, + copy_message, + send_video_note, + send_media_group, + send_location, + edit_message_live_location, + edit_message_live_location_inline, + stop_message_live_location, + stop_message_live_location_inline, + send_venue, + send_contact, + send_dice, + send_chat_action, + get_user_profile_photos, + get_file, + kick_chat_member, + ban_chat_member, + unban_chat_member, + restrict_chat_member, + promote_chat_member, + set_chat_administrator_custom_title, + ban_chat_sender_chat, + unban_chat_sender_chat, + set_chat_permissions, + export_chat_invite_link, + create_chat_invite_link, + edit_chat_invite_link, + revoke_chat_invite_link, + set_chat_photo, + delete_chat_photo, + set_chat_title, + set_chat_description, + pin_chat_message, + unpin_chat_message, + unpin_all_chat_messages, + leave_chat, + get_chat, + get_chat_administrators, + get_chat_members_count, + get_chat_member_count, + get_chat_member, + set_chat_sticker_set, + delete_chat_sticker_set, + answer_callback_query, + set_my_commands, + get_my_commands, + set_chat_menu_button, + get_chat_menu_button, + set_my_default_administrator_rights, + get_my_default_administrator_rights, + delete_my_commands, + answer_inline_query, + answer_web_app_query, + edit_message_media, + edit_message_media_inline, + edit_message_reply_markup, + edit_message_reply_markup_inline, + stop_poll, + delete_message, + send_sticker, + get_sticker_set, + get_custom_emoji_stickers, + upload_sticker_file, + create_new_sticker_set, + add_sticker_to_set, + set_sticker_position_in_set, + delete_sticker_from_set, + set_sticker_set_thumb, + send_invoice, + create_invoice_link, + answer_shipping_query, + answer_pre_checkout_query, + set_passport_data_errors, + send_game, + set_game_score, + set_game_score_inline, + get_game_high_scores, + approve_chat_join_request, + decline_chat_join_request + => fid, fty + } +} + +download_forward! { + 'w + B + DefaultParseMode + { this => this.inner() } +} diff --git a/crates/teloxide-core/src/adaptors/throttle.rs b/crates/teloxide-core/src/adaptors/throttle.rs new file mode 100644 index 00000000..7c4aaba3 --- /dev/null +++ b/crates/teloxide-core/src/adaptors/throttle.rs @@ -0,0 +1,210 @@ +/// `ThrottlingRequest` and `ThrottlingSend` structures +mod request; +/// Lock that allows requests to wait until they are allowed to be sent +mod request_lock; +/// `impl Requester for Throttle<_>` +mod requester_impl; +/// `Settings` and `Limits` structures +mod settings; +/// "Worker" that checks the limits +mod worker; + +use std::{ + future::Future, + hash::{Hash, Hasher}, +}; + +use tokio::sync::{ + mpsc, + oneshot::{self}, +}; + +use crate::{errors::AsResponseParameters, requests::Requester, types::*}; + +use self::{ + request_lock::{channel, RequestLock}, + worker::{worker, FreezeUntil, InfoMessage}, +}; + +pub use request::{ThrottlingRequest, ThrottlingSend}; +pub use settings::{Limits, Settings}; + +/// Automatic request limits respecting mechanism. +/// +/// Telegram has strict [limits], which, if exceeded will sooner or later cause +/// `RequestError::RetryAfter(_)` errors. These errors can cause users of your +/// bot to never receive responses from the bot or receive them in a wrong +/// order. +/// +/// This bot wrapper automatically checks for limits, suspending requests until +/// they could be sent without exceeding limits (request order in chats is not +/// changed). +/// +/// It's recommended to use this wrapper before other wrappers (i.e.: +/// `SomeWrapper>` not `Throttle>`) because if +/// done otherwise inner wrappers may cause `Throttle` to miscalculate limits +/// usage. +/// +/// [limits]: https://core.telegram.org/bots/faq#my-bot-is-hitting-limits-how-do-i-avoid-this +/// +/// ## Examples +/// +/// ```no_run (throttle fails to spawn task without tokio runtime) +/// use teloxide_core::{adaptors::throttle::Limits, requests::RequesterExt, Bot}; +/// +/// let bot = Bot::new("TOKEN") +/// .throttle(Limits::default()); +/// +/// /* send many requests here */ +/// ``` +/// +/// ## Note about send-by-@channelusername +/// +/// Telegram have limits on sending messages to _the same chat_. To check them +/// we store `chat_id`s of several last requests. _However_ there is no good way +/// to tell if given `ChatId::Id(x)` corresponds to the same chat as +/// `ChatId::ChannelUsername(u)`. +/// +/// Our current approach is to just give up and check `chat_id_a == chat_id_b`. +/// This may give incorrect results. +/// +/// As such, we encourage not to use `ChatId::ChannelUsername(u)` with this bot +/// wrapper. +#[derive(Clone, Debug)] +pub struct Throttle { + bot: B, + // `RequestLock` allows to unlock requests (allowing them to be sent). + queue: mpsc::Sender<(ChatIdHash, RequestLock)>, + info_tx: mpsc::Sender, +} + +impl Throttle { + /// Creates new [`Throttle`] alongside with worker future. + /// + /// Note: [`Throttle`] will only send requests if returned worker is + /// polled/spawned/awaited. + pub fn new(bot: B, limits: Limits) -> (Self, impl Future) + where + B: Requester + Clone, + B::Err: AsResponseParameters, + { + let settings = Settings { limits, ..<_>::default() }; + Self::with_settings(bot, settings) + } + + /// Creates new [`Throttle`] alongside with worker future. + /// + /// Note: [`Throttle`] will only send requests if returned worker is + /// polled/spawned/awaited. + pub fn with_settings(bot: B, settings: Settings) -> (Self, impl Future) + where + B: Requester + Clone, + B::Err: AsResponseParameters, + { + let (tx, rx) = mpsc::channel(settings.limits.messages_per_sec_overall as usize); + let (info_tx, info_rx) = mpsc::channel(2); + + let worker = worker(settings, rx, info_rx, bot.clone()); + let this = Self { bot, queue: tx, info_tx }; + + (this, worker) + } + + /// Creates new [`Throttle`] spawning the worker with `tokio::spawn` + /// + /// Note: it's recommended to use [`RequesterExt::throttle`] instead. + /// + /// [`RequesterExt::throttle`]: crate::requests::RequesterExt::throttle + pub fn new_spawn(bot: B, limits: Limits) -> Self + where + B: Requester + Clone + Send + Sync + 'static, + B::Err: AsResponseParameters, + B::GetChat: Send, + { + let (this, worker) = Self::new(bot, limits); + + tokio::spawn(worker); + + this + } + + /// Creates new [`Throttle`] spawning the worker with `tokio::spawn` + pub fn spawn_with_settings(bot: B, settings: Settings) -> Self + where + B: Requester + Clone + Send + Sync + 'static, + B::Err: AsResponseParameters, + B::GetChat: Send, + { + let (this, worker) = Self::with_settings(bot, settings); + + tokio::spawn(worker); + this + } + + /// Allows to access inner bot + pub fn inner(&self) -> &B { + &self.bot + } + + /// Unwraps inner bot + pub fn into_inner(self) -> B { + self.bot + } + + /// Returns currently used [`Limits`]. + pub async fn limits(&self) -> Limits { + const WORKER_DIED: &str = "worker died before last `Throttle` instance"; + + let (tx, rx) = oneshot::channel(); + + self.info_tx.send(InfoMessage::GetLimits { response: tx }).await.expect(WORKER_DIED); + + rx.await.expect(WORKER_DIED) + } + + /// Sets new limits. + /// + /// Note: changes may not be applied immediately. + pub async fn set_limits(&self, new: Limits) { + let (tx, rx) = oneshot::channel(); + + self.info_tx.send(InfoMessage::SetLimits { new, response: tx }).await.ok(); + + rx.await.ok(); + } +} + +/// An ID used in the worker. +/// +/// It is used instead of `ChatId` to make copying cheap even in case of +/// usernames. (It is just a hashed username.) +#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq)] +enum ChatIdHash { + Id(ChatId), + ChannelUsernameHash(u64), +} + +impl ChatIdHash { + fn is_channel(&self) -> bool { + match self { + &Self::Id(id) => id.is_channel_or_supergroup(), + Self::ChannelUsernameHash(_) => true, + } + } +} + +impl From<&Recipient> for ChatIdHash { + fn from(value: &Recipient) -> Self { + match value { + Recipient::Id(id) => ChatIdHash::Id(*id), + Recipient::ChannelUsername(username) => { + // FIXME: this could probably use a faster hasher, `DefaultHasher` is known to + // be slow (it's not like we _need_ this to be fast, but still) + let mut hasher = std::collections::hash_map::DefaultHasher::new(); + username.hash(&mut hasher); + let hash = hasher.finish(); + ChatIdHash::ChannelUsernameHash(hash) + } + } + } +} diff --git a/crates/teloxide-core/src/adaptors/throttle/request.rs b/crates/teloxide-core/src/adaptors/throttle/request.rs new file mode 100644 index 00000000..46235fce --- /dev/null +++ b/crates/teloxide-core/src/adaptors/throttle/request.rs @@ -0,0 +1,223 @@ +use std::{ + future::{Future, IntoFuture}, + pin::Pin, + sync::Arc, + time::Instant, +}; + +use futures::{ + future::BoxFuture, + task::{Context, Poll}, +}; +use tokio::sync::mpsc; + +use crate::{ + adaptors::throttle::{channel, ChatIdHash, FreezeUntil, RequestLock}, + errors::AsResponseParameters, + requests::{HasPayload, Output, Request}, +}; + +/// Request returned by [`Throttling`](crate::adaptors::Throttle) methods. +#[must_use = "Requests are lazy and do nothing unless sent"] +pub struct ThrottlingRequest { + pub(super) request: Arc, + pub(super) chat_id: fn(&R::Payload) -> ChatIdHash, + pub(super) worker: mpsc::Sender<(ChatIdHash, RequestLock)>, +} + +/// Future returned by [`ThrottlingRequest`]s. +#[pin_project::pin_project] +pub struct ThrottlingSend(#[pin] BoxFuture<'static, Result, R::Err>>); + +enum ShareableRequest { + Shared(Arc), + // Option is used to `take` ownership + Owned(Option), +} + +impl HasPayload for ThrottlingRequest { + type Payload = R::Payload; + + /// Note that if this request was already executed via `send_ref` and it + /// didn't yet completed, this method will clone the underlying request. + fn payload_mut(&mut self) -> &mut Self::Payload { + Arc::make_mut(&mut self.request).payload_mut() + } + + fn payload_ref(&self) -> &Self::Payload { + self.request.payload_ref() + } +} + +impl Request for ThrottlingRequest +where + R: Request + Clone + Send + Sync + 'static, // TODO: rem static + R::Err: AsResponseParameters + Send, + Output: Send, +{ + type Err = R::Err; + type Send = ThrottlingSend; + type SendRef = ThrottlingSend; + + fn send(self) -> Self::Send { + let chat = (self.chat_id)(self.payload_ref()); + let request = match Arc::try_unwrap(self.request) { + Ok(owned) => ShareableRequest::Owned(Some(owned)), + Err(shared) => ShareableRequest::Shared(shared), + }; + let fut = send(request, chat, self.worker); + + ThrottlingSend(Box::pin(fut)) + } + + fn send_ref(&self) -> Self::SendRef { + let chat = (self.chat_id)(self.payload_ref()); + let request = ShareableRequest::Shared(Arc::clone(&self.request)); + let fut = send(request, chat, self.worker.clone()); + + ThrottlingSend(Box::pin(fut)) + } +} + +impl IntoFuture for ThrottlingRequest +where + R: Request + Clone + Send + Sync + 'static, + R::Err: AsResponseParameters + Send, + Output: Send, +{ + type Output = Result, ::Err>; + type IntoFuture = ::Send; + + fn into_future(self) -> Self::IntoFuture { + self.send() + } +} + +impl Future for ThrottlingSend +where + R::Err: AsResponseParameters, +{ + type Output = Result, R::Err>; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + self.as_mut().project().0.poll(cx) + } +} + +// This diagram explains how `ThrottlingRequest` works/what `send` does +// +// │ +// ThrottlingRequest │ worker() +// │ +// ┌───────────────┐ │ ┌────────────────────────┐ +// ┌──────────────────►│request is sent│ │ │see worker documentation│ +// │ └───────┬───────┘ │ │and comments for more │ +// │ │ │ │information on how it │ +// │ ▼ │ │actually works │ +// │ ┌─────────┐ │ └────────────────────────┘ +// │ ┌────────────────┐ │send lock│ │ +// │ │has worker died?│◄──┤to worker├─────►:───────────┐ +// │ └─┬─────────────┬┘ └─────────┘ │ ▼ +// │ │ │ │ ┌──────────────────┐ +// │ Y └─N───────┐ │ │ *magic* │ +// │ │ │ │ └────────┬─────────┘ +// │ ▼ ▼ │ │ +// │ ┌───────────┐ ┌────────────────┐ │ ▼ +// │ │send inner │ │wait for worker │ │ ┌─────────────────┐ +// │ │request │ │to allow sending│◄──:◄─┤ `lock.unlock()` │ +// │ └───┬───────┘ │this request │ │ └─────────────────┘ +// │ │ └────────┬───────┘ │ +// │ │ │ │ +// │ ▼ ▼ │ +// │ ┌──────┐ ┌────────────────────┐ │ +// │ │return│ │send inner request │ │ +// │ │result│ │and check its result│ │ +// │ └──────┘ └─┬─────────┬────────┘ │ +// │ ▲ ▲ │ │ │ +// │ │ │ │ Err(RetryAfter(n)) │ +// │ │ │ else │ │ +// │ │ │ │ ▼ │ +// │ │ └─────┘ ┌───────────────┐ │ +// │ │ │are retries on?│ │ +// │ │ └┬─────────────┬┘ │ +// │ │ │ │ │ +// │ └────────────N─┘ Y │ +// │ │ │ ┌──────────────────┐ +// │ ▼ │ │ *magic* │ +// │ ┌──────────────────┐ │ └──────────────────┘ +// ┌┴────────────┐ │notify worker that│ │ ▲ +// │retry request│◄──┤RetryAfter error ├──►:───────────┘ +// └─────────────┘ │has happened │ │ +// └──────────────────┘ │ +// │ + +/// Actual implementation of the `ThrottlingSend` future +async fn send( + mut request: ShareableRequest, + chat: ChatIdHash, + worker: mpsc::Sender<(ChatIdHash, RequestLock)>, +) -> Result, R::Err> +where + R: Request + Send + Sync + 'static, + R::Err: AsResponseParameters + Send, + Output: Send, +{ + // We use option in `ShareableRequest` to `take` when sending by value. + // + // All unwraps down below will succeed because we always return immediately + // after taking. + + loop { + let (lock, wait) = channel(); + + // The worker is unlikely to drop queue before sending all requests, + // but just in case it has dropped the queue, we want to just send the + // request. + if worker.send((chat, lock)).await.is_err() { + log::error!("Worker dropped the queue before sending all requests"); + + let res = match &mut request { + ShareableRequest::Shared(shared) => shared.send_ref().await, + ShareableRequest::Owned(owned) => owned.take().unwrap().await, + }; + + return res; + }; + + let (retry, freeze) = wait.await; + + let res = match (retry, &mut request) { + // Retries are turned on, use `send_ref` even if we have owned access + (true, request) => { + let request = match request { + ShareableRequest::Shared(shared) => &**shared, + ShareableRequest::Owned(owned) => owned.as_ref().unwrap(), + }; + + request.send_ref().await + } + (false, ShareableRequest::Shared(shared)) => shared.send_ref().await, + (false, ShareableRequest::Owned(owned)) => owned.take().unwrap().await, + }; + + let retry_after = res.as_ref().err().and_then(<_>::retry_after); + if let Some(retry_after) = retry_after { + let after = retry_after; + let until = Instant::now() + after; + + // If we'll retry, we check that worker hasn't died at the start of the loop + // otherwise we don't care if the worker is alive or not + let _ = freeze.send(FreezeUntil { until, after, chat }).await; + + if retry { + log::warn!("Freezing, before retrying: {:?}", retry_after); + tokio::time::sleep_until(until.into()).await; + } + } + + match res { + Err(_) if retry && retry_after.is_some() => continue, + res => break res, + }; + } +} diff --git a/crates/teloxide-core/src/adaptors/throttle/request_lock.rs b/crates/teloxide-core/src/adaptors/throttle/request_lock.rs new file mode 100644 index 00000000..e21093c2 --- /dev/null +++ b/crates/teloxide-core/src/adaptors/throttle/request_lock.rs @@ -0,0 +1,45 @@ +use std::pin::Pin; + +use futures::{ + task::{Context, Poll}, + Future, +}; +use tokio::sync::{ + mpsc, + oneshot::{self, Receiver, Sender}, +}; + +use crate::adaptors::throttle::FreezeUntil; + +pub(super) fn channel() -> (RequestLock, RequestWaiter) { + let (tx, rx) = oneshot::channel(); + let tx = RequestLock(tx); + let rx = RequestWaiter(rx); + (tx, rx) +} + +#[must_use] +pub(super) struct RequestLock(Sender<(bool, mpsc::Sender)>); + +#[must_use] +#[pin_project::pin_project] +pub(super) struct RequestWaiter(#[pin] Receiver<(bool, mpsc::Sender)>); + +impl RequestLock { + pub(super) fn unlock(self, retry: bool, freeze: mpsc::Sender) -> Result<(), ()> { + self.0.send((retry, freeze)).map_err(drop) + } +} + +impl Future for RequestWaiter { + type Output = (bool, mpsc::Sender); + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let this = self.project(); + match this.0.poll(cx) { + Poll::Ready(Ok(ret)) => Poll::Ready(ret), + Poll::Ready(Err(_)) => panic!("`RequestLock` is dropped by the throttle worker"), + Poll::Pending => Poll::Pending, + } + } +} diff --git a/crates/teloxide-core/src/adaptors/throttle/requester_impl.rs b/crates/teloxide-core/src/adaptors/throttle/requester_impl.rs new file mode 100644 index 00000000..e7817f50 --- /dev/null +++ b/crates/teloxide-core/src/adaptors/throttle/requester_impl.rs @@ -0,0 +1,177 @@ +use std::sync::Arc; + +use url::Url; + +use crate::{ + adaptors::{throttle::ThrottlingRequest, Throttle}, + errors::AsResponseParameters, + requests::{HasPayload, Requester}, + types::*, +}; + +macro_rules! f { + ($m:ident $this:ident ($($arg:ident : $T:ty),*)) => { + ThrottlingRequest { + request: Arc::new($this.inner().$m($($arg),*)), + chat_id: |p| (&p.payload_ref().chat_id).into(), + worker: $this.queue.clone(), + } + }; +} + +macro_rules! fty { + ($T:ident) => { + ThrottlingRequest + }; +} + +macro_rules! fid { + ($m:ident $this:ident ($($arg:ident : $T:ty),*)) => { + $this.inner().$m($($arg),*) + }; +} + +macro_rules! ftyid { + ($T:ident) => { + B::$T + }; +} + +impl Requester for Throttle +where + B::Err: AsResponseParameters, + + B::SendMessage: Clone + Send + Sync + 'static, + B::ForwardMessage: Clone + Send + Sync + 'static, + B::CopyMessage: Clone + Send + Sync + 'static, + B::SendPhoto: Clone + Send + Sync + 'static, + B::SendAudio: Clone + Send + Sync + 'static, + B::SendDocument: Clone + Send + Sync + 'static, + B::SendVideo: Clone + Send + Sync + 'static, + B::SendAnimation: Clone + Send + Sync + 'static, + B::SendVoice: Clone + Send + Sync + 'static, + B::SendVideoNote: Clone + Send + Sync + 'static, + B::SendMediaGroup: Clone + Send + Sync + 'static, + B::SendLocation: Clone + Send + Sync + 'static, + B::SendVenue: Clone + Send + Sync + 'static, + B::SendContact: Clone + Send + Sync + 'static, + B::SendPoll: Clone + Send + Sync + 'static, + B::SendDice: Clone + Send + Sync + 'static, + B::SendSticker: Clone + Send + Sync + 'static, + B::SendInvoice: Clone + Send + Sync + 'static, +{ + type Err = B::Err; + + requester_forward! { + send_message, + forward_message, + copy_message, + send_photo, + send_audio, + send_document, + send_video, + send_animation, + send_voice, + send_video_note, + send_media_group, + send_location, + send_venue, + send_contact, + send_poll, + send_dice, + send_sticker, + send_invoice + => f, fty + } + + requester_forward! { + get_me, + log_out, + close, + get_updates, + set_webhook, + delete_webhook, + get_webhook_info, + edit_message_live_location, + edit_message_live_location_inline, + stop_message_live_location, + stop_message_live_location_inline, + send_chat_action, + get_user_profile_photos, + get_file, + kick_chat_member, + ban_chat_member, + unban_chat_member, + restrict_chat_member, + promote_chat_member, + set_chat_administrator_custom_title, + ban_chat_sender_chat, + unban_chat_sender_chat, + set_chat_permissions, + export_chat_invite_link, + create_chat_invite_link, + edit_chat_invite_link, + revoke_chat_invite_link, + set_chat_photo, + delete_chat_photo, + set_chat_title, + set_chat_description, + pin_chat_message, + unpin_chat_message, + unpin_all_chat_messages, + leave_chat, + get_chat, + get_chat_administrators, + get_chat_members_count, + get_chat_member_count, + get_chat_member, + set_chat_sticker_set, + delete_chat_sticker_set, + answer_callback_query, + set_my_commands, + get_my_commands, + set_chat_menu_button, + get_chat_menu_button, + set_my_default_administrator_rights, + get_my_default_administrator_rights, + delete_my_commands, + answer_inline_query, + answer_web_app_query, + edit_message_text, + edit_message_text_inline, + edit_message_caption, + edit_message_caption_inline, + edit_message_media, + edit_message_media_inline, + edit_message_reply_markup, + edit_message_reply_markup_inline, + stop_poll, + delete_message, + get_sticker_set, + get_custom_emoji_stickers, + upload_sticker_file, + create_new_sticker_set, + add_sticker_to_set, + set_sticker_position_in_set, + delete_sticker_from_set, + set_sticker_set_thumb, + answer_shipping_query, + create_invoice_link, + answer_pre_checkout_query, + set_passport_data_errors, + send_game, + set_game_score, + set_game_score_inline, + approve_chat_join_request, + decline_chat_join_request, + get_game_high_scores + => fid, ftyid + } +} + +download_forward! { + 'w + B + Throttle + { this => this.inner() } +} diff --git a/crates/teloxide-core/src/adaptors/throttle/settings.rs b/crates/teloxide-core/src/adaptors/throttle/settings.rs new file mode 100644 index 00000000..57b38e9a --- /dev/null +++ b/crates/teloxide-core/src/adaptors/throttle/settings.rs @@ -0,0 +1,110 @@ +use std::pin::Pin; + +use futures::{future::ready, Future}; + +// Required to not trigger `clippy::type-complexity` lint +type BoxedFnMut = Box O + Send>; +type BoxedFuture = Pin + Send>>; + +/// Settings used by [`Throttle`] adaptor. +/// +/// ## Examples +/// +/// ``` +/// use teloxide_core::adaptors::throttle; +/// +/// let settings = throttle::Settings::default() +/// .on_queue_full(|pending| async move { /* do something when internal queue is full */ }); +/// +/// // use settings in `Throttle::with_settings` or other constructors +/// # let _ = settings; +/// ``` +/// +/// [`Throttle`]: crate::adaptors::throttle::Throttle +#[must_use] +#[non_exhaustive] +pub struct Settings { + pub limits: Limits, + pub on_queue_full: BoxedFnMut, + pub retry: bool, + pub check_slow_mode: bool, +} + +/// Telegram request limits. +/// +/// This struct is used in [`Throttle`]. +/// +/// Note that you may ask telegram [@BotSupport] to increase limits for your +/// particular bot if it has a lot of users (but they may or may not do that). +/// +/// [@BotSupport]: https://t.me/botsupport +/// [`Throttle`]: crate::adaptors::throttle::Throttle +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] +pub struct Limits { + /// Allowed messages in one chat per second. + pub messages_per_sec_chat: u32, + + /// Allowed messages in one chat per minute. + pub messages_per_min_chat: u32, + + /// Allowed messages in one channel per minute. + pub messages_per_min_channel: u32, + + /// Allowed messages per second. + pub messages_per_sec_overall: u32, +} + +impl Settings { + pub fn limits(mut self, val: Limits) -> Self { + self.limits = val; + self + } + + pub fn on_queue_full(mut self, mut val: F) -> Self + where + F: FnMut(usize) -> Fut + Send + 'static, + Fut: Future + Send + 'static, + { + self.on_queue_full = Box::new(move |pending| Box::pin(val(pending))); + self + } + + pub fn no_retry(mut self) -> Self { + self.retry = false; + self + } + + pub fn check_slow_mode(mut self) -> Self { + self.check_slow_mode = true; + self + } +} + +impl Default for Settings { + fn default() -> Self { + Self { + limits: <_>::default(), + on_queue_full: Box::new(|pending| { + log::warn!("Throttle queue is full ({} pending requests)", pending); + Box::pin(ready(())) + }), + retry: true, + check_slow_mode: false, + } + } +} + +/// Defaults are taken from [telegram documentation][tgdoc] (except for +/// `messages_per_min_channel`). +/// +/// [tgdoc]: https://core.telegram.org/bots/faq#my-bot-is-hitting-limits-how-do-i-avoid-this +impl Default for Limits { + fn default() -> Self { + Self { + messages_per_sec_chat: 1, + messages_per_sec_overall: 30, + messages_per_min_chat: 20, + messages_per_min_channel: 10, + } + } +} diff --git a/crates/teloxide-core/src/adaptors/throttle/worker.rs b/crates/teloxide-core/src/adaptors/throttle/worker.rs new file mode 100644 index 00000000..6a2994dd --- /dev/null +++ b/crates/teloxide-core/src/adaptors/throttle/worker.rs @@ -0,0 +1,387 @@ +use std::{ + collections::{hash_map::Entry, HashMap, VecDeque}, + time::{Duration, Instant}, +}; + +use tokio::sync::{mpsc, mpsc::error::TryRecvError, oneshot::Sender}; +use vecrem::VecExt; + +use crate::{ + adaptors::throttle::{request_lock::RequestLock, ChatIdHash, Limits, Settings}, + errors::AsResponseParameters, + requests::Requester, +}; + +const MINUTE: Duration = Duration::from_secs(60); +const SECOND: Duration = Duration::from_secs(1); + +// Delay between worker iterations. +// +// For now it's `second/4`, but that number is chosen pretty randomly, we may +// want to change this. +const DELAY: Duration = Duration::from_millis(250); + +/// Minimal time between calls to queue_full function +const QUEUE_FULL_DELAY: Duration = Duration::from_secs(4); + +#[derive(Debug)] +pub(super) enum InfoMessage { + GetLimits { response: Sender }, + SetLimits { new: Limits, response: Sender<()> }, +} + +type RequestsSent = u32; + +// I wish there was special data structure for history which removed the +// need in 2 hashmaps +// (waffle) +#[derive(Default)] +struct RequestsSentToChats { + per_min: HashMap, + per_sec: HashMap, +} + +pub(super) struct FreezeUntil { + pub(super) until: Instant, + pub(super) after: Duration, + pub(super) chat: ChatIdHash, +} + +// Throttling is quite complicated. This comment describes the algorithm of the +// current implementation. +// +// ### Request +// +// When a throttling request is sent, it sends a tuple of `ChatId` and +// `Sender<()>` to the worker. Then the request waits for a notification from +// the worker. When notification is received, it sends the underlying request. +// +// ### Worker +// +// The worker does the most important job -- it ensures that the limits are +// never exceeded. +// +// The worker stores a history of requests sent in the last minute (and to which +// chats they were sent) and a queue of pending updates. +// +// The worker does the following algorithm loop: +// +// 1. If the queue is empty, wait for the first message in incoming channel (and +// add it to the queue). +// +// 2. Read all present messages from an incoming channel and transfer them to +// the queue. +// +// 3. Record the current time. +// +// 4. Clear the history from records whose time < (current time - minute). +// +// 5. Count all requests which were sent last second, `allowed = +// limit.messages_per_sec_overall - count`. +// +// 6. If `allowed == 0` wait a bit and `continue` to the next iteration. +// +// 7. Count how many requests were sent to which chats (i.e.: create +// `Map`). (Note: the same map, but for last minute also exists, +// but it's updated, instead of recreation.) +// +// 8. While `allowed >= 0` search for requests which chat haven't exceed the +// limits (i.e.: map[chat] < limit), if one is found, decrease `allowed`, notify +// the request that it can be now executed, increase counts, add record to the +// history. +pub(super) async fn worker( + Settings { mut limits, mut on_queue_full, retry, check_slow_mode }: Settings, + mut rx: mpsc::Receiver<(ChatIdHash, RequestLock)>, + mut info_rx: mpsc::Receiver, + bot: B, +) where + B: Requester, + B::Err: AsResponseParameters, +{ + // FIXME(waffle): Make an research about data structures for this queue. + // Currently this is O(n) removing (n = number of elements + // stayed), amortized O(1) push (vec+vecrem). + let mut queue: Vec<(ChatIdHash, RequestLock)> = + Vec::with_capacity(limits.messages_per_sec_overall as usize); + + let mut history: VecDeque<(ChatIdHash, Instant)> = VecDeque::new(); + let mut requests_sent = RequestsSentToChats::default(); + + let mut slow_mode: Option> = + check_slow_mode.then(HashMap::new); + + let mut rx_is_closed = false; + + let mut last_queue_full = + Instant::now().checked_sub(QUEUE_FULL_DELAY).unwrap_or_else(Instant::now); + + let (freeze_tx, mut freeze_rx) = mpsc::channel::(1); + + while !rx_is_closed || !queue.is_empty() { + // FIXME(waffle): + // 1. If the `queue` is empty, `read_from_rx` call down below will 'block' + // execution until a request is sent. While the execution is 'blocked' no + // `InfoMessage`s could be answered. + // + // 2. If limits are decreased, ideally we want to shrink queue. + // + // *blocked in asynchronous way + answer_info(&mut info_rx, &mut limits); + + loop { + tokio::select! { + freeze_until = freeze_rx.recv() => { + freeze( + &mut freeze_rx, + slow_mode.as_mut(), + &bot, + freeze_until + ) + .await; + }, + () = read_from_rx(&mut rx, &mut queue, &mut rx_is_closed) => break, + } + } + //debug_assert_eq!(queue.capacity(), limits.messages_per_sec_overall as usize); + + if queue.len() == queue.capacity() && last_queue_full.elapsed() > QUEUE_FULL_DELAY { + last_queue_full = Instant::now(); + tokio::spawn(on_queue_full(queue.len())); + } + + // _Maybe_ we need to use `spawn_blocking` here, because there is + // decent amount of blocking work. However _for now_ I've decided not + // to use it here. + // + // Reasons (not to use `spawn_blocking`): + // + // 1. The work seems not very CPU-bound, it's not heavy computations, + // it's more like light computations. + // + // 2. `spawn_blocking` is not zero-cost — it spawns a new system thread + // + do so other work. This may actually be *worse* then current + // "just do everything in this async fn" approach. + // + // 3. With `rt-threaded` feature, tokio uses [`num_cpus()`] threads + // which should be enough to work fine with one a-bit-blocking task. + // Crucially current behaviour will be problem mostly with + // single-threaded runtimes (and in case you're using one, you + // probably don't want to spawn unnecessary threads anyway). + // + // I think if we'll ever change this behaviour, we need to make it + // _configurable_. + // + // See also [discussion (ru)]. + // + // NOTE: If you are reading this because you have any problems because + // of this worker, open an [issue on github] + // + // [`num_cpus()`]: https://vee.gg/JGwq2 + // [discussion (ru)]: https://t.me/rust_async/27891 + // [issue on github]: https://github.com/teloxide/teloxide/issues/new + // + // (waffle) + + let now = Instant::now(); + let min_back = now - MINUTE; + let sec_back = now - SECOND; + + // make history and requests_sent up-to-date + while let Some((_, time)) = history.front() { + // history is sorted, we found first up-to-date thing + if time >= &min_back { + break; + } + + if let Some((chat, _)) = history.pop_front() { + let entry = requests_sent.per_min.entry(chat).and_modify(|count| { + *count -= 1; + }); + + if let Entry::Occupied(entry) = entry { + if *entry.get() == 0 { + entry.remove_entry(); + } + } + } + } + + // as truncates which is ok since in case of truncation it would always be >= + // limits.overall_s + let used = history.iter().take_while(|(_, time)| time > &sec_back).count() as u32; + let mut allowed = limits.messages_per_sec_overall.saturating_sub(used); + + if allowed == 0 { + requests_sent.per_sec.clear(); + tokio::time::sleep(DELAY).await; + continue; + } + + for (chat, _) in history.iter().take_while(|(_, time)| time > &sec_back) { + *requests_sent.per_sec.entry(*chat).or_insert(0) += 1; + } + + let mut queue_removing = queue.removing(); + + while let Some(entry) = queue_removing.next() { + let chat = &entry.value().0; + + let slow_mode = slow_mode.as_mut().and_then(|sm| sm.get_mut(chat)); + + if let Some(&mut (delay, last)) = slow_mode { + if last + delay > Instant::now() { + continue; + } + } + + let requests_sent_per_sec_count = requests_sent.per_sec.get(chat).copied().unwrap_or(0); + let requests_sent_per_min_count = requests_sent.per_min.get(chat).copied().unwrap_or(0); + + let messages_per_min_limit = if chat.is_channel() { + limits.messages_per_min_channel + } else { + limits.messages_per_min_chat + }; + + let limits_not_exceeded = requests_sent_per_sec_count < limits.messages_per_sec_chat + && requests_sent_per_min_count < messages_per_min_limit; + + if limits_not_exceeded { + // Unlock the associated request. + + let chat = *chat; + let (_, lock) = entry.remove(); + + // Only count request as sent if the request wasn't dropped before unlocked + if lock.unlock(retry, freeze_tx.clone()).is_ok() { + *requests_sent.per_sec.entry(chat).or_insert(0) += 1; + *requests_sent.per_min.entry(chat).or_insert(0) += 1; + history.push_back((chat, Instant::now())); + + if let Some((_, last)) = slow_mode { + *last = Instant::now(); + } + + // We have "sent" one request, so now we can send one less. + allowed -= 1; + if allowed == 0 { + break; + } + } + } + } + + // It's easier to just recompute last second stats, instead of keeping + // track of it alongside with minute stats, so we just throw this away. + requests_sent.per_sec.clear(); + tokio::time::sleep(DELAY).await; + } +} + +fn answer_info(rx: &mut mpsc::Receiver, limits: &mut Limits) { + while let Ok(req) = rx.try_recv() { + // Errors are ignored with .ok(). Error means that the response channel + // is closed and the response isn't needed. + match req { + InfoMessage::GetLimits { response } => response.send(*limits).ok(), + InfoMessage::SetLimits { new, response } => { + *limits = new; + response.send(()).ok() + } + }; + } +} + +async fn freeze( + rx: &mut mpsc::Receiver, + mut slow_mode: Option<&mut HashMap>, + bot: &impl Requester, + mut imm: Option, +) { + while let Some(freeze_until) = imm.take().or_else(|| rx.try_recv().ok()) { + let FreezeUntil { until, after, chat } = freeze_until; + + // Clippy thinks that this `.as_deref_mut()` doesn't change the type (&mut + // HashMap -> &mut HashMap), but it's actually a reborrow (the lifetimes + // differ), since we are in a loop, simply using `slow_mode` would produce a + // moved-out error. + #[allow(clippy::needless_option_as_deref)] + if let Some(slow_mode) = slow_mode.as_deref_mut() { + // TODO: do something with channels?... + if let hash @ ChatIdHash::Id(id) = chat { + // TODO: maybe not call `get_chat` every time? + + // At this point there isn't much we can do with the error besides ignoring + if let Ok(chat) = bot.get_chat(id).await { + match chat.slow_mode_delay() { + Some(delay) => { + let now = Instant::now(); + let new_delay = Duration::from_secs(delay.into()); + slow_mode.insert(hash, (new_delay, now)); + } + None => { + slow_mode.remove(&hash); + } + }; + } + } + } + + // slow mode is enabled and it is <= to the delay asked by telegram + let slow_mode_enabled_and_likely_the_cause = slow_mode + .as_ref() + .and_then(|m| m.get(&chat).map(|(delay, _)| delay <= &after)) + .unwrap_or(false); + + // Do not sleep if slow mode is enabled since the freeze is most likely caused + // by the said slow mode and not by the global limits. + if !slow_mode_enabled_and_likely_the_cause { + log::warn!( + "freezing the bot for approximately {:?} due to `RetryAfter` error from telegram", + after + ); + + tokio::time::sleep_until(until.into()).await; + + log::warn!("unfreezing the bot"); + } + } +} + +async fn read_from_rx(rx: &mut mpsc::Receiver, queue: &mut Vec, rx_is_closed: &mut bool) { + if queue.is_empty() { + log::debug!("blocking on queue"); + + match rx.recv().await { + Some(req) => queue.push(req), + None => *rx_is_closed = true, + } + } + + // Don't grow queue bigger than the capacity to limit DOS possibility + while queue.len() < queue.capacity() { + match rx.try_recv() { + Ok(req) => queue.push(req), + Err(TryRecvError::Disconnected) => { + *rx_is_closed = true; + break; + } + // There are no items in queue. + Err(TryRecvError::Empty) => break, + } + } +} + +#[cfg(test)] +mod tests { + #[tokio::test] + async fn issue_535() { + let (tx, mut rx) = tokio::sync::mpsc::channel(1); + + // Close channel + drop(tx); + + // Previously this caused an infinite loop + super::read_from_rx::<()>(&mut rx, &mut Vec::new(), &mut false).await; + } +} diff --git a/crates/teloxide-core/src/adaptors/trace.rs b/crates/teloxide-core/src/adaptors/trace.rs new file mode 100644 index 00000000..bcc43828 --- /dev/null +++ b/crates/teloxide-core/src/adaptors/trace.rs @@ -0,0 +1,341 @@ +use std::{ + fmt::Debug, + future::{Future, IntoFuture}, + pin::Pin, + task::{self, Poll}, +}; + +use futures::ready; +use url::Url; + +use crate::{ + requests::{HasPayload, Output, Payload, Request, Requester}, + types::*, +}; + +/// Trace requests and responses. +/// +/// This is a tool for debugging. +/// +/// Depending on [`Settings`] and `log` facade this adaptor may output messages +/// like these: +/// ```text +/// TRACE teloxide_core::adaptors::trace > Sending `SendDice` request +/// TRACE teloxide_core::adaptors::trace > Got response from `SendDice` request +/// TRACE teloxide_core::adaptors::trace > Sending `SendDice` request: SendDice { chat_id: Id(0), emoji: Some(Dice), disable_notification: None, reply_to_message_id: None, allow_sending_without_reply: None, reply_markup: None } +/// TRACE teloxide_core::adaptors::trace > Got response from `SendDice` request: Ok(Message { id: 13812, date: 1625926524, chat: Chat { .. }, via_bot: None, kind: Dice(MessageDice { dice: Dice { emoji: Dice, value: 3 } }) }) +/// ``` +#[derive(Clone, Debug)] +pub struct Trace { + inner: B, + settings: Settings, +} + +impl Trace { + pub fn new(inner: B, settings: Settings) -> Self { + Self { inner, settings } + } + + pub fn inner(&self) -> &B { + &self.inner + } + + pub fn into_inner(self) -> B { + self.inner + } + + pub fn settings(&self) -> Settings { + self.settings + } +} + +bitflags::bitflags! { + /// [`Trace`] settings that determine what will be logged. + /// + /// ## Examples + /// + /// ``` + /// use teloxide_core::adaptors::trace::Settings; + /// + /// // Trace nothing + /// let _ = Settings::empty(); + /// // Trace only requests + /// let _ = Settings::TRACE_REQUESTS; + /// // Trace requests verbosely and responses (non verbosely) + /// let _ = Settings::TRACE_REQUESTS_VERBOSE | Settings::TRACE_RESPONSES; + /// ``` + pub struct Settings: u8 { + /// Trace requests (only request kind, e.g. `send_message`) + const TRACE_REQUESTS = 0b00000001; + + /// Trace requests verbosely (with all parameters). + /// + /// Implies [`TRACE_REQUESTS`] + const TRACE_REQUESTS_VERBOSE = 0b00000011; + + /// Trace responses (only request kind, e.g. `send_message`) + const TRACE_RESPONSES = 0b00000100; + + /// Trace responses verbosely (with full response). + /// + /// Implies [`TRACE_RESPONSES`] + const TRACE_RESPONSES_VERBOSE = 0b00001100; + + /// Trace everything. + /// + /// Implies [`TRACE_REQUESTS`] and [`TRACE_RESPONSES`]. + const TRACE_EVERYTHING = Self::TRACE_REQUESTS.bits | Self::TRACE_RESPONSES.bits; + + /// Trace everything verbosely. + /// + /// Implies [`TRACE_REQUESTS_VERBOSE`] and [`TRACE_RESPONSES_VERBOSE`]. + const TRACE_EVERYTHING_VERBOSE = Self::TRACE_REQUESTS_VERBOSE.bits | Self::TRACE_RESPONSES_VERBOSE.bits; + } +} + +macro_rules! fty { + ($T:ident) => { + TraceRequest + }; +} + +macro_rules! fwd_inner { + ($m:ident $this:ident ($($arg:ident : $T:ty),*)) => { + TraceRequest { + inner: $this.inner().$m($($arg),*), + settings: $this.settings + } + }; +} + +impl Requester for Trace +where + B: Requester, +{ + type Err = B::Err; + + requester_forward! { + get_me, + log_out, + close, + get_updates, + set_webhook, + delete_webhook, + get_webhook_info, + forward_message, + copy_message, + send_message, + send_photo, + send_audio, + send_document, + send_video, + send_animation, + send_voice, + send_video_note, + send_media_group, + send_location, + edit_message_live_location, + edit_message_live_location_inline, + stop_message_live_location, + stop_message_live_location_inline, + send_venue, + send_contact, + send_poll, + send_dice, + send_chat_action, + get_user_profile_photos, + get_file, + kick_chat_member, + ban_chat_member, + unban_chat_member, + restrict_chat_member, + promote_chat_member, + set_chat_administrator_custom_title, + ban_chat_sender_chat, + unban_chat_sender_chat, + set_chat_permissions, + export_chat_invite_link, + create_chat_invite_link, + edit_chat_invite_link, + revoke_chat_invite_link, + set_chat_photo, + delete_chat_photo, + set_chat_title, + set_chat_description, + pin_chat_message, + unpin_chat_message, + unpin_all_chat_messages, + leave_chat, + get_chat, + get_chat_administrators, + get_chat_members_count, + get_chat_member_count, + get_chat_member, + set_chat_sticker_set, + delete_chat_sticker_set, + answer_callback_query, + set_my_commands, + get_my_commands, + set_chat_menu_button, + get_chat_menu_button, + set_my_default_administrator_rights, + get_my_default_administrator_rights, + delete_my_commands, + answer_inline_query, + answer_web_app_query, + edit_message_text, + edit_message_text_inline, + edit_message_caption, + edit_message_caption_inline, + edit_message_media, + edit_message_media_inline, + edit_message_reply_markup, + edit_message_reply_markup_inline, + stop_poll, + delete_message, + send_sticker, + get_sticker_set, + get_custom_emoji_stickers, + upload_sticker_file, + create_new_sticker_set, + add_sticker_to_set, + set_sticker_position_in_set, + delete_sticker_from_set, + set_sticker_set_thumb, + send_invoice, + create_invoice_link, + answer_shipping_query, + answer_pre_checkout_query, + set_passport_data_errors, + send_game, + set_game_score, + set_game_score_inline, + get_game_high_scores, + approve_chat_join_request, + decline_chat_join_request + => fwd_inner, fty + } +} + +#[must_use = "Requests are lazy and do nothing unless sent"] +pub struct TraceRequest { + inner: R, + settings: Settings, +} + +impl TraceRequest +where + R: Request, +{ + fn trace_request(&self) + where + R::Payload: Debug, + { + if self.settings.contains(Settings::TRACE_REQUESTS_VERBOSE) { + log::trace!( + "Sending `{}` request: {:?}", + ::NAME, + self.inner.payload_ref() + ); + } else if self.settings.contains(Settings::TRACE_REQUESTS) { + log::trace!("Sending `{}` request", R::Payload::NAME); + } + } + + fn trace_response_fn(&self) -> fn(&Result, R::Err>) + where + Output: Debug, + R::Err: Debug, + { + if self.settings.contains(Settings::TRACE_RESPONSES_VERBOSE) { + |response| { + log::trace!("Got response from `{}` request: {:?}", R::Payload::NAME, response) + } + } else if self.settings.contains(Settings::TRACE_RESPONSES) { + |_| log::trace!("Got response from `{}` request", R::Payload::NAME) + } else { + |_| {} + } + } +} + +impl HasPayload for TraceRequest +where + R: HasPayload, +{ + type Payload = R::Payload; + + fn payload_mut(&mut self) -> &mut Self::Payload { + self.inner.payload_mut() + } + + fn payload_ref(&self) -> &Self::Payload { + self.inner.payload_ref() + } +} + +impl Request for TraceRequest +where + R: Request, + Output: Debug, + R::Err: Debug, + R::Payload: Debug, +{ + type Err = R::Err; + + type Send = Send; + + type SendRef = Send; + + fn send(self) -> Self::Send { + self.trace_request(); + + Send { trace_fn: self.trace_response_fn(), inner: self.inner.send() } + } + + fn send_ref(&self) -> Self::SendRef { + self.trace_request(); + + Send { trace_fn: self.trace_response_fn(), inner: self.inner.send_ref() } + } +} + +impl IntoFuture for TraceRequest +where + R: Request, + Output: Debug, + R::Err: Debug, + R::Payload: Debug, +{ + type Output = Result, ::Err>; + type IntoFuture = ::Send; + + fn into_future(self) -> Self::IntoFuture { + self.send() + } +} + +#[pin_project::pin_project] +pub struct Send +where + F: Future, +{ + trace_fn: fn(&F::Output), + #[pin] + inner: F, +} + +impl Future for Send +where + F: Future, +{ + type Output = F::Output; + + fn poll(self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> Poll { + let this = self.project(); + + let ret = ready!(this.inner.poll(cx)); + (this.trace_fn)(&ret); + Poll::Ready(ret) + } +} diff --git a/crates/teloxide-core/src/bot.rs b/crates/teloxide-core/src/bot.rs new file mode 100644 index 00000000..ef0801f8 --- /dev/null +++ b/crates/teloxide-core/src/bot.rs @@ -0,0 +1,297 @@ +use std::{future::Future, sync::Arc}; + +use reqwest::Client; +use serde::{de::DeserializeOwned, Serialize}; + +use crate::{ + net, + requests::{MultipartPayload, Payload, ResponseResult}, + serde_multipart, +}; + +mod api; +mod download; + +const TELOXIDE_TOKEN: &str = "TELOXIDE_TOKEN"; + +/// A requests sender. +/// +/// This is the main type of the library, it allows to send requests to the +/// [Telegram Bot API] and download files. +/// +/// ## TBA methods +/// +/// All TBA methods are located in the [`Requester`] [`impl for Bot`]. This +/// allows for opt-in behaviours using requester [adaptors]. +/// +/// ``` +/// # async { +/// use teloxide_core::prelude::*; +/// +/// let bot = Bot::new("TOKEN"); +/// dbg!(bot.get_me().await?); +/// # Ok::<_, teloxide_core::RequestError>(()) }; +/// ``` +/// +/// [`Requester`]: crate::requests::Requester +/// [`impl for Bot`]: Bot#impl-Requester +/// [adaptors]: crate::adaptors +/// +/// ## File download +/// +/// In the similar way as with TBA methods, file downloading methods are located +/// in a trait — [`Download<'_>`]. See its documentation for more. +/// +/// [`Download<'_>`]: crate::net::Download +/// +/// ## Clone cost +/// +/// `Bot::clone` is relatively cheap, so if you need to share `Bot`, it's +/// recommended to clone it, instead of wrapping it in [`Arc<_>`]. +/// +/// [`Arc`]: std::sync::Arc +/// [Telegram Bot API]: https://core.telegram.org/bots/api +#[must_use] +#[derive(Debug, Clone)] +pub struct Bot { + token: Arc, + api_url: Arc, + client: Client, +} + +/// Constructors +impl Bot { + /// Creates a new `Bot` with the specified token and the default + /// [http-client](reqwest::Client). + /// + /// # Panics + /// + /// If it cannot create [`reqwest::Client`]. + pub fn new(token: S) -> Self + where + S: Into, + { + let client = net::default_reqwest_settings().build().expect("Client creation failed"); + + Self::with_client(token, client) + } + + /// Creates a new `Bot` with the specified token and your + /// [`reqwest::Client`]. + /// + /// # Caution + /// + /// Your custom client might not be configured correctly to be able to work + /// in long time durations, see [issue 223]. + /// + /// [`reqwest::Client`]: https://docs.rs/reqwest/latest/reqwest/struct.Client.html + /// [issue 223]: https://github.com/teloxide/teloxide/issues/223 + pub fn with_client(token: S, client: Client) -> Self + where + S: Into, + { + let token = Into::::into(token).into(); + let api_url = Arc::new( + reqwest::Url::parse(net::TELEGRAM_API_URL) + .expect("Failed to parse default Telegram bot API url"), + ); + + Self { token, api_url, client } + } + + /// Creates a new `Bot` with the `TELOXIDE_TOKEN` & `TELOXIDE_PROXY` + /// environmental variables (a bot's token & a proxy) and the default + /// [`reqwest::Client`]. + /// + /// This function passes the value of `TELOXIDE_PROXY` into + /// [`reqwest::Proxy::all`], if it exists, otherwise returns the default + /// client. + /// + /// # Panics + /// - If cannot get the `TELOXIDE_TOKEN` environmental variable. + /// - If it cannot create [`reqwest::Client`]. + /// + /// [`reqwest::Client`]: https://docs.rs/reqwest/0.10.1/reqwest/struct.Client.html + /// [`reqwest::Proxy::all`]: https://docs.rs/reqwest/latest/reqwest/struct.Proxy.html#method.all + pub fn from_env() -> Self { + Self::from_env_with_client(crate::net::client_from_env()) + } + + /// Creates a new `Bot` with the `TELOXIDE_TOKEN` environmental variable (a + /// bot's token) and your [`reqwest::Client`]. + /// + /// # Panics + /// If cannot get the `TELOXIDE_TOKEN` environmental variable. + /// + /// # Caution + /// Your custom client might not be configured correctly to be able to work + /// in long time durations, see [issue 223]. + /// + /// [`reqwest::Client`]: https://docs.rs/reqwest/0.10.1/reqwest/struct.Client.html + /// [issue 223]: https://github.com/teloxide/teloxide/issues/223 + pub fn from_env_with_client(client: Client) -> Self { + Self::with_client(&get_env(TELOXIDE_TOKEN), client) + } + + /// Sets a custom API URL. + /// + /// For example, you can run your own [Telegram bot API server][tbas] and + /// set its URL using this method. + /// + /// [tbas]: https://github.com/tdlib/telegram-bot-api + /// + /// ## Examples + /// + /// ``` + /// use teloxide_core::{ + /// requests::{Request, Requester}, + /// Bot, + /// }; + /// + /// # async { + /// let url = reqwest::Url::parse("https://localhost/tbas").unwrap(); + /// let bot = Bot::new("TOKEN").set_api_url(url); + /// // From now all methods will use "https://localhost/tbas" as an API URL. + /// bot.get_me().await + /// # }; + /// ``` + /// + /// ## Multi-instance behaviour + /// + /// This method only sets the url for one bot instace, older clones are + /// unaffected. + /// + /// ``` + /// use teloxide_core::Bot; + /// + /// let bot = Bot::new("TOKEN"); + /// let bot2 = bot.clone(); + /// let bot = bot.set_api_url(reqwest::Url::parse("https://example.com/").unwrap()); + /// + /// assert_eq!(bot.api_url().as_str(), "https://example.com/"); + /// assert_eq!(bot.clone().api_url().as_str(), "https://example.com/"); + /// assert_ne!(bot2.api_url().as_str(), "https://example.com/"); + /// ``` + pub fn set_api_url(mut self, url: reqwest::Url) -> Self { + self.api_url = Arc::new(url); + self + } +} + +/// Getters +impl Bot { + /// Returns currently used token. + #[must_use] + pub fn token(&self) -> &str { + &self.token + } + + /// Returns currently used http-client. + #[must_use] + pub fn client(&self) -> &Client { + &self.client + } + + /// Returns currently used token API url. + #[must_use] + pub fn api_url(&self) -> reqwest::Url { + reqwest::Url::clone(&*self.api_url) + } +} + +impl Bot { + pub(crate) fn execute_json

( + &self, + payload: &P, + ) -> impl Future> + 'static + where + P: Payload + Serialize, + P::Output: DeserializeOwned, + { + let client = self.client.clone(); + let token = Arc::clone(&self.token); + let api_url = Arc::clone(&self.api_url); + + let timeout_hint = payload.timeout_hint(); + let params = serde_json::to_vec(payload) + // this `expect` should be ok since we don't write request those may trigger error here + .expect("serialization of request to be infallible"); + + // async move to capture client&token&api_url¶ms + async move { + net::request_json( + &client, + token.as_ref(), + reqwest::Url::clone(&*api_url), + P::NAME, + params, + timeout_hint, + ) + .await + } + } + + pub(crate) fn execute_multipart

( + &self, + payload: &mut P, + ) -> impl Future> + where + P: MultipartPayload + Serialize, + P::Output: DeserializeOwned, + { + let client = self.client.clone(); + let token = Arc::clone(&self.token); + let api_url = Arc::clone(&self.api_url); + + let timeout_hint = payload.timeout_hint(); + let params = serde_multipart::to_form(payload); + + // async move to capture client&token&api_url¶ms + async move { + let params = params?.await; + net::request_multipart( + &client, + token.as_ref(), + reqwest::Url::clone(&*api_url), + P::NAME, + params, + timeout_hint, + ) + .await + } + } + + pub(crate) fn execute_multipart_ref

( + &self, + payload: &P, + ) -> impl Future> + where + P: MultipartPayload + Serialize, + P::Output: DeserializeOwned, + { + let client = self.client.clone(); + let token = Arc::clone(&self.token); + let api_url = self.api_url.clone(); + + let timeout_hint = payload.timeout_hint(); + let params = serde_multipart::to_form_ref(payload); + + // async move to capture client&token&api_url¶ms + async move { + let params = params?.await; + net::request_multipart( + &client, + token.as_ref(), + reqwest::Url::clone(&*api_url), + P::NAME, + params, + timeout_hint, + ) + .await + } + } +} + +fn get_env(env: &'static str) -> String { + std::env::var(env).unwrap_or_else(|_| panic!("Cannot get the {} env variable", env)) +} diff --git a/crates/teloxide-core/src/bot/api.rs b/crates/teloxide-core/src/bot/api.rs new file mode 100644 index 00000000..1572a371 --- /dev/null +++ b/crates/teloxide-core/src/bot/api.rs @@ -0,0 +1,1198 @@ +use url::Url; + +use crate::{ + payloads, + prelude::Requester, + requests::{JsonRequest, MultipartRequest}, + types::{ + BotCommand, ChatId, ChatPermissions, InlineQueryResult, InputFile, InputMedia, + InputSticker, LabeledPrice, MessageId, Recipient, UserId, + }, + Bot, +}; + +impl Requester for Bot { + type Err = crate::errors::RequestError; + + type GetUpdates = JsonRequest; + + fn get_updates(&self) -> Self::GetUpdates { + Self::GetUpdates::new(self.clone(), payloads::GetUpdates::new()) + } + + type SetWebhook = MultipartRequest; + + fn set_webhook(&self, url: Url) -> Self::SetWebhook { + Self::SetWebhook::new(self.clone(), payloads::SetWebhook::new(url)) + } + + type DeleteWebhook = JsonRequest; + + fn delete_webhook(&self) -> Self::DeleteWebhook { + Self::DeleteWebhook::new(self.clone(), payloads::DeleteWebhook::new()) + } + + type GetWebhookInfo = JsonRequest; + + fn get_webhook_info(&self) -> Self::GetWebhookInfo { + Self::GetWebhookInfo::new(self.clone(), payloads::GetWebhookInfo::new()) + } + + type GetMe = JsonRequest; + + fn get_me(&self) -> Self::GetMe { + Self::GetMe::new(self.clone(), payloads::GetMe::new()) + } + + type SendMessage = JsonRequest; + + fn send_message(&self, chat_id: C, text: T) -> Self::SendMessage + where + C: Into, + T: Into, + { + Self::SendMessage::new(self.clone(), payloads::SendMessage::new(chat_id, text)) + } + + type ForwardMessage = JsonRequest; + + fn forward_message( + &self, + chat_id: C, + from_chat_id: F, + message_id: MessageId, + ) -> Self::ForwardMessage + where + C: Into, + F: Into, + { + Self::ForwardMessage::new( + self.clone(), + payloads::ForwardMessage::new(chat_id, from_chat_id, message_id), + ) + } + + type SendPhoto = MultipartRequest; + + fn send_photo(&self, chat_id: C, photo: InputFile) -> Self::SendPhoto + where + C: Into, + { + Self::SendPhoto::new(self.clone(), payloads::SendPhoto::new(chat_id, photo)) + } + + type SendAudio = MultipartRequest; + + fn send_audio(&self, chat_id: C, audio: InputFile) -> Self::SendAudio + where + C: Into, + { + Self::SendAudio::new(self.clone(), payloads::SendAudio::new(chat_id, audio)) + } + + type SendDocument = MultipartRequest; + + fn send_document(&self, chat_id: C, document: InputFile) -> Self::SendDocument + where + C: Into, + { + Self::SendDocument::new(self.clone(), payloads::SendDocument::new(chat_id, document)) + } + + type SendVideo = MultipartRequest; + + fn send_video(&self, chat_id: C, video: InputFile) -> Self::SendVideo + where + C: Into, + { + Self::SendVideo::new(self.clone(), payloads::SendVideo::new(chat_id, video)) + } + + type SendAnimation = MultipartRequest; + + fn send_animation(&self, chat_id: C, animation: InputFile) -> Self::SendAnimation + where + C: Into, + { + Self::SendAnimation::new(self.clone(), payloads::SendAnimation::new(chat_id, animation)) + } + + type SendVoice = MultipartRequest; + + fn send_voice(&self, chat_id: C, voice: InputFile) -> Self::SendVoice + where + C: Into, + { + Self::SendVoice::new(self.clone(), payloads::SendVoice::new(chat_id, voice)) + } + + type SendVideoNote = MultipartRequest; + + fn send_video_note(&self, chat_id: C, video_note: InputFile) -> Self::SendVideoNote + where + C: Into, + { + Self::SendVideoNote::new(self.clone(), payloads::SendVideoNote::new(chat_id, video_note)) + } + + type SendMediaGroup = MultipartRequest; + + fn send_media_group(&self, chat_id: C, media: M) -> Self::SendMediaGroup + where + C: Into, + M: IntoIterator, + { + Self::SendMediaGroup::new(self.clone(), payloads::SendMediaGroup::new(chat_id, media)) + } + + type SendLocation = JsonRequest; + + fn send_location(&self, chat_id: C, latitude: f64, longitude: f64) -> Self::SendLocation + where + C: Into, + { + Self::SendLocation::new( + self.clone(), + payloads::SendLocation::new(chat_id, latitude, longitude), + ) + } + + type EditMessageLiveLocation = JsonRequest; + + fn edit_message_live_location( + &self, + chat_id: C, + message_id: MessageId, + latitude: f64, + longitude: f64, + ) -> Self::EditMessageLiveLocation + where + C: Into, + { + Self::EditMessageLiveLocation::new( + self.clone(), + payloads::EditMessageLiveLocation::new(chat_id, message_id, latitude, longitude), + ) + } + + type EditMessageLiveLocationInline = JsonRequest; + + fn edit_message_live_location_inline( + &self, + inline_message_id: I, + latitude: f64, + longitude: f64, + ) -> Self::EditMessageLiveLocationInline + where + I: Into, + { + Self::EditMessageLiveLocationInline::new( + self.clone(), + payloads::EditMessageLiveLocationInline::new(inline_message_id, latitude, longitude), + ) + } + + type StopMessageLiveLocation = JsonRequest; + + fn stop_message_live_location( + &self, + chat_id: C, + message_id: MessageId, + latitude: f64, + longitude: f64, + ) -> Self::StopMessageLiveLocation + where + C: Into, + { + Self::StopMessageLiveLocation::new( + self.clone(), + payloads::StopMessageLiveLocation::new(chat_id, message_id, latitude, longitude), + ) + } + + type StopMessageLiveLocationInline = JsonRequest; + + fn stop_message_live_location_inline( + &self, + inline_message_id: I, + latitude: f64, + longitude: f64, + ) -> Self::StopMessageLiveLocationInline + where + I: Into, + { + Self::StopMessageLiveLocationInline::new( + self.clone(), + payloads::StopMessageLiveLocationInline::new(inline_message_id, latitude, longitude), + ) + } + + type SendVenue = JsonRequest; + + fn send_venue( + &self, + chat_id: C, + latitude: f64, + longitude: f64, + title: T, + address: A, + ) -> Self::SendVenue + where + C: Into, + T: Into, + A: Into, + { + Self::SendVenue::new( + self.clone(), + payloads::SendVenue::new(chat_id, latitude, longitude, title, address), + ) + } + + type SendContact = JsonRequest; + + fn send_contact(&self, chat_id: C, phone_number: P, first_name: F) -> Self::SendContact + where + C: Into, + P: Into, + F: Into, + { + Self::SendContact::new( + self.clone(), + payloads::SendContact::new(chat_id, phone_number, first_name), + ) + } + + type SendPoll = JsonRequest; + + fn send_poll(&self, chat_id: C, question: Q, options: O) -> Self::SendPoll + where + C: Into, + Q: Into, + O: IntoIterator, + { + Self::SendPoll::new(self.clone(), payloads::SendPoll::new(chat_id, question, options)) + } + + type SendDice = JsonRequest; + + fn send_dice(&self, chat_id: C) -> Self::SendDice + where + C: Into, + { + Self::SendDice::new(self.clone(), payloads::SendDice::new(chat_id)) + } + + type SendChatAction = JsonRequest; + + fn send_chat_action( + &self, + chat_id: C, + action: crate::types::ChatAction, + ) -> Self::SendChatAction + where + C: Into, + { + Self::SendChatAction::new(self.clone(), payloads::SendChatAction::new(chat_id, action)) + } + + type GetUserProfilePhotos = JsonRequest; + + fn get_user_profile_photos(&self, user_id: UserId) -> Self::GetUserProfilePhotos { + Self::GetUserProfilePhotos::new(self.clone(), payloads::GetUserProfilePhotos::new(user_id)) + } + + type GetFile = JsonRequest; + + fn get_file(&self, file_id: F) -> Self::GetFile + where + F: Into, + { + Self::GetFile::new(self.clone(), payloads::GetFile::new(file_id)) + } + + type KickChatMember = JsonRequest; + + fn kick_chat_member(&self, chat_id: C, user_id: UserId) -> Self::KickChatMember + where + C: Into, + { + Self::KickChatMember::new(self.clone(), payloads::KickChatMember::new(chat_id, user_id)) + } + + type BanChatMember = JsonRequest; + + fn ban_chat_member(&self, chat_id: C, user_id: UserId) -> Self::BanChatMember + where + C: Into, + { + Self::BanChatMember::new(self.clone(), payloads::BanChatMember::new(chat_id, user_id)) + } + + type UnbanChatMember = JsonRequest; + + fn unban_chat_member(&self, chat_id: C, user_id: UserId) -> Self::UnbanChatMember + where + C: Into, + { + Self::UnbanChatMember::new(self.clone(), payloads::UnbanChatMember::new(chat_id, user_id)) + } + + type RestrictChatMember = JsonRequest; + + fn restrict_chat_member( + &self, + chat_id: C, + user_id: UserId, + permissions: ChatPermissions, + ) -> Self::RestrictChatMember + where + C: Into, + { + Self::RestrictChatMember::new( + self.clone(), + payloads::RestrictChatMember::new(chat_id, user_id, permissions), + ) + } + + type PromoteChatMember = JsonRequest; + + fn promote_chat_member(&self, chat_id: C, user_id: UserId) -> Self::PromoteChatMember + where + C: Into, + { + Self::PromoteChatMember::new( + self.clone(), + payloads::PromoteChatMember::new(chat_id, user_id), + ) + } + + type SetChatAdministratorCustomTitle = JsonRequest; + + fn set_chat_administrator_custom_title( + &self, + chat_id: Ch, + user_id: UserId, + custom_title: Cu, + ) -> Self::SetChatAdministratorCustomTitle + where + Ch: Into, + Cu: Into, + { + Self::SetChatAdministratorCustomTitle::new( + self.clone(), + payloads::SetChatAdministratorCustomTitle::new(chat_id, user_id, custom_title), + ) + } + + type BanChatSenderChat = JsonRequest; + + fn ban_chat_sender_chat(&self, chat_id: C, sender_chat_id: S) -> Self::BanChatSenderChat + where + C: Into, + S: Into, + { + Self::BanChatSenderChat::new( + self.clone(), + payloads::BanChatSenderChat::new(chat_id, sender_chat_id), + ) + } + + type UnbanChatSenderChat = JsonRequest; + + fn unban_chat_sender_chat( + &self, + chat_id: C, + sender_chat_id: S, + ) -> Self::UnbanChatSenderChat + where + C: Into, + S: Into, + { + Self::UnbanChatSenderChat::new( + self.clone(), + payloads::UnbanChatSenderChat::new(chat_id, sender_chat_id), + ) + } + + type SetChatPermissions = JsonRequest; + + fn set_chat_permissions( + &self, + chat_id: C, + permissions: ChatPermissions, + ) -> Self::SetChatPermissions + where + C: Into, + { + Self::SetChatPermissions::new( + self.clone(), + payloads::SetChatPermissions::new(chat_id, permissions), + ) + } + + type ExportChatInviteLink = JsonRequest; + + fn export_chat_invite_link(&self, chat_id: C) -> Self::ExportChatInviteLink + where + C: Into, + { + Self::ExportChatInviteLink::new(self.clone(), payloads::ExportChatInviteLink::new(chat_id)) + } + + type CreateChatInviteLink = JsonRequest; + + fn create_chat_invite_link(&self, chat_id: C) -> Self::CreateChatInviteLink + where + C: Into, + { + Self::CreateChatInviteLink::new(self.clone(), payloads::CreateChatInviteLink::new(chat_id)) + } + + type EditChatInviteLink = JsonRequest; + + fn edit_chat_invite_link(&self, chat_id: C, invite_link: I) -> Self::EditChatInviteLink + where + C: Into, + I: Into, + { + Self::EditChatInviteLink::new( + self.clone(), + payloads::EditChatInviteLink::new(chat_id, invite_link), + ) + } + + type RevokeChatInviteLink = JsonRequest; + + fn revoke_chat_invite_link( + &self, + chat_id: C, + invite_link: I, + ) -> Self::RevokeChatInviteLink + where + C: Into, + I: Into, + { + Self::RevokeChatInviteLink::new( + self.clone(), + payloads::RevokeChatInviteLink::new(chat_id, invite_link), + ) + } + + type ApproveChatJoinRequest = JsonRequest; + + fn approve_chat_join_request( + &self, + chat_id: C, + user_id: UserId, + ) -> Self::ApproveChatJoinRequest + where + C: Into, + { + Self::ApproveChatJoinRequest::new( + self.clone(), + payloads::ApproveChatJoinRequest::new(chat_id, user_id), + ) + } + + type DeclineChatJoinRequest = JsonRequest; + + fn decline_chat_join_request( + &self, + chat_id: C, + user_id: UserId, + ) -> Self::DeclineChatJoinRequest + where + C: Into, + { + Self::DeclineChatJoinRequest::new( + self.clone(), + payloads::DeclineChatJoinRequest::new(chat_id, user_id), + ) + } + + type SetChatPhoto = MultipartRequest; + + fn set_chat_photo(&self, chat_id: C, photo: InputFile) -> Self::SetChatPhoto + where + C: Into, + { + Self::SetChatPhoto::new(self.clone(), payloads::SetChatPhoto::new(chat_id, photo)) + } + + type DeleteChatPhoto = JsonRequest; + + fn delete_chat_photo(&self, chat_id: C) -> Self::DeleteChatPhoto + where + C: Into, + { + Self::DeleteChatPhoto::new(self.clone(), payloads::DeleteChatPhoto::new(chat_id)) + } + + type SetChatTitle = JsonRequest; + + fn set_chat_title(&self, chat_id: C, title: T) -> Self::SetChatTitle + where + C: Into, + T: Into, + { + Self::SetChatTitle::new(self.clone(), payloads::SetChatTitle::new(chat_id, title)) + } + + type SetChatDescription = JsonRequest; + + fn set_chat_description(&self, chat_id: C) -> Self::SetChatDescription + where + C: Into, + { + Self::SetChatDescription::new(self.clone(), payloads::SetChatDescription::new(chat_id)) + } + + type PinChatMessage = JsonRequest; + + fn pin_chat_message(&self, chat_id: C, message_id: MessageId) -> Self::PinChatMessage + where + C: Into, + { + Self::PinChatMessage::new(self.clone(), payloads::PinChatMessage::new(chat_id, message_id)) + } + + type UnpinChatMessage = JsonRequest; + + fn unpin_chat_message(&self, chat_id: C) -> Self::UnpinChatMessage + where + C: Into, + { + Self::UnpinChatMessage::new(self.clone(), payloads::UnpinChatMessage::new(chat_id)) + } + + type LeaveChat = JsonRequest; + + fn leave_chat(&self, chat_id: C) -> Self::LeaveChat + where + C: Into, + { + Self::LeaveChat::new(self.clone(), payloads::LeaveChat::new(chat_id)) + } + + type GetChat = JsonRequest; + + fn get_chat(&self, chat_id: C) -> Self::GetChat + where + C: Into, + { + Self::GetChat::new(self.clone(), payloads::GetChat::new(chat_id)) + } + + type GetChatAdministrators = JsonRequest; + + fn get_chat_administrators(&self, chat_id: C) -> Self::GetChatAdministrators + where + C: Into, + { + Self::GetChatAdministrators::new( + self.clone(), + payloads::GetChatAdministrators::new(chat_id), + ) + } + + type GetChatMembersCount = JsonRequest; + + fn get_chat_members_count(&self, chat_id: C) -> Self::GetChatMembersCount + where + C: Into, + { + Self::GetChatMembersCount::new(self.clone(), payloads::GetChatMembersCount::new(chat_id)) + } + + type GetChatMemberCount = JsonRequest; + + fn get_chat_member_count(&self, chat_id: C) -> Self::GetChatMemberCount + where + C: Into, + { + Self::GetChatMemberCount::new(self.clone(), payloads::GetChatMemberCount::new(chat_id)) + } + + type GetChatMember = JsonRequest; + + fn get_chat_member(&self, chat_id: C, user_id: UserId) -> Self::GetChatMember + where + C: Into, + { + Self::GetChatMember::new(self.clone(), payloads::GetChatMember::new(chat_id, user_id)) + } + + type SetChatStickerSet = JsonRequest; + + fn set_chat_sticker_set(&self, chat_id: C, sticker_set_name: S) -> Self::SetChatStickerSet + where + C: Into, + S: Into, + { + Self::SetChatStickerSet::new( + self.clone(), + payloads::SetChatStickerSet::new(chat_id, sticker_set_name), + ) + } + + type DeleteChatStickerSet = JsonRequest; + + fn delete_chat_sticker_set(&self, chat_id: C) -> Self::DeleteChatStickerSet + where + C: Into, + { + Self::DeleteChatStickerSet::new(self.clone(), payloads::DeleteChatStickerSet::new(chat_id)) + } + + type AnswerCallbackQuery = JsonRequest; + + fn answer_callback_query(&self, callback_query_id: C) -> Self::AnswerCallbackQuery + where + C: Into, + { + Self::AnswerCallbackQuery::new( + self.clone(), + payloads::AnswerCallbackQuery::new(callback_query_id), + ) + } + + type SetMyCommands = JsonRequest; + + fn set_my_commands(&self, commands: C) -> Self::SetMyCommands + where + C: IntoIterator, + { + Self::SetMyCommands::new(self.clone(), payloads::SetMyCommands::new(commands)) + } + + type GetMyCommands = JsonRequest; + + fn get_my_commands(&self) -> Self::GetMyCommands { + Self::GetMyCommands::new(self.clone(), payloads::GetMyCommands::new()) + } + + type SetChatMenuButton = JsonRequest; + + fn set_chat_menu_button(&self) -> Self::SetChatMenuButton { + Self::SetChatMenuButton::new(self.clone(), payloads::SetChatMenuButton::new()) + } + + type GetChatMenuButton = JsonRequest; + + fn get_chat_menu_button(&self) -> Self::GetChatMenuButton { + Self::GetChatMenuButton::new(self.clone(), payloads::GetChatMenuButton::new()) + } + + type SetMyDefaultAdministratorRights = JsonRequest; + + fn set_my_default_administrator_rights(&self) -> Self::SetMyDefaultAdministratorRights { + Self::SetMyDefaultAdministratorRights::new( + self.clone(), + payloads::SetMyDefaultAdministratorRights::new(), + ) + } + + type GetMyDefaultAdministratorRights = JsonRequest; + + fn get_my_default_administrator_rights(&self) -> Self::GetMyDefaultAdministratorRights { + Self::GetMyDefaultAdministratorRights::new( + self.clone(), + payloads::GetMyDefaultAdministratorRights::new(), + ) + } + + type DeleteMyCommands = JsonRequest; + + fn delete_my_commands(&self) -> Self::DeleteMyCommands { + Self::DeleteMyCommands::new(self.clone(), payloads::DeleteMyCommands::new()) + } + + type AnswerInlineQuery = JsonRequest; + + fn answer_inline_query(&self, inline_query_id: I, results: R) -> Self::AnswerInlineQuery + where + I: Into, + R: IntoIterator, + { + Self::AnswerInlineQuery::new( + self.clone(), + payloads::AnswerInlineQuery::new(inline_query_id, results), + ) + } + + type AnswerWebAppQuery = JsonRequest; + + fn answer_web_app_query( + &self, + web_app_query_id: W, + result: InlineQueryResult, + ) -> Self::AnswerWebAppQuery + where + W: Into, + { + Self::AnswerWebAppQuery::new( + self.clone(), + payloads::AnswerWebAppQuery::new(web_app_query_id, result), + ) + } + + type EditMessageText = JsonRequest; + + fn edit_message_text( + &self, + chat_id: C, + message_id: MessageId, + text: T, + ) -> Self::EditMessageText + where + C: Into, + T: Into, + { + Self::EditMessageText::new( + self.clone(), + payloads::EditMessageText::new(chat_id, message_id, text), + ) + } + + type EditMessageTextInline = JsonRequest; + + fn edit_message_text_inline( + &self, + inline_message_id: I, + text: T, + ) -> Self::EditMessageTextInline + where + I: Into, + T: Into, + { + Self::EditMessageTextInline::new( + self.clone(), + payloads::EditMessageTextInline::new(inline_message_id, text), + ) + } + + type EditMessageCaption = JsonRequest; + + fn edit_message_caption(&self, chat_id: C, message_id: MessageId) -> Self::EditMessageCaption + where + C: Into, + { + Self::EditMessageCaption::new( + self.clone(), + payloads::EditMessageCaption::new(chat_id, message_id), + ) + } + + type EditMessageCaptionInline = JsonRequest; + + fn edit_message_caption_inline(&self, inline_message_id: I) -> Self::EditMessageCaptionInline + where + I: Into, + { + Self::EditMessageCaptionInline::new( + self.clone(), + payloads::EditMessageCaptionInline::new(inline_message_id), + ) + } + + type EditMessageMedia = MultipartRequest; + + fn edit_message_media( + &self, + chat_id: C, + message_id: MessageId, + media: InputMedia, + ) -> Self::EditMessageMedia + where + C: Into, + { + Self::EditMessageMedia::new( + self.clone(), + payloads::EditMessageMedia::new(chat_id, message_id, media), + ) + } + + type EditMessageMediaInline = MultipartRequest; + + fn edit_message_media_inline( + &self, + inline_message_id: I, + media: InputMedia, + ) -> Self::EditMessageMediaInline + where + I: Into, + { + Self::EditMessageMediaInline::new( + self.clone(), + payloads::EditMessageMediaInline::new(inline_message_id, media), + ) + } + + type EditMessageReplyMarkup = JsonRequest; + + fn edit_message_reply_markup( + &self, + chat_id: C, + message_id: MessageId, + ) -> Self::EditMessageReplyMarkup + where + C: Into, + { + Self::EditMessageReplyMarkup::new( + self.clone(), + payloads::EditMessageReplyMarkup::new(chat_id, message_id), + ) + } + + type EditMessageReplyMarkupInline = JsonRequest; + + fn edit_message_reply_markup_inline( + &self, + inline_message_id: I, + ) -> Self::EditMessageReplyMarkupInline + where + I: Into, + { + Self::EditMessageReplyMarkupInline::new( + self.clone(), + payloads::EditMessageReplyMarkupInline::new(inline_message_id), + ) + } + + type StopPoll = JsonRequest; + + fn stop_poll(&self, chat_id: C, message_id: MessageId) -> Self::StopPoll + where + C: Into, + { + Self::StopPoll::new(self.clone(), payloads::StopPoll::new(chat_id, message_id)) + } + + type DeleteMessage = JsonRequest; + + fn delete_message(&self, chat_id: C, message_id: MessageId) -> Self::DeleteMessage + where + C: Into, + { + Self::DeleteMessage::new(self.clone(), payloads::DeleteMessage::new(chat_id, message_id)) + } + + type SendSticker = MultipartRequest; + + fn send_sticker(&self, chat_id: C, sticker: InputFile) -> Self::SendSticker + where + C: Into, + { + Self::SendSticker::new(self.clone(), payloads::SendSticker::new(chat_id, sticker)) + } + + type GetStickerSet = JsonRequest; + + fn get_sticker_set(&self, name: N) -> Self::GetStickerSet + where + N: Into, + { + Self::GetStickerSet::new(self.clone(), payloads::GetStickerSet::new(name)) + } + + type GetCustomEmojiStickers = JsonRequest; + + fn get_custom_emoji_stickers(&self, custom_emoji_ids: C) -> Self::GetCustomEmojiStickers + where + C: IntoIterator, + { + Self::GetCustomEmojiStickers::new( + self.clone(), + payloads::GetCustomEmojiStickers::new(custom_emoji_ids), + ) + } + + type UploadStickerFile = MultipartRequest; + + fn upload_sticker_file( + &self, + user_id: UserId, + png_sticker: InputFile, + ) -> Self::UploadStickerFile where { + Self::UploadStickerFile::new( + self.clone(), + payloads::UploadStickerFile::new(user_id, png_sticker), + ) + } + + type CreateNewStickerSet = MultipartRequest; + + fn create_new_sticker_set( + &self, + user_id: UserId, + name: N, + title: T, + sticker: InputSticker, + emojis: E, + ) -> Self::CreateNewStickerSet + where + N: Into, + T: Into, + E: Into, + { + Self::CreateNewStickerSet::new( + self.clone(), + payloads::CreateNewStickerSet::new(user_id, name, title, sticker, emojis), + ) + } + + type AddStickerToSet = MultipartRequest; + + fn add_sticker_to_set( + &self, + user_id: UserId, + name: N, + sticker: InputSticker, + emojis: E, + ) -> Self::AddStickerToSet + where + N: Into, + E: Into, + { + Self::AddStickerToSet::new( + self.clone(), + payloads::AddStickerToSet::new(user_id, name, sticker, emojis), + ) + } + + type SetStickerPositionInSet = JsonRequest; + + fn set_sticker_position_in_set( + &self, + sticker: S, + position: u32, + ) -> Self::SetStickerPositionInSet + where + S: Into, + { + Self::SetStickerPositionInSet::new( + self.clone(), + payloads::SetStickerPositionInSet::new(sticker, position), + ) + } + + type DeleteStickerFromSet = JsonRequest; + + fn delete_sticker_from_set(&self, sticker: S) -> Self::DeleteStickerFromSet + where + S: Into, + { + Self::DeleteStickerFromSet::new(self.clone(), payloads::DeleteStickerFromSet::new(sticker)) + } + + type SetStickerSetThumb = MultipartRequest; + + fn set_sticker_set_thumb(&self, name: N, user_id: UserId) -> Self::SetStickerSetThumb + where + N: Into, + { + Self::SetStickerSetThumb::new( + self.clone(), + payloads::SetStickerSetThumb::new(name, user_id), + ) + } + + type SendInvoice = JsonRequest; + + fn send_invoice( + &self, + chat_id: Ch, + title: T, + description: D, + payload: Pa, + provider_token: P, + currency: C, + prices: Pri, + ) -> Self::SendInvoice + where + Ch: Into, + T: Into, + D: Into, + Pa: Into, + P: Into, + C: Into, + Pri: IntoIterator, + { + Self::SendInvoice::new( + self.clone(), + payloads::SendInvoice::new( + chat_id, + title, + description, + payload, + provider_token, + currency, + prices, + ), + ) + } + + type CreateInvoiceLink = JsonRequest; + + fn create_invoice_link( + &self, + title: T, + description: D, + payload: Pa, + provider_token: P, + currency: C, + prices: Pri, + ) -> Self::CreateInvoiceLink + where + T: Into, + D: Into, + Pa: Into, + P: Into, + C: Into, + Pri: IntoIterator, + { + Self::CreateInvoiceLink::new( + self.clone(), + payloads::CreateInvoiceLink::new( + title, + description, + payload, + provider_token, + currency, + prices, + ), + ) + } + + type AnswerShippingQuery = JsonRequest; + + fn answer_shipping_query(&self, shipping_query_id: S, ok: bool) -> Self::AnswerShippingQuery + where + S: Into, + { + Self::AnswerShippingQuery::new( + self.clone(), + payloads::AnswerShippingQuery::new(shipping_query_id, ok), + ) + } + + type AnswerPreCheckoutQuery = JsonRequest; + + fn answer_pre_checkout_query

( + &self, + pre_checkout_query_id: P, + ok: bool, + ) -> Self::AnswerPreCheckoutQuery + where + P: Into, + { + Self::AnswerPreCheckoutQuery::new( + self.clone(), + payloads::AnswerPreCheckoutQuery::new(pre_checkout_query_id, ok), + ) + } + + type SetPassportDataErrors = JsonRequest; + + fn set_passport_data_errors(&self, user_id: UserId, errors: E) -> Self::SetPassportDataErrors + where + E: IntoIterator, + { + Self::SetPassportDataErrors::new( + self.clone(), + payloads::SetPassportDataErrors::new(user_id, errors), + ) + } + + type SendGame = JsonRequest; + + fn send_game(&self, chat_id: u32, game_short_name: G) -> Self::SendGame + where + G: Into, + { + Self::SendGame::new(self.clone(), payloads::SendGame::new(chat_id, game_short_name)) + } + + type SetGameScore = JsonRequest; + + fn set_game_score( + &self, + user_id: UserId, + score: u64, + chat_id: u32, + message_id: MessageId, + ) -> Self::SetGameScore { + Self::SetGameScore::new( + self.clone(), + payloads::SetGameScore::new(user_id, score, chat_id, message_id), + ) + } + + type SetGameScoreInline = JsonRequest; + + fn set_game_score_inline( + &self, + user_id: UserId, + score: u64, + inline_message_id: I, + ) -> Self::SetGameScoreInline + where + I: Into, + { + Self::SetGameScoreInline::new( + self.clone(), + payloads::SetGameScoreInline::new(user_id, score, inline_message_id), + ) + } + + type GetGameHighScores = JsonRequest; + + fn get_game_high_scores(&self, user_id: UserId, target: T) -> Self::GetGameHighScores + where + T: Into, + { + Self::GetGameHighScores::new( + self.clone(), + payloads::GetGameHighScores::new(user_id, target), + ) + } + + type LogOut = JsonRequest; + + fn log_out(&self) -> Self::LogOut { + Self::LogOut::new(self.clone(), payloads::LogOut::new()) + } + + type Close = JsonRequest; + + fn close(&self) -> Self::Close { + Self::Close::new(self.clone(), payloads::Close::new()) + } + + type CopyMessage = JsonRequest; + + fn copy_message( + &self, + chat_id: C, + from_chat_id: F, + message_id: MessageId, + ) -> Self::CopyMessage + where + C: Into, + F: Into, + { + Self::CopyMessage::new( + self.clone(), + payloads::CopyMessage::new(chat_id, from_chat_id, message_id), + ) + } + + type UnpinAllChatMessages = JsonRequest; + + fn unpin_all_chat_messages(&self, chat_id: C) -> Self::UnpinAllChatMessages + where + C: Into, + { + Self::UnpinAllChatMessages::new(self.clone(), payloads::UnpinAllChatMessages::new(chat_id)) + } +} diff --git a/crates/teloxide-core/src/bot/download.rs b/crates/teloxide-core/src/bot/download.rs new file mode 100644 index 00000000..4b07460b --- /dev/null +++ b/crates/teloxide-core/src/bot/download.rs @@ -0,0 +1,47 @@ +use bytes::Bytes; +use futures::{future::BoxFuture, stream::BoxStream, FutureExt, StreamExt}; +use tokio::io::AsyncWrite; + +use crate::{ + bot::Bot, + net::{self, Download}, + DownloadError, +}; + +impl<'w> Download<'w> for Bot { + type Err = DownloadError; + + // I would like to unbox this, but my coworkers will kill me if they'll see yet + // another hand written `Future`. (waffle) + type Fut = BoxFuture<'w, Result<(), Self::Err>>; + + fn download_file( + &self, + path: &str, + destination: &'w mut (dyn AsyncWrite + Unpin + Send), + ) -> Self::Fut { + net::download_file( + &self.client, + reqwest::Url::clone(&*self.api_url), + &self.token, + path, + destination, + ) + .boxed() + } + + type StreamErr = reqwest::Error; + + type Stream = BoxStream<'static, Result>; + + fn download_file_stream(&self, path: &str) -> Self::Stream { + net::download_file_stream( + &self.client, + reqwest::Url::clone(&*self.api_url), + &self.token, + path, + ) + .map(|res| res.map_err(crate::errors::hide_token)) + .boxed() + } +} diff --git a/crates/teloxide-core/src/codegen.rs b/crates/teloxide-core/src/codegen.rs new file mode 100644 index 00000000..4c71c165 --- /dev/null +++ b/crates/teloxide-core/src/codegen.rs @@ -0,0 +1,185 @@ +//! `teloxide-core` uses codegen in order to implement request payloads and +//! `Requester` trait. +//! +//! These are utilities for doing codegen inspired by/stolen from r-a's +//! [sourcegen]. +//! +//! [sourcegen]: https://github.com/rust-lang/rust-analyzer/blob/master/crates/sourcegen + +// TODO(waffle): does it make sense to extract these utilities (at least project +// agnostic ones) in a standalone crate? + +pub(crate) mod convert; +mod patch; +pub(crate) mod schema; + +use std::{ + fs, + io::{Read, Write}, + path::{Path, PathBuf}, +}; + +use aho_corasick::AhoCorasick; +use xshell::{cmd, Shell}; + +fn ensure_rustfmt(sh: &Shell) { + // FIXME(waffle): find a better way to set toolchain + let toolchain = "nightly-2022-09-23"; + + let version = cmd!(sh, "rustup run {toolchain} rustfmt --version").read().unwrap_or_default(); + + if !version.contains("nightly") { + panic!( + "Failed to run rustfmt from toolchain '{toolchain}'. Please run `rustup component add \ + rustfmt --toolchain {toolchain}` to install it.", + ); + } +} + +pub fn reformat(text: String) -> String { + let toolchain = "nightly-2022-09-23"; + + let sh = Shell::new().unwrap(); + ensure_rustfmt(&sh); + let rustfmt_toml = project_root().join("../../rustfmt.toml"); + let mut stdout = cmd!( + sh, + "rustup run {toolchain} rustfmt --config-path {rustfmt_toml} --config fn_single_line=true" + ) + .stdin(text) + .read() + .unwrap(); + if !stdout.ends_with('\n') { + stdout.push('\n'); + } + stdout +} + +pub fn add_hidden_preamble(generator: &'static str, mut text: String) -> String { + let preamble = format!("// Generated by `{generator}`, do not edit by hand.\n\n"); + text.insert_str(0, &preamble); + text +} + +pub fn add_preamble(generator: &'static str, mut text: String) -> String { + let preamble = format!("//! Generated by `{generator}`, do not edit by hand.\n\n"); + text.insert_str(0, &preamble); + text +} + +/// Checks that the `file` has the specified `contents`. If that is not the +/// case, updates the file and then fails the test. +pub fn ensure_file_contents(file: &Path, contents: &str) { + ensure_files_contents([(file, contents)]) +} + +pub fn ensure_files_contents<'a>( + files_and_contents: impl IntoIterator, +) { + let mut err_count = 0; + + for (path, contents) in files_and_contents { + let mut file = fs::File::options().read(true).write(true).create(true).open(path).unwrap(); + let mut old_contents = String::with_capacity(contents.len()); + file.read_to_string(&mut old_contents).unwrap(); + + if normalize_newlines(&old_contents) == normalize_newlines(contents) { + // File is already up to date. + continue; + } + + err_count += 1; + + let display_path = path.strip_prefix(&project_root()).unwrap_or(path); + eprintln!( + "\n\x1b[31;1merror\x1b[0m: {} was not up-to-date, updating\n", + display_path.display() + ); + if let Some(parent) = path.parent() { + let _ = fs::create_dir_all(parent); + } + file.write_all(contents.as_bytes()).unwrap(); + } + + let (s, were) = match err_count { + // No erros, everything is up to date + 0 => return, + // Singular + 1 => ("", "was"), + // Plural + _ => ("s", "were"), + }; + + if std::env::var("CI").is_ok() { + eprintln!(" NOTE: run `cargo test` locally and commit the updated files\n"); + } + + panic!("some file{s} {were} not up to date and has been updated, simply re-run the tests"); +} + +pub fn replace_block(path: &Path, title: &str, new: &str) -> String { + let file = fs::read_to_string(path).unwrap(); + + let start = format!("// START BLOCK {title}\n"); + let end = format!("// END BLOCK {title}\n"); + + let mut starts = vec![]; + let mut ends = vec![]; + + let searcher = AhoCorasick::new_auto_configured(&[start, end]); + + for finding in searcher.find_iter(&file) { + match finding.pattern() { + // start + 0 => starts.push(finding.start()..finding.end()), + // end + 1 => ends.push(finding.start()..finding.end()), + n => panic!("{n}"), + } + } + + let start_offset = match &*starts { + [] => panic!("Coulnd't find start of block {title} in {p}", p = path.display()), + [offset] => offset.end, + [..] => panic!(), + }; + + let end_offset = match &*ends { + [] => panic!("Coulnd't find end of block {title} in {p}", p = path.display()), + [offset] => offset.start, + [..] => panic!(), + }; + + if end_offset < start_offset { + panic!("End of the {title} block is located before the start in {p}", p = path.display()); + } + + format!("{}{}{}", &file[..start_offset], new, &file[end_offset..]) +} + +fn normalize_newlines(s: &str) -> String { + s.replace("\r\n", "\n") +} + +/// Changes the first character in a string to uppercase. +pub fn to_uppercase(s: &str) -> String { + let mut chars = s.chars(); + format!("{}{}", chars.next().unwrap().to_uppercase(), chars.as_str()) +} + +pub fn project_root() -> PathBuf { + let dir = env!("CARGO_MANIFEST_DIR"); + let res = PathBuf::from(dir); + assert!(res.join("CHANGELOG.md").exists()); + res +} + +/// Returns minimal prefix of `l` such that `r` doesn't start with the prefix. +#[track_caller] +pub fn min_prefix<'a>(l: &'a str, r: &str) -> &'a str { + l.char_indices() + .zip(r.chars()) + .find(|((_, l), r)| l != r) + .map(|((i, _), _)| &l[..=i]) + .unwrap_or_else(|| panic!("there is no different prefix for {l} and {r}")) +} diff --git a/crates/teloxide-core/src/codegen/convert.rs b/crates/teloxide-core/src/codegen/convert.rs new file mode 100644 index 00000000..a31b3e88 --- /dev/null +++ b/crates/teloxide-core/src/codegen/convert.rs @@ -0,0 +1,32 @@ +use crate::codegen::schema::Type; + +pub enum Convert { + Id(Type), + Into(Type), + Collect(Type), +} + +pub fn convert_for(ty: &Type) -> Convert { + match ty { + ty @ Type::True + | ty @ Type::u8 + | ty @ Type::u16 + | ty @ Type::u32 + | ty @ Type::i32 + | ty @ Type::u64 + | ty @ Type::i64 + | ty @ Type::f64 + | ty @ Type::bool => Convert::Id(ty.clone()), + ty @ Type::String => Convert::Into(ty.clone()), + Type::Option(inner) => convert_for(inner), + Type::ArrayOf(ty) => Convert::Collect((**ty).clone()), + Type::RawTy(s) => match s.as_str() { + raw @ "Recipient" | raw @ "ChatId" | raw @ "TargetMessage" | raw @ "ReplyMarkup" => { + Convert::Into(Type::RawTy(raw.to_owned())) + } + raw => Convert::Id(Type::RawTy(raw.to_owned())), + }, + ty @ Type::Url => Convert::Id(ty.clone()), + ty @ Type::DateTime => Convert::Into(ty.clone()), + } +} diff --git a/crates/teloxide-core/src/codegen/patch.rs b/crates/teloxide-core/src/codegen/patch.rs new file mode 100644 index 00000000..60ebc820 --- /dev/null +++ b/crates/teloxide-core/src/codegen/patch.rs @@ -0,0 +1,315 @@ +use crate::codegen::schema::{Doc, Schema, Type}; + +pub fn patch_schema(mut schema: Schema) -> Schema { + fn check(l: &Option<&str>, r: &str) -> bool { + l.map(|m| r == m).unwrap_or(true) + } + + schema.methods.iter_mut().for_each(|method| { + method.params.iter_mut().map(|p| &mut p.name).for_each(escape_kw); + + DOC_PATCHES.iter().for_each(|(key, patch)| match key { + Target::Method(m) => { + if check(m, &method.names.0) { + method.doc.patch(patch, *key); + } + } + Target::Field { method_name: m, field_name: f } => { + if check(m, &method.names.0) { + method + .params + .iter_mut() + .filter(|p| check(f, &p.name)) + .for_each(|p| p.descr.patch(patch, *key)) + } + } + Target::Any { method_name: m } => { + if check(m, &method.names.0) { + method.doc.patch(patch, *key); + + method.params.iter_mut().for_each(|p| p.descr.patch(patch, *key)) + } + } + }); + }); + + schema +} + +static DOC_PATCHES: &[(Target, Patch)] = &[ + ( + Target::Any { method_name: None }, + Patch::ReplaceLink { + name: "More info on Sending Files »", + value: "crate::types::InputFile", + }, + ), + ( + Target::Field { + method_name: Some("sendChatAction"), + field_name: Some("action"), + }, + Patch::ReplaceLink { + name: "text messages", + value: "crate::payloads::SendMessage", + }, + ), + ( + Target::Field { + method_name: Some("sendChatAction"), + field_name: Some("action"), + }, + Patch::ReplaceLink { + name: "photos", + value: "crate::payloads::SendPhoto", + }, + ), + ( + Target::Field { + method_name: Some("sendChatAction"), + field_name: Some("action"), + }, + Patch::ReplaceLink { + name: "videos", + value: "crate::payloads::SendVideo", + }, + ), + ( + Target::Field { + method_name: Some("sendChatAction"), + field_name: Some("action"), + }, + Patch::ReplaceLink { + name: "audio files", + value: "crate::payloads::SendAudio", + }, + ), + ( + Target::Field { + method_name: Some("sendChatAction"), + field_name: Some("action"), + }, + Patch::ReplaceLink { + name: "general files", + value: "crate::payloads::SendDocument", + }, + ), + ( + Target::Field { + method_name: Some("sendChatAction"), + field_name: Some("action"), + }, + Patch::ReplaceLink { + name: "location data", + value: "crate::payloads::SendLocation", + }, + ), + ( + Target::Field { + method_name: Some("sendChatAction"), + field_name: Some("action"), + }, + Patch::ReplaceLink { + name: "video notes", + value: "crate::payloads::SendVideoNote", + }, + ), + ( + Target::Field { + method_name: Some("sendChatAction"), + field_name: Some("action"), + }, + Patch::ReplaceLink { + name: "stickers", + value: "crate::payloads::SendSticker", + }, + ), + ( + Target::Any { method_name: None }, + Patch::Custom(intra_links), + ), + ( + Target::Method(Some("addStickerToSet")), + Patch::Replace { + text: "You **must** use exactly one of the fields _png\\_sticker_ or _tgs\\_sticker_. ", + with: "", + }, + ), + ( + Target::Method(Some("GetFile")), + Patch::Replace { + text: "The file can then be downloaded via the link `https://api.telegram.org/file/bot/`, where `` is taken from the response. It is guaranteed that the link will be valid for at least 1 hour. When the link expires, a new one can be requested by calling [`GetFile`] again.", + with: "The file can then be downloaded via the method [`Bot::download_file(file_path, dst)`], where `file_path` is taken from the response. It is guaranteed that the path from [`GetFile`] will be valid for at least 1 hour. When the path expires, a new one can be requested by calling [`GetFile`].", + }, + ), + ( + Target::Method(Some("GetFile")), + Patch::AddLink { + name: "`Bot::download_file(file_path, dst)`", + value: "crate::net::Download::download_file", + }, + ), + // FIXME RETUNRS +]; + +#[derive(Debug, Clone, Copy)] +enum Target<'a> { + Any { method_name: Option<&'a str> }, + Method(Option<&'a str>), + Field { method_name: Option<&'a str>, field_name: Option<&'a str> }, +} + +impl<'a> Target<'a> { + fn is_exact(&self) -> bool { + match self { + Target::Method(m) => m.is_some(), + Target::Field { method_name, field_name } => { + method_name.is_some() && field_name.is_some() + } + Target::Any { method_name: _ } => false, + } + } +} + +enum Patch<'a> { + ReplaceLink { name: &'a str, value: &'a str }, + AddLink { name: &'a str, value: &'a str }, + // RemoveLink { name: &'a str }, + // FullReplace { text: &'a str, with: &'a str }, + Replace { text: &'a str, with: &'a str }, + Custom(fn(&mut Doc)), +} + +impl Doc { + fn patch(&mut self, patch: &Patch, key: Target) { + match patch { + Patch::ReplaceLink { name, value } => { + if let Some(link) = self.md_links.get_mut(*name) { + link.clear(); + *link += *value; + } else if key.is_exact() { + panic!("Patch error: {:?} doesn't have link {}", key, name); + } + } + Patch::AddLink { name, value } => { + self.md_links.insert((*name).to_owned(), (*value).to_owned()); + } + // Patch::RemoveLink { name } => drop(self.md_links.remove(*name)), + // Patch::FullReplace { text, with } => { + // assert_eq!(self.md.as_str(), *text); + + // self.md.clear(); + // self.md += with; + // } + Patch::Replace { text, with } => self.md = self.md.replace(*text, with), + Patch::Custom(f) => f(self), + } + } +} + +fn intra_links(doc: &mut Doc) { + let mut repls_t = Vec::new(); + let mut repls_m = Vec::new(); + + doc.md_links + .iter_mut() + .filter(|(k, v)| { + v.starts_with("https://core.telegram.org/bots/api#") + && !k.contains(&['-', '_', '.', ' '][..]) + }) + .for_each(|(k, v)| { + if let Some(c) = k.chars().next() { + match () { + _ if k == "games" => {} + _ if k == "unbanned" => *v = String::from("crate::payloads::UnbanChatMember"), + _ if c.is_lowercase() && !["update"].contains(&&**k) => { + repls_m.push(k.clone()); + *v = format!("crate::payloads::{}", to_uppercase(k)); + } + _ => { + repls_t.push(k.clone()); + *v = format!("crate::types::{}", k); + } + } + } + }); + + for repl in repls_t { + if let Some(value) = doc.md_links.remove(repl.as_str()) { + doc.md = doc.md.replace(format!("[{}]", repl).as_str(), &format!("[`{}`]", repl)); + doc.md_links.insert(format!("`{}`", repl), value); + } + } + + for repl in repls_m { + if let Some(value) = doc.md_links.remove(repl.as_str()) { + let repln = to_uppercase(&repl); + doc.md = doc.md.replace(format!("[{}]", repl).as_str(), &format!("[`{}`]", repln)); + doc.md_links.insert(format!("`{}`", repln), value); + } + } +} + +fn escape_kw(s: &mut String) { + if ["type"].contains(&s.as_str()) { + *s = format!("{}_", s); + } +} + +fn to_uppercase(s: &str) -> String { + let mut chars = s.chars(); + format!("{}{}", chars.next().unwrap().to_uppercase(), chars.as_str()) +} + +pub(crate) fn patch_ty(mut schema: Schema) -> Schema { + // URLs + patch_types(&mut schema, Type::String, Type::Url, &[("set_webhook", "url")]); + + patch_types( + &mut schema, + Type::Option(Box::new(Type::String)), + Type::Option(Box::new(Type::Url)), + &[("answer_callback_query", "url"), ("send_invoice", "photo_url")], + ); + + // Dates + patch_types( + &mut schema, + Type::Option(Box::new(Type::u64)), + Type::Option(Box::new(Type::DateTime)), + &[ + ("send_poll", "close_date"), + ("ban_chat_member", "until_date"), + ("kick_chat_member", "until_date"), + ("restrict_chat_member", "until_date"), + ], + ); + patch_types( + &mut schema, + Type::Option(Box::new(Type::i64)), + Type::Option(Box::new(Type::DateTime)), + &[("create_chat_invite_link", "expire_date"), ("edit_chat_invite_link", "expire_date")], + ); + + schema +} + +fn patch_types(schema: &mut Schema, from: Type, to: Type, list: &[(&str, &str)]) { + // URLs + for &(method, param) in list { + let m = schema + .methods + .iter_mut() + .find(|m| m.names.2 == method) + .expect("Couldn't find method for patching"); + + let p = m + .params + .iter_mut() + .find(|p| p.name == param) + .expect("Couldn't find parameter for patching"); + + assert_eq!(p.ty, from, "{}::{}", method, param); + p.ty = to.clone(); + } +} diff --git a/crates/teloxide-core/src/codegen/schema.rs b/crates/teloxide-core/src/codegen/schema.rs new file mode 100644 index 00000000..6f3eaa14 --- /dev/null +++ b/crates/teloxide-core/src/codegen/schema.rs @@ -0,0 +1,107 @@ +use std::fs; + +use indexmap::IndexMap as HashMap; +use serde::{Deserialize, Serialize}; + +use crate::codegen::project_root; + +pub fn get() -> Schema { + let path = project_root().join("schema.ron"); + let text = fs::read_to_string(path).unwrap(); + let schema = ron::from_str::(&text).unwrap(); + + let schema = super::patch::patch_schema(schema); + let schema = super::patch::patch_ty(schema); + + schema +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Schema { + pub api_version: ApiVersion, + pub methods: Vec, + pub tg_categories: HashMap, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ApiVersion { + pub ver: String, + pub date: String, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Method { + pub names: (String, String, String), + pub return_ty: Type, + pub doc: Doc, + pub tg_doc: String, + pub tg_category: String, + #[serde(default)] + pub notes: Vec, + pub params: Vec, + #[serde(default)] + pub sibling: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Doc { + pub md: String, + #[serde(default)] + pub md_links: HashMap, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Param { + pub name: String, + pub ty: Type, + pub descr: Doc, +} + +#[allow(non_camel_case_types)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub enum Type { + True, + u8, + u16, + u32, + i32, + u64, + i64, + f64, + bool, + String, + Option(Box), + ArrayOf(Box), + RawTy(String), + + Url, + DateTime, +} + +impl std::fmt::Display for Type { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Type::True => write!(f, "True"), + Type::u8 => write!(f, "u8"), + Type::u16 => write!(f, "u16"), + Type::u32 => write!(f, "u32"), + Type::i32 => write!(f, "i32"), + Type::u64 => write!(f, "u64"), + Type::i64 => write!(f, "i64"), + Type::f64 => write!(f, "f64"), + Type::bool => write!(f, "bool"), + Type::String => write!(f, "String"), + Type::Option(inner) => write!(f, "Option<{}>", inner), + Type::ArrayOf(inner) => write!(f, "Vec<{}>", inner), + Type::RawTy(raw) => f.write_str(raw), + Type::Url => write!(f, "Url"), + Type::DateTime => write!(f, "DateTime"), + } + } +} diff --git a/crates/teloxide-core/src/errors.rs b/crates/teloxide-core/src/errors.rs new file mode 100644 index 00000000..3de77f09 --- /dev/null +++ b/crates/teloxide-core/src/errors.rs @@ -0,0 +1,840 @@ +//! Possible error types. + +use std::{io, time::Duration}; + +use serde::Deserialize; +use thiserror::Error; + +use crate::types::ResponseParameters; + +/// An error caused by sending a request to Telegram. +#[derive(Debug, Error)] +pub enum RequestError { + /// A Telegram API error. + #[error("A Telegram's error: {0}")] + Api(#[from] ApiError), + + /// The group has been migrated to a supergroup with the specified + /// identifier. + #[error("The group has been migrated to a supergroup with ID #{0}")] + MigrateToChatId(i64), + + /// In case of exceeding flood control, the number of seconds left to wait + /// before the request can be repeated. + #[error("Retry after {0:?}")] + RetryAfter(Duration), + + /// Network error while sending a request to Telegram. + #[error("A network error: {0}")] + // NOTE: this variant must not be created by anything except the explicit From impl + Network(#[source] reqwest::Error), + + /// Error while parsing a response from Telegram. + /// + /// If you've received this error, please, [open an issue] with the + /// description of the error. + /// + /// [open an issue]: https://github.com/teloxide/teloxide/issues/new + #[error("An error while parsing JSON: {source} (raw: {raw:?})")] + InvalidJson { + #[source] + source: serde_json::Error, + /// The raw string JSON that couldn't been parsed + raw: Box, + }, + + /// Occurs when trying to send a file to Telegram. + #[error("An I/O error: {0}")] + Io(#[from] io::Error), +} + +/// An error caused by downloading a file. +#[derive(Debug, Error)] +pub enum DownloadError { + /// A network error while downloading a file from Telegram. + #[error("A network error: {0}")] + // NOTE: this variant must not be created by anything except the explicit From impl + Network(#[source] reqwest::Error), + + /// An I/O error while writing a file to destination. + #[error("An I/O error: {0}")] + Io(#[from] std::io::Error), +} + +pub trait AsResponseParameters { + fn response_parameters(&self) -> Option; + + fn retry_after(&self) -> Option { + self.response_parameters().and_then(|rp| match rp { + ResponseParameters::RetryAfter(n) => Some(n), + _ => None, + }) + } + + fn migrate_to_chat_id(&self) -> Option { + self.response_parameters().and_then(|rp| match rp { + ResponseParameters::MigrateToChatId(id) => Some(id), + _ => None, + }) + } +} + +impl AsResponseParameters for crate::RequestError { + fn response_parameters(&self) -> Option { + match *self { + Self::RetryAfter(n) => Some(ResponseParameters::RetryAfter(n)), + Self::MigrateToChatId(id) => Some(ResponseParameters::MigrateToChatId(id)), + _ => None, + } + } +} + +/// A kind of an API error. +#[derive(Debug, Error, Deserialize, PartialEq, Hash, Eq, Clone)] +#[serde(field_identifier)] +#[non_exhaustive] +pub enum ApiError { + /// Occurs when the bot tries to send message to user who blocked the bot. + #[serde(rename = "Forbidden: bot was blocked by the user")] + #[error("Forbidden: bot was blocked by the user")] + BotBlocked, + + /// Occurs when the bot token is incorrect. + #[serde(rename = "Not Found")] + #[error("Not Found")] + NotFound, + + /// Occurs when bot tries to modify a message without modification content. + /// + /// May happen in methods: + /// 1. [`EditMessageText`] + /// + /// [`EditMessageText`]: crate::payloads::EditMessageText + #[serde(rename = "Bad Request: message is not modified: specified new message content and \ + reply markup are exactly the same as a current content and reply markup \ + of the message")] + #[error( + "Bad Request: message is not modified: specified new message content and reply markup are \ + exactly the same as a current content and reply markup of the message" + )] + MessageNotModified, + + /// Occurs when bot tries to forward or delete a message which was deleted. + /// + /// May happen in methods: + /// 1. [`ForwardMessage`] + /// 2. [`DeleteMessage`] + /// + /// [`ForwardMessage`]: crate::payloads::ForwardMessage + /// [`DeleteMessage`]: crate::payloads::DeleteMessage + #[serde(rename = "Bad Request: MESSAGE_ID_INVALID")] + #[error("Bad Request: MESSAGE_ID_INVALID")] + MessageIdInvalid, + + /// Occurs when bot tries to forward a message which does not exists. + /// + /// May happen in methods: + /// 1. [`ForwardMessage`] + /// + /// [`ForwardMessage`]: crate::payloads::ForwardMessage + #[serde(rename = "Bad Request: message to forward not found")] + #[error("Bad Request: message to forward not found")] + MessageToForwardNotFound, + + /// Occurs when bot tries to delete a message which does not exists. + /// + /// May happen in methods: + /// 1. [`DeleteMessage`] + /// + /// [`DeleteMessage`]: crate::payloads::DeleteMessage + #[serde(rename = "Bad Request: message to delete not found")] + #[error("Bad Request: message to delete not found")] + MessageToDeleteNotFound, + + /// Occurs when bot tries to send a text message without text. + /// + /// May happen in methods: + /// 1. [`SendMessage`] + /// + /// [`SendMessage`]: crate::payloads::SendMessage + #[serde(rename = "Bad Request: message text is empty")] + #[error("Bad Request: message text is empty")] + MessageTextIsEmpty, + + /// Occurs when bot tries to edit a message after long time. + /// + /// May happen in methods: + /// 1. [`EditMessageText`] + /// + /// [`EditMessageText`]: crate::payloads::EditMessageText + #[serde(rename = "Bad Request: message can't be edited")] + #[error("Bad Request: message can't be edited")] + MessageCantBeEdited, + + /// Occurs when bot tries to delete a someone else's message in group where + /// it does not have enough rights. + /// + /// May happen in methods: + /// 1. [`DeleteMessage`] + /// + /// [`DeleteMessage`]: crate::payloads::DeleteMessage + #[serde(rename = "Bad Request: message can't be deleted")] + #[error("Bad Request: message can't be deleted")] + MessageCantBeDeleted, + + /// Occurs when bot tries to edit a message which does not exists. + /// + /// May happen in methods: + /// 1. [`EditMessageText`] + /// + /// [`EditMessageText`]: crate::payloads::EditMessageText + #[serde(rename = "Bad Request: message to edit not found")] + #[error("Bad Request: message to edit not found")] + MessageToEditNotFound, + + /// Occurs when bot tries to reply to a message which does not exists. + /// + /// May happen in methods: + /// 1. [`SendMessage`] + /// + /// [`SendMessage`]: crate::payloads::SendMessage + #[serde(rename = "Bad Request: reply message not found")] + #[error("Bad Request: reply message not found")] + MessageToReplyNotFound, + + /// Occurs when bot tries to + #[serde(rename = "Bad Request: message identifier is not specified")] + #[error("Bad Request: message identifier is not specified")] + MessageIdentifierNotSpecified, + + /// Occurs when bot tries to send a message with text size greater then + /// 4096 symbols. + /// + /// May happen in methods: + /// 1. [`SendMessage`] + /// + /// [`SendMessage`]: crate::payloads::SendMessage + #[serde(rename = "Bad Request: message is too long")] + #[error("Bad Request: message is too long")] + MessageIsTooLong, + + /// Occurs when bot tries to edit a message with text size greater then + /// 4096 symbols. + /// + /// May happen in methods: + /// 1. [`EditMessageText`] + /// 2. [`EditMessageTextInline`] + /// 3. [`EditMessageCaption`] + /// 4. [`EditMessageCaptionInline`] + /// + /// [`EditMessageText`]: crate::payloads::EditMessageText + /// [`EditMessageTextInline`]: crate::payloads::EditMessageTextInline + /// [`EditMessageCaption`]: crate::payloads::EditMessageCaption + /// [`EditMessageCaptionInline`]: crate::payloads::EditMessageCaptionInline + #[serde(rename = "Bad Request: MESSAGE_TOO_LONG")] + #[error("Bad Request: MESSAGE_TOO_LONG")] + EditedMessageIsTooLong, + + /// Occurs when bot tries to send media group with more than 10 items. + /// + /// May happen in methods: + /// 1. [`SendMediaGroup`] + /// + /// [`SendMediaGroup`]: crate::payloads::SendMediaGroup + #[serde(rename = "Bad Request: Too much messages to send as an album")] + #[error("Bad Request: Too much messages to send as an album")] + ToMuchMessages, + + /// Occurs when bot tries to answer an inline query with more than 50 + /// results. + /// + /// Consider using offsets to paginate results. + /// + /// May happen in methods: + /// 1. [`AnswerInlineQuery`] + /// + /// [`AnswerInlineQuery`]: crate::payloads::AnswerInlineQuery + #[serde(rename = "Bad Request: RESULTS_TOO_MUCH")] + #[error("Bad Request: RESULTS_TOO_MUCH")] + TooMuchInlineQueryResults, + + /// Occurs when bot tries to stop poll that has already been stopped. + /// + /// May happen in methods: + /// 1. [`SendPoll`] + /// + /// [`SendPoll`]: crate::payloads::SendPoll + #[serde(rename = "Bad Request: poll has already been closed")] + #[error("Bad Request: poll has already been closed")] + PollHasAlreadyClosed, + + /// Occurs when bot tries to send poll with less than 2 options. + /// + /// May happen in methods: + /// 1. [`SendPoll`] + /// + /// [`SendPoll`]: crate::payloads::SendPoll + #[serde(rename = "Bad Request: poll must have at least 2 option")] + #[error("Bad Request: poll must have at least 2 option")] + PollMustHaveMoreOptions, + + /// Occurs when bot tries to send poll with more than 10 options. + /// + /// May happen in methods: + /// 1. [`SendPoll`] + /// + /// [`SendPoll`]: crate::payloads::SendPoll + #[serde(rename = "Bad Request: poll can't have more than 10 options")] + #[error("Bad Request: poll can't have more than 10 options")] + PollCantHaveMoreOptions, + + /// Occurs when bot tries to send poll with empty option (without text). + /// + /// May happen in methods: + /// 1. [`SendPoll`] + /// + /// [`SendPoll`]: crate::payloads::SendPoll + #[serde(rename = "Bad Request: poll options must be non-empty")] + #[error("Bad Request: poll options must be non-empty")] + PollOptionsMustBeNonEmpty, + + /// Occurs when bot tries to send poll with empty question (without text). + /// + /// May happen in methods: + /// 1. [`SendPoll`] + /// + /// [`SendPoll`]: crate::payloads::SendPoll + #[serde(rename = "Bad Request: poll question must be non-empty")] + #[error("Bad Request: poll question must be non-empty")] + PollQuestionMustBeNonEmpty, + + /// Occurs when bot tries to send poll with total size of options more than + /// 100 symbols. + /// + /// May happen in methods: + /// 1. [`SendPoll`] + /// + /// [`SendPoll`]: crate::payloads::SendPoll + #[serde(rename = "Bad Request: poll options length must not exceed 100")] + #[error("Bad Request: poll options length must not exceed 100")] + PollOptionsLengthTooLong, + + /// Occurs when bot tries to send poll with question size more than 255 + /// symbols. + /// + /// May happen in methods: + /// 1. [`SendPoll`] + /// + /// [`SendPoll`]: crate::payloads::SendPoll + #[serde(rename = "Bad Request: poll question length must not exceed 255")] + #[error("Bad Request: poll question length must not exceed 255")] + PollQuestionLengthTooLong, + + /// Occurs when bot tries to stop poll with message without poll. + /// + /// May happen in methods: + /// 1. [`StopPoll`] + /// + /// [`StopPoll`]: crate::payloads::StopPoll + #[serde(rename = "Bad Request: message with poll to stop not found")] + #[error("Bad Request: message with poll to stop not found")] + MessageWithPollNotFound, + + /// Occurs when bot tries to stop poll with message without poll. + /// + /// May happen in methods: + /// 1. [`StopPoll`] + /// + /// [`StopPoll`]: crate::payloads::StopPoll + #[serde(rename = "Bad Request: message is not a poll")] + #[error("Bad Request: message is not a poll")] + MessageIsNotAPoll, + + /// Occurs when bot tries to send a message to chat in which it is not a + /// member. + /// + /// May happen in methods: + /// 1. [`SendMessage`] + /// + /// [`SendMessage`]: crate::payloads::SendMessage + #[serde(rename = "Bad Request: chat not found")] + #[error("Bad Request: chat not found")] + ChatNotFound, + + /// Occurs when bot tries to send method with unknown user_id. + /// + /// May happen in methods: + /// 1. [`getUserProfilePhotos`] + /// + /// [`getUserProfilePhotos`]: + /// crate::payloads::GetUserProfilePhotos + #[serde(rename = "Bad Request: user not found")] + #[error("Bad Request: user not found")] + UserNotFound, + + /// Occurs when bot tries to send [`SetChatDescription`] with same text as + /// in the current description. + /// + /// May happen in methods: + /// 1. [`SetChatDescription`] + /// + /// [`SetChatDescription`]: crate::payloads::SetChatDescription + #[serde(rename = "Bad Request: chat description is not modified")] + #[error("Bad Request: chat description is not modified")] + ChatDescriptionIsNotModified, + + /// Occurs when bot tries to answer to query after timeout expire. + /// + /// May happen in methods: + /// 1. [`AnswerCallbackQuery`] + /// + /// [`AnswerCallbackQuery`]: crate::payloads::AnswerCallbackQuery + #[serde(rename = "Bad Request: query is too old and response timeout expired or query id is \ + invalid")] + #[error("Bad Request: query is too old and response timeout expired or query id is invalid")] + InvalidQueryId, + + /// Occurs when bot tries to send InlineKeyboardMarkup with invalid button + /// url. + /// + /// May happen in methods: + /// 1. [`SendMessage`] + /// + /// [`SendMessage`]: crate::payloads::SendMessage + #[serde(rename = "Bad Request: BUTTON_URL_INVALID")] + #[error("Bad Request: BUTTON_URL_INVALID")] + ButtonUrlInvalid, + + /// Occurs when bot tries to send button with data size more than 64 bytes. + /// + /// May happen in methods: + /// 1. [`SendMessage`] + /// + /// [`SendMessage`]: crate::payloads::SendMessage + #[serde(rename = "Bad Request: BUTTON_DATA_INVALID")] + #[error("Bad Request: BUTTON_DATA_INVALID")] + ButtonDataInvalid, + + /// Occurs when bot tries to send button with data size == 0. + /// + /// May happen in methods: + /// 1. [`SendMessage`] + /// + /// [`SendMessage`]: crate::payloads::SendMessage + #[serde(rename = "Bad Request: can't parse inline keyboard button: Text buttons are \ + unallowed in the inline keyboard")] + #[error( + "Bad Request: can't parse inline keyboard button: Text buttons are unallowed in the \ + inline keyboard" + )] + TextButtonsAreUnallowed, + + /// Occurs when bot tries to get file by wrong file id. + /// + /// May happen in methods: + /// 1. [`GetFile`] + /// + /// [`GetFile`]: crate::payloads::GetFile + #[serde(rename = "Bad Request: wrong file id")] + #[error("Bad Request: wrong file id")] + WrongFileId, + + /// Occurs when bot tries to send files with wrong file identifier or HTTP + /// url + #[serde(rename = "Bad Request: wrong file identifier/HTTP URL specified")] + #[error("Bad Request: wrong file identifier/HTTP URL specified")] + WrongFileIdOrUrl, + + /// Occurs when When sending files with an url to a site that doesn't + /// respond. + #[serde(rename = "Bad Request: failed to get HTTP URL content")] + #[error("Bad Request: failed to get HTTP URL content")] + FailedToGetUrlContent, + + /// Occurs when bot tries to do some with group which was deactivated. + #[serde(rename = "Bad Request: group is deactivated")] + #[error("Bad Request: group is deactivated")] + GroupDeactivated, + + /// Occurs when bot tries to set chat photo from file ID + /// + /// May happen in methods: + /// 1. [`SetChatPhoto`] + /// + /// [`SetChatPhoto`]: crate::payloads::SetChatPhoto + #[serde(rename = "Bad Request: Photo should be uploaded as an InputFile")] + #[error("Bad Request: Photo should be uploaded as an InputFile")] + PhotoAsInputFileRequired, + + /// Occurs when bot tries to add sticker to stickerset by invalid name. + /// + /// May happen in methods: + /// 1. [`AddStickerToSet`] + /// + /// [`AddStickerToSet`]: crate::payloads::AddStickerToSet + #[serde(rename = "Bad Request: STICKERSET_INVALID")] + #[error("Bad Request: STICKERSET_INVALID")] + InvalidStickersSet, + + /// Occurs when bot tries to create a sticker set with a name that is + /// already used by another sticker set. + /// + /// May happen in methods: + /// 1. [`CreateNewStickerSet`] + /// + /// [`CreateNewStickerSet`]: crate::payloads::CreateNewStickerSet + #[serde(rename = "Bad Request: sticker set name is already occupied")] + #[error("Bad Request: sticker set name is already occupied")] + StickerSetNameOccupied, + + /// Occurs when bot tries to create a sticker set with user id of a bot. + /// + /// May happen in methods: + /// 1. [`CreateNewStickerSet`] + /// + /// [`CreateNewStickerSet`]: crate::payloads::CreateNewStickerSet + #[serde(rename = "Bad Request: USER_IS_BOT")] + #[error("Bad Request: USER_IS_BOT")] + StickerSetOwnerIsBot, + + /// Occurs when bot tries to create a sticker set with invalid name. + /// + /// From documentation of [`CreateNewStickerSet`]: + /// > Short name of sticker set, to be used in `t.me/addstickers/` URLs + /// (e.g., _animals_). Can contain only english letters, digits and + /// underscores. Must begin with a letter, can't contain consecutive + /// underscores and must end in “\_by\_”. + /// is case insensitive. 1-64 characters. + /// + /// May happen in methods: + /// 1. [`CreateNewStickerSet`] + /// + /// [`CreateNewStickerSet`]: crate::payloads::CreateNewStickerSet + #[serde(rename = "Bad Request: invalid sticker set name is specified")] + #[error("Bad Request: invalid sticker set name is specified")] + InvalidStickerName, + + /// Occurs when bot tries to pin a message without rights to pin in this + /// chat. + /// + /// May happen in methods: + /// 1. [`PinChatMessage`] + /// + /// [`PinChatMessage`]: crate::payloads::PinChatMessage + #[serde(rename = "Bad Request: not enough rights to pin a message")] + #[error("Bad Request: not enough rights to pin a message")] + NotEnoughRightsToPinMessage, + + /// Occurs when bot tries to pin or unpin a message without rights to pin + /// in this chat. + /// + /// May happen in methods: + /// 1. [`PinChatMessage`] + /// 2. [`UnpinChatMessage`] + /// + /// [`PinChatMessage`]: crate::payloads::PinChatMessage + /// [`UnpinChatMessage`]: crate::payloads::UnpinChatMessage + #[serde(rename = "Bad Request: not enough rights to manage pinned messages in the chat")] + #[error("Bad Request: not enough rights to manage pinned messages in the chat")] + NotEnoughRightsToManagePins, + + /// Occurs when bot tries change default chat permissions without "Ban + /// Users" permission in this chat. + /// + /// May happen in methods: + /// 1. [`SetChatPermissions`] + /// + /// [`SetChatPermissions`]: crate::payloads::SetChatPermissions + #[serde(rename = "Bad Request: not enough rights to change chat permissions")] + #[error("Bad Request: not enough rights to change chat permissions")] + NotEnoughRightsToChangeChatPermissions, + + /// Occurs when bot tries to use method in group which is allowed only in a + /// supergroup or channel. + #[serde(rename = "Bad Request: method is available only for supergroups and channel")] + #[error("Bad Request: method is available only for supergroups and channel")] + MethodNotAvailableInPrivateChats, + + /// Occurs when bot tries to demote chat creator. + /// + /// May happen in methods: + /// 1. [`PromoteChatMember`] + /// + /// [`PromoteChatMember`]: crate::payloads::PromoteChatMember + #[serde(rename = "Bad Request: can't demote chat creator")] + #[error("Bad Request: can't demote chat creator")] + CantDemoteChatCreator, + + /// Occurs when bot tries to restrict self in group chats. + /// + /// May happen in methods: + /// 1. [`RestrictChatMember`] + /// + /// [`RestrictChatMember`]: crate::payloads::RestrictChatMember + #[serde(rename = "Bad Request: can't restrict self")] + #[error("Bad Request: can't restrict self")] + CantRestrictSelf, + + /// Occurs when bot tries to restrict chat member without rights to + /// restrict in this chat. + /// + /// May happen in methods: + /// 1. [`RestrictChatMember`] + /// + /// [`RestrictChatMember`]: crate::payloads::RestrictChatMember + #[serde(rename = "Bad Request: not enough rights to restrict/unrestrict chat member")] + #[error("Bad Request: not enough rights to restrict/unrestrict chat member")] + NotEnoughRightsToRestrict, + + /// Occurs when bot tries to post a message in a channel without "Post + /// Messages" admin right. + #[serde(rename = "Bad Request: need administrator rights in the channel chat")] + #[error("Bad Request: need administrator rights in the channel chat")] + NotEnoughRightsToPostMessages, + + /// Occurs when bot tries set webhook to protocol other than HTTPS. + /// + /// May happen in methods: + /// 1. [`SetWebhook`] + /// + /// [`SetWebhook`]: crate::payloads::SetWebhook + #[serde(rename = "Bad Request: bad webhook: HTTPS url must be provided for webhook")] + #[error("Bad Request: bad webhook: HTTPS url must be provided for webhook")] + WebhookRequireHttps, + + /// Occurs when bot tries to set webhook to port other than 80, 88, 443 or + /// 8443. + /// + /// May happen in methods: + /// 1. [`SetWebhook`] + /// + /// [`SetWebhook`]: crate::payloads::SetWebhook + #[serde(rename = "Bad Request: bad webhook: Webhook can be set up only on ports 80, 88, 443 \ + or 8443")] + #[error("Bad Request: bad webhook: Webhook can be set up only on ports 80, 88, 443 or 8443")] + BadWebhookPort, + + /// Occurs when bot tries to set webhook to unknown host. + /// + /// May happen in methods: + /// 1. [`SetWebhook`] + /// + /// [`SetWebhook`]: crate::payloads::SetWebhook + #[serde(rename = "Bad Request: bad webhook: Failed to resolve host: Name or service not known")] + #[error("Bad Request: bad webhook: Failed to resolve host: Name or service not known")] + UnknownHost, + + /// Occurs when bot tries to set webhook to invalid URL. + /// + /// May happen in methods: + /// 1. [`SetWebhook`] + /// + /// [`SetWebhook`]: crate::payloads::SetWebhook + #[serde(rename = "Bad Request: can't parse URL")] + #[error("Bad Request: can't parse URL")] + CantParseUrl, + + /// Occurs when bot tries to send message with unfinished entities. + /// + /// May happen in methods: + /// 1. [`SendMessage`] + /// + /// [`SendMessage`]: crate::payloads::SendMessage + #[serde(rename = "Bad Request: can't parse entities")] + #[error("Bad Request: can't parse entities")] + CantParseEntities, + + /// Occurs when bot tries to use getUpdates while webhook is active. + /// + /// May happen in methods: + /// 1. [`GetUpdates`] + /// + /// [`GetUpdates`]: crate::payloads::GetUpdates + #[serde(rename = "can't use getUpdates method while webhook is active")] + #[error("can't use getUpdates method while webhook is active")] + CantGetUpdates, + + /// Occurs when bot tries to do some in group where bot was kicked. + /// + /// May happen in methods: + /// 1. [`SendMessage`] + /// + /// [`SendMessage`]: crate::payloads::SendMessage + #[serde(rename = "Unauthorized: bot was kicked from a chat")] + #[error("Unauthorized: bot was kicked from a chat")] + BotKicked, + + /// Occurs when bot tries to do something in a supergroup the bot was + /// kicked from. + /// + /// May happen in methods: + /// 1. [`SendMessage`] + /// + /// [`SendMessage`]: crate::payloads::SendMessage + #[serde(rename = "Forbidden: bot was kicked from the supergroup chat")] + #[error("Forbidden: bot was kicked from the supergroup chat")] + BotKickedFromSupergroup, + + /// Occurs when bot tries to send a message to a deactivated user (i.e. a + /// user that was banned by telegram). + /// + /// May happen in methods: + /// 1. [`SendMessage`] + /// + /// [`SendMessage`]: crate::payloads::SendMessage + #[serde(rename = "Forbidden: user is deactivated")] + #[error("Forbidden: user is deactivated")] + UserDeactivated, + + /// Occurs when you tries to initiate conversation with a user. + /// + /// May happen in methods: + /// 1. [`SendMessage`] + /// + /// [`SendMessage`]: crate::payloads::SendMessage + #[serde(rename = "Unauthorized: bot can't initiate conversation with a user")] + #[error("Unauthorized: bot can't initiate conversation with a user")] + CantInitiateConversation, + + /// Occurs when you tries to send message to bot. + /// + /// May happen in methods: + /// 1. [`SendMessage`] + /// + /// [`SendMessage`]: crate::payloads::SendMessage + #[serde(rename = "Unauthorized: bot can't send messages to bots")] + #[error("Unauthorized: bot can't send messages to bots")] + CantTalkWithBots, + + /// Occurs when bot tries to send button with invalid http url. + /// + /// May happen in methods: + /// 1. [`SendMessage`] + /// + /// [`SendMessage`]: crate::payloads::SendMessage + #[serde(rename = "Bad Request: wrong HTTP URL")] + #[error("Bad Request: wrong HTTP URL")] + WrongHttpUrl, + + /// Occurs when bot tries GetUpdate before the timeout. Make sure that only + /// one Updater is running. + /// + /// May happen in methods: + /// 1. [`GetUpdates`] + /// + /// [`GetUpdates`]: crate::payloads::GetUpdates + #[serde(rename = "Conflict: terminated by other getUpdates request; make sure that only one \ + bot instance is running")] + #[error( + "Conflict: terminated by other getUpdates request; make sure that only one bot instance \ + is running" + )] + TerminatedByOtherGetUpdates, + + /// Occurs when bot tries to get file by invalid file id. + /// + /// May happen in methods: + /// 1. [`GetFile`] + /// + /// [`GetFile`]: crate::payloads::GetFile + #[serde(rename = "Bad Request: invalid file id")] + #[error("Bad Request: invalid file id")] + FileIdInvalid, + + /// Error which is not known to `teloxide`. + /// + /// If you've received this error, please [open an issue] with the + /// description of the error. + /// + /// [open an issue]: https://github.com/teloxide/teloxide/issues/new + #[error("Unknown error: {0:?}")] + Unknown(String), +} + +/// This impl allows to use `?` to propagate [`DownloadError`]s in function +/// returning [`RequestError`]s. For example: +/// +/// ```rust +/// # use teloxide_core::errors::{DownloadError, RequestError}; +/// +/// async fn handler() -> Result<(), RequestError> { +/// download_file().await?; // `?` just works +/// +/// Ok(()) +/// } +/// +/// async fn download_file() -> Result<(), DownloadError> { +/// /* download file here */ +/// Ok(()) +/// } +/// ``` +impl From for RequestError { + fn from(download_err: DownloadError) -> Self { + match download_err { + DownloadError::Network(err) => RequestError::Network(err), + DownloadError::Io(err) => RequestError::Io(err), + } + } +} + +impl From for DownloadError { + fn from(error: reqwest::Error) -> Self { + DownloadError::Network(hide_token(error)) + } +} + +impl From for RequestError { + fn from(error: reqwest::Error) -> Self { + RequestError::Network(hide_token(error)) + } +} + +/// Replaces token in the url in the error with `token:redacted` string. +pub(crate) fn hide_token(mut error: reqwest::Error) -> reqwest::Error { + let url = match error.url_mut() { + Some(url) => url, + None => return error, + }; + + if let Some(mut segments) = url.path_segments() { + // Usually the url looks like "bot/..." or "file/bot/...". + let (beginning, segment) = match segments.next() { + Some("file") => ("file/", segments.next()), + segment => ("", segment), + }; + + if let Some(token) = segment.and_then(|s| s.strip_prefix("bot")) { + // make sure that what we are about to delete looks like a bot token + if let Some((id, secret)) = token.split_once(':') { + // The part before the : in the token is the id of the bot. + let id_character = |c: char| c.is_ascii_digit(); + + // The part after the : in the token is the secret. + // + // In all bot tokens we could find the secret is 35 characters long and is + // 0-9a-zA-Z_- only. + // + // It would be nice to research if TBA always has 35 character secrets or if it + // is just a coincidence. + const SECRET_LENGTH: usize = 35; + let secret_character = |c: char| c.is_ascii_alphanumeric() || c == '-' || c == '_'; + + if secret.len() >= SECRET_LENGTH + && id.chars().all(id_character) + && secret.chars().all(secret_character) + { + // found token, hide only the token + let without_token = + &url.path()[(beginning.len() + "/bot".len() + token.len())..]; + let redacted = format!("{beginning}token:redacted{without_token}"); + + url.set_path(&redacted); + return error; + } + } + } + } + + // couldn't find token in the url, hide the whole url + error.without_url() +} diff --git a/crates/teloxide-core/src/lib.rs b/crates/teloxide-core/src/lib.rs new file mode 100644 index 00000000..7ac35e29 --- /dev/null +++ b/crates/teloxide-core/src/lib.rs @@ -0,0 +1,122 @@ +//! Core part of the [`teloxide`] library. +//! +//! This library provides tools for making requests to the [Telegram Bot API] +//! (Currently, version `6.2` is supported) with ease. The library is fully +//! asynchronous and built using [`tokio`]. +//! +//!```toml +//! teloxide_core = "0.8" +//! ``` +//! _Compiler support: requires rustc 1.64+_. +//! +//! ``` +//! # async { +//! # let chat_id = teloxide_core::types::ChatId(-1); +//! use teloxide_core::{ +//! prelude::*, +//! types::{DiceEmoji, ParseMode}, +//! }; +//! +//! let bot = Bot::from_env().parse_mode(ParseMode::MarkdownV2); +//! +//! let me = bot.get_me().await?; +//! +//! bot.send_dice(chat_id).emoji(DiceEmoji::Dice).await?; +//! bot.send_message(chat_id, format!("Hi, my name is **{}** 👋", me.user.first_name)).await?; +//! # Ok::<_, Box>(()) }; +//! ``` +//! +//!

+//! +//!
+//! +//! [`teloxide`]: https://docs.rs/teloxide +//! [Telegram Bot API]: https://core.telegram.org/bots/api +//! [`tokio`]: https://tokio.rs +//! +//! ## Cargo features +//! +//! - `native-tls` = use [`native-tls`] tls implementation (**enabled by +//! default**) +//! - `rustls` — use [`rustls`] tls implementation +//! - `trace_adaptor` — enables [`Trace`] bot adaptor +//! - `erased` — enables [`ErasedRequester`] bot adaptor +//! - `throttle` — enables [`Throttle`] bot adaptor +//! - `cache_me` — enables [`CacheMe`] bot adaptor +//! - `full` — enables all features except `nightly` and tls-related +//! - `nightly` — enables nightly-only features, currently: +//! - Removes some future boxing using `#![feature(type_alias_impl_trait)]` +//! - Used to built docs (`#![feature(doc_cfg, doc_notable_trait)]`) +//! - `auto_send` — enables [`AutoSend`] bot adaptor (deprecated) +//! +//! [`AutoSend`]: adaptors::AutoSend +//! [`Trace`]: adaptors::Trace +//! [`ErasedRequester`]: adaptors::ErasedRequester +//! [`Throttle`]: adaptors::Throttle +//! [`CacheMe`]: adaptors::CacheMe +//! [`native-tls`]: https://docs.rs/native-tls +//! [`rustls`]: https://docs.rs/rustls + +#![doc( + // FIXME(waffle): use github + html_logo_url = "https://cdn.discordapp.com/attachments/224881373326999553/798598120760934410/logo.png", + html_favicon_url = "https://cdn.discordapp.com/attachments/224881373326999553/798598120760934410/logo.png" +)] +#![forbid(unsafe_code)] +// we pass "--cfg docsrs" when building docs to add `This is supported on feature="..." only.` +// +// To properly build docs of this crate run +// ```console +// $ cargo docs +// ``` +// (docs alias is defined in `.cargo/config.toml`) +// +// `dep_docsrs` is used for the same purpose, but when `teloxide-core` is built as a dependency +// (see: `teloxide`). We can't use `docsrs` as it breaks tokio compilation in this case. +#![cfg_attr( + all(any(docsrs, dep_docsrs), feature = "nightly"), + feature(doc_cfg, doc_auto_cfg, doc_notable_trait) +)] +#![cfg_attr(feature = "nightly", feature(type_alias_impl_trait))] +#![cfg_attr(all(feature = "full", docsrs), deny(rustdoc::broken_intra_doc_links))] +//#![deny(missing_docs)] +#![warn(clippy::print_stdout, clippy::dbg_macro)] +#![allow(clippy::let_and_return)] +#![allow(clippy::bool_assert_comparison)] +// Unless this becomes machine applicable, I'm not adding 334 #[must_use]s (waffle) +#![allow(clippy::return_self_not_must_use)] +// Workaround for CI +#![allow(rustdoc::bare_urls)] +// FIXME: deal with these lints +#![allow( + clippy::collapsible_str_replace, + clippy::borrow_deref_ref, + clippy::unnecessary_lazy_evaluations, + clippy::derive_partial_eq_without_eq +)] + +// The internal helper macros. +#[macro_use] +mod local_macros; + +pub use self::{ + bot::Bot, + errors::{ApiError, DownloadError, RequestError}, +}; + +pub mod adaptors; +pub mod errors; +pub mod net; +pub mod payloads; +pub mod prelude; +pub mod requests; +pub mod types; + +// reexported +mod bot; + +// implementation details +mod serde_multipart; + +#[cfg(test)] +mod codegen; diff --git a/crates/teloxide-core/src/local_macros.rs b/crates/teloxide-core/src/local_macros.rs new file mode 100644 index 00000000..de69f36d --- /dev/null +++ b/crates/teloxide-core/src/local_macros.rs @@ -0,0 +1,1339 @@ +macro_rules! req_future { + ( + $v2:vis def: | $( $arg:ident: $ArgTy:ty ),* $(,)? | $body:block + + $(#[$($meta:tt)*])* + $v:vis $i:ident<$T:ident> ($inner:ident) -> $Out:ty + $(where $($wh:tt)*)? + ) => { + #[pin_project::pin_project] + $v + struct $i<$T> + $(where $($wh)*)? + { + #[pin] + inner: $inner::$i<$T> + } + + impl<$T> $i<$T> + $(where $($wh)*)? + { + $v2 fn new($( $arg: $ArgTy ),*) -> Self { + Self { inner: $inner::def($( $arg ),*) } + } + } + + // HACK(waffle): workaround for https://github.com/rust-lang/rust/issues/55997 + mod $inner { + #![allow(type_alias_bounds)] + + // Mostly to bring `use`s + #[allow(unused_imports)] + use super::{*, $i as _}; + + #[cfg(feature = "nightly")] + pub(crate) type $i<$T> + $(where $($wh)*)? = impl ::core::future::Future; + + #[cfg(feature = "nightly")] + pub(crate) fn def<$T>($( $arg: $ArgTy ),*) -> $i<$T> + $(where $($wh)*)? + { + $body + } + + #[cfg(not(feature = "nightly"))] + pub(crate) type $i<$T> + $(where $($wh)*)? = ::core::pin::Pin + ::core::marker::Send + 'static>>; + + #[cfg(not(feature = "nightly"))] + pub(crate) fn def<$T>($( $arg: $ArgTy ),*) -> $i<$T> + $(where $($wh)*)? + { + Box::pin($body) + } + } + + impl<$T> ::core::future::Future for $i<$T> + $(where $($wh)*)? + { + type Output = $Out; + + fn poll(self: ::core::pin::Pin<&mut Self>, cx: &mut ::core::task::Context<'_>) -> ::core::task::Poll { + let this = self.project(); + this.inner.poll(cx) + } + } + + }; +} + +/// Declares an item with a doc attribute computed by some macro expression. +/// This allows documentation to be dynamically generated based on input. +/// Necessary to work around https://github.com/rust-lang/rust/issues/52607. +macro_rules! calculated_doc { + ( + $( + #[doc = $doc:expr] + $thing:item + )* + ) => ( + $( + #[doc = $doc] + $thing + )* + ); +} + +/// Declare payload type, implement `Payload` trait and ::new method for it, +/// declare setters trait and implement it for all type which have payload. +macro_rules! impl_payload { + ( + $( + @[multipart = $($multipart_attr:ident),*] + )? + $( + @[timeout_secs = $timeout_secs:ident] + )? + $( + #[ $($method_meta:tt)* ] + )* + $vi:vis $Method:ident ($Setters:ident) => $Ret:ty { + $( + required { + $( + $( + #[ $($field_meta:tt)* ] + )* + $v:vis $fields:ident : $FTy:ty $([$conv:ident])? + , + )* + } + )? + + $( + optional { + $( + $( + #[ $($opt_field_meta:tt)* ] + )* + $opt_v:vis $opt_fields:ident : $OptFTy:ty $([$opt_conv:ident])? + ),* + $(,)? + } + )? + } + ) => { + #[serde_with_macros::skip_serializing_none] + #[must_use = "Requests do nothing unless sent"] + $( + #[ $($method_meta)* ] + )* + $vi struct $Method { + $( + $( + $( + #[ $($field_meta)* ] + )* + $v $fields : $FTy, + )* + )? + $( + $( + $( + #[ $($opt_field_meta)* ] + )* + $opt_v $opt_fields : core::option::Option<$OptFTy>, + )* + )? + } + + impl $Method { + // We mirror Telegram API and can't do anything with too many arguments. + #[allow(clippy::too_many_arguments)] + // It's just easier for macros to generate such code. + #[allow(clippy::redundant_field_names)] + // It's obvious what this method does. (If you think it's not, feel free to open a PR) + #[allow(missing_docs)] + $vi fn new($($($fields : impl_payload!(@convert? $FTy $([$conv])?)),*)?) -> Self { + Self { + $( + $( + $fields: impl_payload!(@convert_map ($fields) $([$conv])?), + )* + )? + $( + $( + $opt_fields: None, + )* + )? + } + } + } + + impl $crate::requests::Payload for $Method { + type Output = $Ret; + + const NAME: &'static str = stringify!($Method); + + $( + fn timeout_hint(&self) -> Option { + self.$timeout_secs.map(<_>::into).map(std::time::Duration::from_secs) + } + )? + } + + calculated_doc! { + #[doc = concat!( + "Setters for fields of [`", + stringify!($Method), + "`]" + )] + $vi trait $Setters: $crate::requests::HasPayload + ::core::marker::Sized { + $( + $( + impl_payload! { @setter $Method $fields : $FTy $([$conv])? } + )* + )? + $( + $( + impl_payload! { @setter_opt $Method $opt_fields : $OptFTy $([$opt_conv])? } + )* + )? + } + } + + impl

$Setters for P where P: crate::requests::HasPayload {} + + impl_payload! { @[$(multipart = $($multipart_attr),*)?] $Method req { $($($fields),*)? } opt { $($($opt_fields),*)? } } + }; + (@setter_opt $Method:ident $field:ident : $FTy:ty [into]) => { + calculated_doc! { + #[doc = concat!( + "Setter for [`", + stringify!($field), + "`](", + stringify!($Method), + "::", + stringify!($field), + ") field." + )] + #[allow(clippy::wrong_self_convention)] + #[must_use = "Payloads and requests do nothing unless sent"] + fn $field(mut self, value: T) -> Self + where + T: Into<$FTy>, + { + self.payload_mut().$field = Some(value.into()); + self + } + } + }; + (@setter_opt $Method:ident $field:ident : $FTy:ty [collect]) => { + calculated_doc! { + #[doc = concat!( + "Setter for [`", + stringify!($field), + "`](", + stringify!($Method), + "::", + stringify!($field), + ") field." + )] + #[allow(clippy::wrong_self_convention)] + #[must_use = "Payloads and requests do nothing unless sent"] + fn $field(mut self, value: T) -> Self + where + T: ::core::iter::IntoIterator::Item>, + { + self.payload_mut().$field = Some(value.into_iter().collect()); + self + } + } + }; + (@setter_opt $Method:ident $field:ident : $FTy:ty) => { + calculated_doc! { + #[doc = concat!( + "Setter for [`", + stringify!($field), + "`](", + stringify!($Method), + "::", + stringify!($field), + ") field." + )] + #[allow(clippy::wrong_self_convention)] + #[must_use = "Payloads and requests do nothing unless sent"] + fn $field(mut self, value: $FTy) -> Self { + self.payload_mut().$field = Some(value); + self + } + } + }; + (@setter $Method:ident $field:ident : $FTy:ty [into]) => { + calculated_doc! { + #[doc = concat!( + "Setter for [`", + stringify!($field), + "`](", + stringify!($Method), + "::", + stringify!($field), + ") field." + )] + #[allow(clippy::wrong_self_convention)] + #[must_use = "Payloads and requests do nothing unless sent"] + fn $field(mut self, value: T) -> Self + where + T: Into<$FTy>, + { + self.payload_mut().$field = value.into(); + self + } + } + }; + (@setter $Method:ident $field:ident : $FTy:ty [collect]) => { + calculated_doc! { + #[doc = concat!( + "Setter for [`", + stringify!($field), + "`](", + stringify!($Method), + "::", + stringify!($field), + ") field." + )] + #[allow(clippy::wrong_self_convention)] + #[must_use = "Payloads and requests do nothing unless sent"] + fn $field(mut self, value: T) -> Self + where + T: ::core::iter::IntoIterator::Item>, + { + self.payload_mut().$field = value.into_iter().collect(); + self + } + } + }; + (@setter $Method:ident $field:ident : $FTy:ty) => { + calculated_doc! { + #[doc = concat!( + "Setter for [`", + stringify!($field), + "`](", + stringify!($Method), + "::", + stringify!($field), + ") field." + )] + #[allow(clippy::wrong_self_convention)] + #[must_use = "Payloads and requests do nothing unless sent"] + fn $field(mut self, value: $FTy) -> Self { + self.payload_mut().$field = value; + self + } + } + }; + (@convert? $T:ty [into]) => { + impl ::core::convert::Into<$T> + }; + (@convert? $T:ty [collect]) => { + impl ::core::iter::IntoIterator::Item> + }; + (@convert? $T:ty) => { + $T + }; + (@convert_map ($e:expr) [into]) => { + $e.into() + }; + (@convert_map ($e:expr) [collect]) => { + $e.into_iter().collect() + }; + (@convert_map ($e:expr)) => { + $e + }; + (@[multipart = $($multipart_attr:ident),*] $Method:ident req { $($reqf:ident),* } opt { $($optf:ident),*} ) => { + impl crate::requests::MultipartPayload for $Method { + fn copy_files(&self, into: &mut dyn FnMut(crate::types::InputFile)) { + $( + crate::types::InputFileLike::copy_into(&self.$multipart_attr, into); + )* + } + + fn move_files(&mut self, into: &mut dyn FnMut(crate::types::InputFile)) { + $( + crate::types::InputFileLike::move_into(&mut self.$multipart_attr, into); + )* + } + } + }; + (@[] $($ignored:tt)*) => {} +} + +macro_rules! download_forward { + ($l:lifetime $T:ident $S:ty {$this:ident => $inner:expr}) => { + impl<$l, $T: $crate::net::Download<$l>> $crate::net::Download<$l> for $S { + type Err = <$T as $crate::net::Download<$l>>::Err; + + type Fut = <$T as $crate::net::Download<$l>>::Fut; + + fn download_file( + &self, + path: &str, + destination: &'w mut (dyn tokio::io::AsyncWrite + + core::marker::Unpin + + core::marker::Send), + ) -> Self::Fut { + let $this = self; + ($inner).download_file(path, destination) + } + + type StreamErr = <$T as $crate::net::Download<$l>>::StreamErr; + + type Stream = <$T as $crate::net::Download<$l>>::Stream; + + fn download_file_stream(&self, path: &str) -> Self::Stream { + let $this = self; + ($inner).download_file_stream(path) + } + } + }; +} + +macro_rules! requester_forward { + ($i:ident $(, $rest:ident )* $(,)? => $body:ident, $ty:ident ) => { + requester_forward!(@method $i $body $ty); + $( + requester_forward!(@method $rest $body $ty); + )* + }; + +// START BLOCK requester_forward_at_method +// Generated by `codegen_requester_forward`, do not edit by hand. + + + (@method get_updates $body:ident $ty:ident) => { + type GetUpdates = $ty![GetUpdates]; + + fn get_updates(&self, ) -> Self::GetUpdates { + let this = self; + $body!(get_updates this ()) + } + }; + (@method set_webhook $body:ident $ty:ident) => { + type SetWebhook = $ty![SetWebhook]; + + fn set_webhook(&self, url: Url) -> Self::SetWebhook { + let this = self; + $body!(set_webhook this (url: Url)) + } + }; + (@method delete_webhook $body:ident $ty:ident) => { + type DeleteWebhook = $ty![DeleteWebhook]; + + fn delete_webhook(&self, ) -> Self::DeleteWebhook { + let this = self; + $body!(delete_webhook this ()) + } + }; + (@method get_webhook_info $body:ident $ty:ident) => { + type GetWebhookInfo = $ty![GetWebhookInfo]; + + fn get_webhook_info(&self, ) -> Self::GetWebhookInfo { + let this = self; + $body!(get_webhook_info this ()) + } + }; + (@method get_me $body:ident $ty:ident) => { + type GetMe = $ty![GetMe]; + + fn get_me(&self, ) -> Self::GetMe { + let this = self; + $body!(get_me this ()) + } + }; + (@method log_out $body:ident $ty:ident) => { + type LogOut = $ty![LogOut]; + + fn log_out(&self, ) -> Self::LogOut { + let this = self; + $body!(log_out this ()) + } + }; + (@method close $body:ident $ty:ident) => { + type Close = $ty![Close]; + + fn close(&self, ) -> Self::Close { + let this = self; + $body!(close this ()) + } + }; + (@method send_message $body:ident $ty:ident) => { + type SendMessage = $ty![SendMessage]; + + fn send_message(&self, chat_id: C, text: T) -> Self::SendMessage where C: Into, + T: Into { + let this = self; + $body!(send_message this (chat_id: C, text: T)) + } + }; + (@method forward_message $body:ident $ty:ident) => { + type ForwardMessage = $ty![ForwardMessage]; + + fn forward_message(&self, chat_id: C, from_chat_id: F, message_id: MessageId) -> Self::ForwardMessage where C: Into, + F: Into { + let this = self; + $body!(forward_message this (chat_id: C, from_chat_id: F, message_id: MessageId)) + } + }; + (@method copy_message $body:ident $ty:ident) => { + type CopyMessage = $ty![CopyMessage]; + + fn copy_message(&self, chat_id: C, from_chat_id: F, message_id: MessageId) -> Self::CopyMessage where C: Into, + F: Into { + let this = self; + $body!(copy_message this (chat_id: C, from_chat_id: F, message_id: MessageId)) + } + }; + (@method send_photo $body:ident $ty:ident) => { + type SendPhoto = $ty![SendPhoto]; + + fn send_photo(&self, chat_id: C, photo: InputFile) -> Self::SendPhoto where C: Into { + let this = self; + $body!(send_photo this (chat_id: C, photo: InputFile)) + } + }; + (@method send_audio $body:ident $ty:ident) => { + type SendAudio = $ty![SendAudio]; + + fn send_audio(&self, chat_id: C, audio: InputFile) -> Self::SendAudio where C: Into { + let this = self; + $body!(send_audio this (chat_id: C, audio: InputFile)) + } + }; + (@method send_document $body:ident $ty:ident) => { + type SendDocument = $ty![SendDocument]; + + fn send_document(&self, chat_id: C, document: InputFile) -> Self::SendDocument where C: Into { + let this = self; + $body!(send_document this (chat_id: C, document: InputFile)) + } + }; + (@method send_video $body:ident $ty:ident) => { + type SendVideo = $ty![SendVideo]; + + fn send_video(&self, chat_id: C, video: InputFile) -> Self::SendVideo where C: Into { + let this = self; + $body!(send_video this (chat_id: C, video: InputFile)) + } + }; + (@method send_animation $body:ident $ty:ident) => { + type SendAnimation = $ty![SendAnimation]; + + fn send_animation(&self, chat_id: C, animation: InputFile) -> Self::SendAnimation where C: Into { + let this = self; + $body!(send_animation this (chat_id: C, animation: InputFile)) + } + }; + (@method send_voice $body:ident $ty:ident) => { + type SendVoice = $ty![SendVoice]; + + fn send_voice(&self, chat_id: C, voice: InputFile) -> Self::SendVoice where C: Into { + let this = self; + $body!(send_voice this (chat_id: C, voice: InputFile)) + } + }; + (@method send_video_note $body:ident $ty:ident) => { + type SendVideoNote = $ty![SendVideoNote]; + + fn send_video_note(&self, chat_id: C, video_note: InputFile) -> Self::SendVideoNote where C: Into { + let this = self; + $body!(send_video_note this (chat_id: C, video_note: InputFile)) + } + }; + (@method send_media_group $body:ident $ty:ident) => { + type SendMediaGroup = $ty![SendMediaGroup]; + + fn send_media_group(&self, chat_id: C, media: M) -> Self::SendMediaGroup where C: Into, + M: IntoIterator { + let this = self; + $body!(send_media_group this (chat_id: C, media: M)) + } + }; + (@method send_location $body:ident $ty:ident) => { + type SendLocation = $ty![SendLocation]; + + fn send_location(&self, chat_id: C, latitude: f64, longitude: f64) -> Self::SendLocation where C: Into { + let this = self; + $body!(send_location this (chat_id: C, latitude: f64, longitude: f64)) + } + }; + (@method edit_message_live_location $body:ident $ty:ident) => { + type EditMessageLiveLocation = $ty![EditMessageLiveLocation]; + + fn edit_message_live_location(&self, chat_id: C, message_id: MessageId, latitude: f64, longitude: f64) -> Self::EditMessageLiveLocation where C: Into { + let this = self; + $body!(edit_message_live_location this (chat_id: C, message_id: MessageId, latitude: f64, longitude: f64)) + } + }; + (@method edit_message_live_location_inline $body:ident $ty:ident) => { + type EditMessageLiveLocationInline = $ty![EditMessageLiveLocationInline]; + + fn edit_message_live_location_inline(&self, inline_message_id: I, latitude: f64, longitude: f64) -> Self::EditMessageLiveLocationInline where I: Into { + let this = self; + $body!(edit_message_live_location_inline this (inline_message_id: I, latitude: f64, longitude: f64)) + } + }; + (@method stop_message_live_location $body:ident $ty:ident) => { + type StopMessageLiveLocation = $ty![StopMessageLiveLocation]; + + fn stop_message_live_location(&self, chat_id: C, message_id: MessageId, latitude: f64, longitude: f64) -> Self::StopMessageLiveLocation where C: Into { + let this = self; + $body!(stop_message_live_location this (chat_id: C, message_id: MessageId, latitude: f64, longitude: f64)) + } + }; + (@method stop_message_live_location_inline $body:ident $ty:ident) => { + type StopMessageLiveLocationInline = $ty![StopMessageLiveLocationInline]; + + fn stop_message_live_location_inline(&self, inline_message_id: I, latitude: f64, longitude: f64) -> Self::StopMessageLiveLocationInline where I: Into { + let this = self; + $body!(stop_message_live_location_inline this (inline_message_id: I, latitude: f64, longitude: f64)) + } + }; + (@method send_venue $body:ident $ty:ident) => { + type SendVenue = $ty![SendVenue]; + + fn send_venue(&self, chat_id: C, latitude: f64, longitude: f64, title: T, address: A) -> Self::SendVenue where C: Into, + T: Into, + A: Into { + let this = self; + $body!(send_venue this (chat_id: C, latitude: f64, longitude: f64, title: T, address: A)) + } + }; + (@method send_contact $body:ident $ty:ident) => { + type SendContact = $ty![SendContact]; + + fn send_contact(&self, chat_id: C, phone_number: P, first_name: F) -> Self::SendContact where C: Into, + P: Into, + F: Into { + let this = self; + $body!(send_contact this (chat_id: C, phone_number: P, first_name: F)) + } + }; + (@method send_poll $body:ident $ty:ident) => { + type SendPoll = $ty![SendPoll]; + + fn send_poll(&self, chat_id: C, question: Q, options: O) -> Self::SendPoll where C: Into, + Q: Into, + O: IntoIterator { + let this = self; + $body!(send_poll this (chat_id: C, question: Q, options: O)) + } + }; + (@method send_dice $body:ident $ty:ident) => { + type SendDice = $ty![SendDice]; + + fn send_dice(&self, chat_id: C) -> Self::SendDice where C: Into { + let this = self; + $body!(send_dice this (chat_id: C)) + } + }; + (@method send_chat_action $body:ident $ty:ident) => { + type SendChatAction = $ty![SendChatAction]; + + fn send_chat_action(&self, chat_id: C, action: ChatAction) -> Self::SendChatAction where C: Into { + let this = self; + $body!(send_chat_action this (chat_id: C, action: ChatAction)) + } + }; + (@method get_user_profile_photos $body:ident $ty:ident) => { + type GetUserProfilePhotos = $ty![GetUserProfilePhotos]; + + fn get_user_profile_photos(&self, user_id: UserId) -> Self::GetUserProfilePhotos { + let this = self; + $body!(get_user_profile_photos this (user_id: UserId)) + } + }; + (@method get_file $body:ident $ty:ident) => { + type GetFile = $ty![GetFile]; + + fn get_file(&self, file_id: F) -> Self::GetFile where F: Into { + let this = self; + $body!(get_file this (file_id: F)) + } + }; + (@method ban_chat_member $body:ident $ty:ident) => { + type BanChatMember = $ty![BanChatMember]; + + fn ban_chat_member(&self, chat_id: C, user_id: UserId) -> Self::BanChatMember where C: Into { + let this = self; + $body!(ban_chat_member this (chat_id: C, user_id: UserId)) + } + }; + (@method kick_chat_member $body:ident $ty:ident) => { + type KickChatMember = $ty![KickChatMember]; + + fn kick_chat_member(&self, chat_id: C, user_id: UserId) -> Self::KickChatMember where C: Into { + let this = self; + $body!(kick_chat_member this (chat_id: C, user_id: UserId)) + } + }; + (@method unban_chat_member $body:ident $ty:ident) => { + type UnbanChatMember = $ty![UnbanChatMember]; + + fn unban_chat_member(&self, chat_id: C, user_id: UserId) -> Self::UnbanChatMember where C: Into { + let this = self; + $body!(unban_chat_member this (chat_id: C, user_id: UserId)) + } + }; + (@method restrict_chat_member $body:ident $ty:ident) => { + type RestrictChatMember = $ty![RestrictChatMember]; + + fn restrict_chat_member(&self, chat_id: C, user_id: UserId, permissions: ChatPermissions) -> Self::RestrictChatMember where C: Into { + let this = self; + $body!(restrict_chat_member this (chat_id: C, user_id: UserId, permissions: ChatPermissions)) + } + }; + (@method promote_chat_member $body:ident $ty:ident) => { + type PromoteChatMember = $ty![PromoteChatMember]; + + fn promote_chat_member(&self, chat_id: C, user_id: UserId) -> Self::PromoteChatMember where C: Into { + let this = self; + $body!(promote_chat_member this (chat_id: C, user_id: UserId)) + } + }; + (@method set_chat_administrator_custom_title $body:ident $ty:ident) => { + type SetChatAdministratorCustomTitle = $ty![SetChatAdministratorCustomTitle]; + + fn set_chat_administrator_custom_title(&self, chat_id: Ch, user_id: UserId, custom_title: C) -> Self::SetChatAdministratorCustomTitle where Ch: Into, + C: Into { + let this = self; + $body!(set_chat_administrator_custom_title this (chat_id: Ch, user_id: UserId, custom_title: C)) + } + }; + (@method ban_chat_sender_chat $body:ident $ty:ident) => { + type BanChatSenderChat = $ty![BanChatSenderChat]; + + fn ban_chat_sender_chat(&self, chat_id: C, sender_chat_id: S) -> Self::BanChatSenderChat where C: Into, + S: Into { + let this = self; + $body!(ban_chat_sender_chat this (chat_id: C, sender_chat_id: S)) + } + }; + (@method unban_chat_sender_chat $body:ident $ty:ident) => { + type UnbanChatSenderChat = $ty![UnbanChatSenderChat]; + + fn unban_chat_sender_chat(&self, chat_id: C, sender_chat_id: S) -> Self::UnbanChatSenderChat where C: Into, + S: Into { + let this = self; + $body!(unban_chat_sender_chat this (chat_id: C, sender_chat_id: S)) + } + }; + (@method set_chat_permissions $body:ident $ty:ident) => { + type SetChatPermissions = $ty![SetChatPermissions]; + + fn set_chat_permissions(&self, chat_id: C, permissions: ChatPermissions) -> Self::SetChatPermissions where C: Into { + let this = self; + $body!(set_chat_permissions this (chat_id: C, permissions: ChatPermissions)) + } + }; + (@method export_chat_invite_link $body:ident $ty:ident) => { + type ExportChatInviteLink = $ty![ExportChatInviteLink]; + + fn export_chat_invite_link(&self, chat_id: C) -> Self::ExportChatInviteLink where C: Into { + let this = self; + $body!(export_chat_invite_link this (chat_id: C)) + } + }; + (@method create_chat_invite_link $body:ident $ty:ident) => { + type CreateChatInviteLink = $ty![CreateChatInviteLink]; + + fn create_chat_invite_link(&self, chat_id: C) -> Self::CreateChatInviteLink where C: Into { + let this = self; + $body!(create_chat_invite_link this (chat_id: C)) + } + }; + (@method edit_chat_invite_link $body:ident $ty:ident) => { + type EditChatInviteLink = $ty![EditChatInviteLink]; + + fn edit_chat_invite_link(&self, chat_id: C, invite_link: I) -> Self::EditChatInviteLink where C: Into, + I: Into { + let this = self; + $body!(edit_chat_invite_link this (chat_id: C, invite_link: I)) + } + }; + (@method revoke_chat_invite_link $body:ident $ty:ident) => { + type RevokeChatInviteLink = $ty![RevokeChatInviteLink]; + + fn revoke_chat_invite_link(&self, chat_id: C, invite_link: I) -> Self::RevokeChatInviteLink where C: Into, + I: Into { + let this = self; + $body!(revoke_chat_invite_link this (chat_id: C, invite_link: I)) + } + }; + (@method approve_chat_join_request $body:ident $ty:ident) => { + type ApproveChatJoinRequest = $ty![ApproveChatJoinRequest]; + + fn approve_chat_join_request(&self, chat_id: C, user_id: UserId) -> Self::ApproveChatJoinRequest where C: Into { + let this = self; + $body!(approve_chat_join_request this (chat_id: C, user_id: UserId)) + } + }; + (@method decline_chat_join_request $body:ident $ty:ident) => { + type DeclineChatJoinRequest = $ty![DeclineChatJoinRequest]; + + fn decline_chat_join_request(&self, chat_id: C, user_id: UserId) -> Self::DeclineChatJoinRequest where C: Into { + let this = self; + $body!(decline_chat_join_request this (chat_id: C, user_id: UserId)) + } + }; + (@method set_chat_photo $body:ident $ty:ident) => { + type SetChatPhoto = $ty![SetChatPhoto]; + + fn set_chat_photo(&self, chat_id: C, photo: InputFile) -> Self::SetChatPhoto where C: Into { + let this = self; + $body!(set_chat_photo this (chat_id: C, photo: InputFile)) + } + }; + (@method delete_chat_photo $body:ident $ty:ident) => { + type DeleteChatPhoto = $ty![DeleteChatPhoto]; + + fn delete_chat_photo(&self, chat_id: C) -> Self::DeleteChatPhoto where C: Into { + let this = self; + $body!(delete_chat_photo this (chat_id: C)) + } + }; + (@method set_chat_title $body:ident $ty:ident) => { + type SetChatTitle = $ty![SetChatTitle]; + + fn set_chat_title(&self, chat_id: C, title: T) -> Self::SetChatTitle where C: Into, + T: Into { + let this = self; + $body!(set_chat_title this (chat_id: C, title: T)) + } + }; + (@method set_chat_description $body:ident $ty:ident) => { + type SetChatDescription = $ty![SetChatDescription]; + + fn set_chat_description(&self, chat_id: C) -> Self::SetChatDescription where C: Into { + let this = self; + $body!(set_chat_description this (chat_id: C)) + } + }; + (@method pin_chat_message $body:ident $ty:ident) => { + type PinChatMessage = $ty![PinChatMessage]; + + fn pin_chat_message(&self, chat_id: C, message_id: MessageId) -> Self::PinChatMessage where C: Into { + let this = self; + $body!(pin_chat_message this (chat_id: C, message_id: MessageId)) + } + }; + (@method unpin_chat_message $body:ident $ty:ident) => { + type UnpinChatMessage = $ty![UnpinChatMessage]; + + fn unpin_chat_message(&self, chat_id: C) -> Self::UnpinChatMessage where C: Into { + let this = self; + $body!(unpin_chat_message this (chat_id: C)) + } + }; + (@method unpin_all_chat_messages $body:ident $ty:ident) => { + type UnpinAllChatMessages = $ty![UnpinAllChatMessages]; + + fn unpin_all_chat_messages(&self, chat_id: C) -> Self::UnpinAllChatMessages where C: Into { + let this = self; + $body!(unpin_all_chat_messages this (chat_id: C)) + } + }; + (@method leave_chat $body:ident $ty:ident) => { + type LeaveChat = $ty![LeaveChat]; + + fn leave_chat(&self, chat_id: C) -> Self::LeaveChat where C: Into { + let this = self; + $body!(leave_chat this (chat_id: C)) + } + }; + (@method get_chat $body:ident $ty:ident) => { + type GetChat = $ty![GetChat]; + + fn get_chat(&self, chat_id: C) -> Self::GetChat where C: Into { + let this = self; + $body!(get_chat this (chat_id: C)) + } + }; + (@method get_chat_administrators $body:ident $ty:ident) => { + type GetChatAdministrators = $ty![GetChatAdministrators]; + + fn get_chat_administrators(&self, chat_id: C) -> Self::GetChatAdministrators where C: Into { + let this = self; + $body!(get_chat_administrators this (chat_id: C)) + } + }; + (@method get_chat_member_count $body:ident $ty:ident) => { + type GetChatMemberCount = $ty![GetChatMemberCount]; + + fn get_chat_member_count(&self, chat_id: C) -> Self::GetChatMemberCount where C: Into { + let this = self; + $body!(get_chat_member_count this (chat_id: C)) + } + }; + (@method get_chat_members_count $body:ident $ty:ident) => { + type GetChatMembersCount = $ty![GetChatMembersCount]; + + fn get_chat_members_count(&self, chat_id: C) -> Self::GetChatMembersCount where C: Into { + let this = self; + $body!(get_chat_members_count this (chat_id: C)) + } + }; + (@method get_chat_member $body:ident $ty:ident) => { + type GetChatMember = $ty![GetChatMember]; + + fn get_chat_member(&self, chat_id: C, user_id: UserId) -> Self::GetChatMember where C: Into { + let this = self; + $body!(get_chat_member this (chat_id: C, user_id: UserId)) + } + }; + (@method set_chat_sticker_set $body:ident $ty:ident) => { + type SetChatStickerSet = $ty![SetChatStickerSet]; + + fn set_chat_sticker_set(&self, chat_id: C, sticker_set_name: S) -> Self::SetChatStickerSet where C: Into, + S: Into { + let this = self; + $body!(set_chat_sticker_set this (chat_id: C, sticker_set_name: S)) + } + }; + (@method delete_chat_sticker_set $body:ident $ty:ident) => { + type DeleteChatStickerSet = $ty![DeleteChatStickerSet]; + + fn delete_chat_sticker_set(&self, chat_id: C) -> Self::DeleteChatStickerSet where C: Into { + let this = self; + $body!(delete_chat_sticker_set this (chat_id: C)) + } + }; + (@method answer_callback_query $body:ident $ty:ident) => { + type AnswerCallbackQuery = $ty![AnswerCallbackQuery]; + + fn answer_callback_query(&self, callback_query_id: C) -> Self::AnswerCallbackQuery where C: Into { + let this = self; + $body!(answer_callback_query this (callback_query_id: C)) + } + }; + (@method set_my_commands $body:ident $ty:ident) => { + type SetMyCommands = $ty![SetMyCommands]; + + fn set_my_commands(&self, commands: C) -> Self::SetMyCommands where C: IntoIterator { + let this = self; + $body!(set_my_commands this (commands: C)) + } + }; + (@method get_my_commands $body:ident $ty:ident) => { + type GetMyCommands = $ty![GetMyCommands]; + + fn get_my_commands(&self, ) -> Self::GetMyCommands { + let this = self; + $body!(get_my_commands this ()) + } + }; + (@method set_chat_menu_button $body:ident $ty:ident) => { + type SetChatMenuButton = $ty![SetChatMenuButton]; + + fn set_chat_menu_button(&self, ) -> Self::SetChatMenuButton { + let this = self; + $body!(set_chat_menu_button this ()) + } + }; + (@method get_chat_menu_button $body:ident $ty:ident) => { + type GetChatMenuButton = $ty![GetChatMenuButton]; + + fn get_chat_menu_button(&self, ) -> Self::GetChatMenuButton { + let this = self; + $body!(get_chat_menu_button this ()) + } + }; + (@method set_my_default_administrator_rights $body:ident $ty:ident) => { + type SetMyDefaultAdministratorRights = $ty![SetMyDefaultAdministratorRights]; + + fn set_my_default_administrator_rights(&self, ) -> Self::SetMyDefaultAdministratorRights { + let this = self; + $body!(set_my_default_administrator_rights this ()) + } + }; + (@method get_my_default_administrator_rights $body:ident $ty:ident) => { + type GetMyDefaultAdministratorRights = $ty![GetMyDefaultAdministratorRights]; + + fn get_my_default_administrator_rights(&self, ) -> Self::GetMyDefaultAdministratorRights { + let this = self; + $body!(get_my_default_administrator_rights this ()) + } + }; + (@method delete_my_commands $body:ident $ty:ident) => { + type DeleteMyCommands = $ty![DeleteMyCommands]; + + fn delete_my_commands(&self, ) -> Self::DeleteMyCommands { + let this = self; + $body!(delete_my_commands this ()) + } + }; + (@method answer_inline_query $body:ident $ty:ident) => { + type AnswerInlineQuery = $ty![AnswerInlineQuery]; + + fn answer_inline_query(&self, inline_query_id: I, results: R) -> Self::AnswerInlineQuery where I: Into, + R: IntoIterator { + let this = self; + $body!(answer_inline_query this (inline_query_id: I, results: R)) + } + }; + (@method answer_web_app_query $body:ident $ty:ident) => { + type AnswerWebAppQuery = $ty![AnswerWebAppQuery]; + + fn answer_web_app_query(&self, web_app_query_id: W, result: InlineQueryResult) -> Self::AnswerWebAppQuery where W: Into { + let this = self; + $body!(answer_web_app_query this (web_app_query_id: W, result: InlineQueryResult)) + } + }; + (@method edit_message_text $body:ident $ty:ident) => { + type EditMessageText = $ty![EditMessageText]; + + fn edit_message_text(&self, chat_id: C, message_id: MessageId, text: T) -> Self::EditMessageText where C: Into, + T: Into { + let this = self; + $body!(edit_message_text this (chat_id: C, message_id: MessageId, text: T)) + } + }; + (@method edit_message_text_inline $body:ident $ty:ident) => { + type EditMessageTextInline = $ty![EditMessageTextInline]; + + fn edit_message_text_inline(&self, inline_message_id: I, text: T) -> Self::EditMessageTextInline where I: Into, + T: Into { + let this = self; + $body!(edit_message_text_inline this (inline_message_id: I, text: T)) + } + }; + (@method edit_message_caption $body:ident $ty:ident) => { + type EditMessageCaption = $ty![EditMessageCaption]; + + fn edit_message_caption(&self, chat_id: C, message_id: MessageId) -> Self::EditMessageCaption where C: Into { + let this = self; + $body!(edit_message_caption this (chat_id: C, message_id: MessageId)) + } + }; + (@method edit_message_caption_inline $body:ident $ty:ident) => { + type EditMessageCaptionInline = $ty![EditMessageCaptionInline]; + + fn edit_message_caption_inline(&self, inline_message_id: I) -> Self::EditMessageCaptionInline where I: Into { + let this = self; + $body!(edit_message_caption_inline this (inline_message_id: I)) + } + }; + (@method edit_message_media $body:ident $ty:ident) => { + type EditMessageMedia = $ty![EditMessageMedia]; + + fn edit_message_media(&self, chat_id: C, message_id: MessageId, media: InputMedia) -> Self::EditMessageMedia where C: Into { + let this = self; + $body!(edit_message_media this (chat_id: C, message_id: MessageId, media: InputMedia)) + } + }; + (@method edit_message_media_inline $body:ident $ty:ident) => { + type EditMessageMediaInline = $ty![EditMessageMediaInline]; + + fn edit_message_media_inline(&self, inline_message_id: I, media: InputMedia) -> Self::EditMessageMediaInline where I: Into { + let this = self; + $body!(edit_message_media_inline this (inline_message_id: I, media: InputMedia)) + } + }; + (@method edit_message_reply_markup $body:ident $ty:ident) => { + type EditMessageReplyMarkup = $ty![EditMessageReplyMarkup]; + + fn edit_message_reply_markup(&self, chat_id: C, message_id: MessageId) -> Self::EditMessageReplyMarkup where C: Into { + let this = self; + $body!(edit_message_reply_markup this (chat_id: C, message_id: MessageId)) + } + }; + (@method edit_message_reply_markup_inline $body:ident $ty:ident) => { + type EditMessageReplyMarkupInline = $ty![EditMessageReplyMarkupInline]; + + fn edit_message_reply_markup_inline(&self, inline_message_id: I) -> Self::EditMessageReplyMarkupInline where I: Into { + let this = self; + $body!(edit_message_reply_markup_inline this (inline_message_id: I)) + } + }; + (@method stop_poll $body:ident $ty:ident) => { + type StopPoll = $ty![StopPoll]; + + fn stop_poll(&self, chat_id: C, message_id: MessageId) -> Self::StopPoll where C: Into { + let this = self; + $body!(stop_poll this (chat_id: C, message_id: MessageId)) + } + }; + (@method delete_message $body:ident $ty:ident) => { + type DeleteMessage = $ty![DeleteMessage]; + + fn delete_message(&self, chat_id: C, message_id: MessageId) -> Self::DeleteMessage where C: Into { + let this = self; + $body!(delete_message this (chat_id: C, message_id: MessageId)) + } + }; + (@method send_sticker $body:ident $ty:ident) => { + type SendSticker = $ty![SendSticker]; + + fn send_sticker(&self, chat_id: C, sticker: InputFile) -> Self::SendSticker where C: Into { + let this = self; + $body!(send_sticker this (chat_id: C, sticker: InputFile)) + } + }; + (@method get_sticker_set $body:ident $ty:ident) => { + type GetStickerSet = $ty![GetStickerSet]; + + fn get_sticker_set(&self, name: N) -> Self::GetStickerSet where N: Into { + let this = self; + $body!(get_sticker_set this (name: N)) + } + }; + (@method get_custom_emoji_stickers $body:ident $ty:ident) => { + type GetCustomEmojiStickers = $ty![GetCustomEmojiStickers]; + + fn get_custom_emoji_stickers(&self, custom_emoji_ids: C) -> Self::GetCustomEmojiStickers where C: IntoIterator { + let this = self; + $body!(get_custom_emoji_stickers this (custom_emoji_ids: C)) + } + }; + (@method upload_sticker_file $body:ident $ty:ident) => { + type UploadStickerFile = $ty![UploadStickerFile]; + + fn upload_sticker_file(&self, user_id: UserId, png_sticker: InputFile) -> Self::UploadStickerFile { + let this = self; + $body!(upload_sticker_file this (user_id: UserId, png_sticker: InputFile)) + } + }; + (@method create_new_sticker_set $body:ident $ty:ident) => { + type CreateNewStickerSet = $ty![CreateNewStickerSet]; + + fn create_new_sticker_set(&self, user_id: UserId, name: N, title: T, sticker: InputSticker, emojis: E) -> Self::CreateNewStickerSet where N: Into, + T: Into, + E: Into { + let this = self; + $body!(create_new_sticker_set this (user_id: UserId, name: N, title: T, sticker: InputSticker, emojis: E)) + } + }; + (@method add_sticker_to_set $body:ident $ty:ident) => { + type AddStickerToSet = $ty![AddStickerToSet]; + + fn add_sticker_to_set(&self, user_id: UserId, name: N, sticker: InputSticker, emojis: E) -> Self::AddStickerToSet where N: Into, + E: Into { + let this = self; + $body!(add_sticker_to_set this (user_id: UserId, name: N, sticker: InputSticker, emojis: E)) + } + }; + (@method set_sticker_position_in_set $body:ident $ty:ident) => { + type SetStickerPositionInSet = $ty![SetStickerPositionInSet]; + + fn set_sticker_position_in_set(&self, sticker: S, position: u32) -> Self::SetStickerPositionInSet where S: Into { + let this = self; + $body!(set_sticker_position_in_set this (sticker: S, position: u32)) + } + }; + (@method delete_sticker_from_set $body:ident $ty:ident) => { + type DeleteStickerFromSet = $ty![DeleteStickerFromSet]; + + fn delete_sticker_from_set(&self, sticker: S) -> Self::DeleteStickerFromSet where S: Into { + let this = self; + $body!(delete_sticker_from_set this (sticker: S)) + } + }; + (@method set_sticker_set_thumb $body:ident $ty:ident) => { + type SetStickerSetThumb = $ty![SetStickerSetThumb]; + + fn set_sticker_set_thumb(&self, name: N, user_id: UserId) -> Self::SetStickerSetThumb where N: Into { + let this = self; + $body!(set_sticker_set_thumb this (name: N, user_id: UserId)) + } + }; + (@method send_invoice $body:ident $ty:ident) => { + type SendInvoice = $ty![SendInvoice]; + + fn send_invoice(&self, chat_id: Ch, title: T, description: D, payload: Pa, provider_token: P, currency: C, prices: Pri) -> Self::SendInvoice where Ch: Into, + T: Into, + D: Into, + Pa: Into, + P: Into, + C: Into, + Pri: IntoIterator { + let this = self; + $body!(send_invoice this (chat_id: Ch, title: T, description: D, payload: Pa, provider_token: P, currency: C, prices: Pri)) + } + }; + (@method create_invoice_link $body:ident $ty:ident) => { + type CreateInvoiceLink = $ty![CreateInvoiceLink]; + + fn create_invoice_link(&self, title: T, description: D, payload: Pa, provider_token: P, currency: C, prices: Pri) -> Self::CreateInvoiceLink where T: Into, + D: Into, + Pa: Into, + P: Into, + C: Into, + Pri: IntoIterator { + let this = self; + $body!(create_invoice_link this (title: T, description: D, payload: Pa, provider_token: P, currency: C, prices: Pri)) + } + }; + (@method answer_shipping_query $body:ident $ty:ident) => { + type AnswerShippingQuery = $ty![AnswerShippingQuery]; + + fn answer_shipping_query(&self, shipping_query_id: S, ok: bool) -> Self::AnswerShippingQuery where S: Into { + let this = self; + $body!(answer_shipping_query this (shipping_query_id: S, ok: bool)) + } + }; + (@method answer_pre_checkout_query $body:ident $ty:ident) => { + type AnswerPreCheckoutQuery = $ty![AnswerPreCheckoutQuery]; + + fn answer_pre_checkout_query

(&self, pre_checkout_query_id: P, ok: bool) -> Self::AnswerPreCheckoutQuery where P: Into { + let this = self; + $body!(answer_pre_checkout_query this (pre_checkout_query_id: P, ok: bool)) + } + }; + (@method set_passport_data_errors $body:ident $ty:ident) => { + type SetPassportDataErrors = $ty![SetPassportDataErrors]; + + fn set_passport_data_errors(&self, user_id: UserId, errors: E) -> Self::SetPassportDataErrors where E: IntoIterator { + let this = self; + $body!(set_passport_data_errors this (user_id: UserId, errors: E)) + } + }; + (@method send_game $body:ident $ty:ident) => { + type SendGame = $ty![SendGame]; + + fn send_game(&self, chat_id: u32, game_short_name: G) -> Self::SendGame where G: Into { + let this = self; + $body!(send_game this (chat_id: u32, game_short_name: G)) + } + }; + (@method set_game_score $body:ident $ty:ident) => { + type SetGameScore = $ty![SetGameScore]; + + fn set_game_score(&self, user_id: UserId, score: u64, chat_id: u32, message_id: MessageId) -> Self::SetGameScore { + let this = self; + $body!(set_game_score this (user_id: UserId, score: u64, chat_id: u32, message_id: MessageId)) + } + }; + (@method set_game_score_inline $body:ident $ty:ident) => { + type SetGameScoreInline = $ty![SetGameScoreInline]; + + fn set_game_score_inline(&self, user_id: UserId, score: u64, inline_message_id: I) -> Self::SetGameScoreInline where I: Into { + let this = self; + $body!(set_game_score_inline this (user_id: UserId, score: u64, inline_message_id: I)) + } + }; + (@method get_game_high_scores $body:ident $ty:ident) => { + type GetGameHighScores = $ty![GetGameHighScores]; + + fn get_game_high_scores(&self, user_id: UserId, target: T) -> Self::GetGameHighScores where T: Into { + let this = self; + $body!(get_game_high_scores this (user_id: UserId, target: T)) + } + };// END BLOCK requester_forward_at_method +} + +#[test] +fn codegen_requester_forward() { + use crate::codegen::{ + add_hidden_preamble, + convert::{convert_for, Convert}, + ensure_file_contents, min_prefix, project_root, reformat, replace_block, + schema::{self, Type}, + to_uppercase, + }; + use indexmap::IndexMap; + use itertools::Itertools; + + let path = project_root().join("src/local_macros.rs"); + let schema = schema::get(); + + let contents = schema + .methods + .iter() + .map(|m| { + let mut convert_params = m + .params + .iter() + .filter(|p| !matches!(p.ty, Type::Option(_))) + .map(|p| (&p.name, convert_for(&p.ty))) + .filter(|(_, c)| !matches!(c, Convert::Id(_))) + .map(|(name, _)| &**name) + .collect::>(); + + convert_params.sort_unstable(); + + let prefixes: IndexMap<_, _> = convert_params + .iter() + .copied() + // Workaround to output the last type as the first letter + .chain(["\0"]) + .tuple_windows() + .map(|(l, r)| (l, min_prefix(l, r))) + .collect(); + + let args = m + .params + .iter() + .filter(|p| !matches!(p.ty, Type::Option(_))) + .map(|p| match prefixes.get(&*p.name) { + Some(prefix) => format!("{}: {}", p.name, to_uppercase(prefix)), + None => format!("{}: {}", p.name, p.ty), + }) + .join(", "); + + let generics = m + .params + .iter() + .flat_map(|p| prefixes.get(&*p.name)) + .copied() + .map(to_uppercase) + .join(", "); + let where_clause = m + .params + .iter() + .filter(|p| !matches!(p.ty, Type::Option(_))) + .flat_map(|p| match convert_for(&p.ty) { + Convert::Id(_) => None, + Convert::Into(ty) => { + Some(format!("{}: Into<{}>", &to_uppercase(prefixes[&*p.name]), ty)) + } + Convert::Collect(ty) => Some(format!( + "{}: IntoIterator", + &to_uppercase(prefixes[&*p.name]), + ty + )), + }) + .join(",\n "); + + let generics = + if generics.is_empty() { String::from("") } else { format!("<{}>", generics) }; + + let where_clause = if where_clause.is_empty() { + String::from("") + } else { + format!(" where {}", where_clause) + }; + + format!( + " + (@method {method} $body:ident $ty:ident) => {{ + type {Method} = $ty![{Method}]; + + fn {method}{generics}(&self, {args}) -> Self::{Method}{where_clause} {{ + let this = self; + $body!({method} this ({args})) + }} + }};", + Method = m.names.1, + method = m.names.2, + ) + }) + .collect(); + + let contents = reformat(replace_block( + &path, + "requester_forward_at_method", + &add_hidden_preamble("codegen_requester_forward", contents), + )); + + ensure_file_contents(&path, &contents); +} diff --git a/crates/teloxide-core/src/net.rs b/crates/teloxide-core/src/net.rs new file mode 100644 index 00000000..d7f96a76 --- /dev/null +++ b/crates/teloxide-core/src/net.rs @@ -0,0 +1,126 @@ +//! Network-specific API. + +use std::time::Duration; + +pub use self::download::{download_file, download_file_stream, Download}; + +pub(crate) use self::{ + request::{request_json, request_multipart}, + telegram_response::TelegramResponse, +}; + +mod download; +mod request; +mod telegram_response; + +/// The default Telegram API URL. +pub const TELEGRAM_API_URL: &str = "https://api.telegram.org"; + +/// Constructs a network client from the `TELOXIDE_PROXY` environmental +/// variable. +/// +/// This function passes the value of `TELOXIDE_PROXY` into +/// [`reqwest::Proxy::all`], if it exists, otherwise returns the default +/// client. +/// +/// ## Note +/// +/// The created client will have safe settings, meaning that it will be able to +/// work in long time durations, see the [issue 223]. +/// +/// [`reqwest::Proxy::all`]: https://docs.rs/reqwest/latest/reqwest/struct.Proxy.html#method.all +/// [issue 223]: https://github.com/teloxide/teloxide/issues/223 +/// +/// ## Panics +/// +/// If `TELOXIDE_PROXY` exists, but isn't correct url. +#[must_use] +pub fn client_from_env() -> reqwest::Client { + use reqwest::Proxy; + + const TELOXIDE_PROXY: &str = "TELOXIDE_PROXY"; + + let builder = default_reqwest_settings(); + + match std::env::var(TELOXIDE_PROXY).ok() { + Some(proxy) => builder.proxy(Proxy::all(&proxy).expect("reqwest::Proxy creation failed")), + None => builder, + } + .build() + .expect("creating reqwest::Client") +} + +/// Returns a reqwest client builder with default settings. +/// +/// Client built from default settings is supposed to work over long time +/// durations, see the [issue 223]. +/// +/// The current settings are: +/// - A connection timeout of 5 seconds. +/// - A timeout of 17 seconds. +/// - `tcp_nodelay` is on. +/// +/// ## Notes +/// +/// 1. The settings may change in the future. +/// 2. If you are using the polling mechanism to get updates, the timeout +/// configured in the client should be bigger than the polling timeout. +/// 3. If you alter the current settings listed above, your bot will not be +/// guaranteed to work over long time durations. +/// +/// [issue 223]: https://github.com/teloxide/teloxide/issues/223 +pub fn default_reqwest_settings() -> reqwest::ClientBuilder { + reqwest::Client::builder() + .connect_timeout(Duration::from_secs(5)) + .timeout(Duration::from_secs(17)) + .tcp_nodelay(true) +} + +/// Creates URL for making HTTPS requests. See the [Telegram documentation]. +/// +/// [Telegram documentation]: https://core.telegram.org/bots/api#making-requests +fn method_url(base: reqwest::Url, token: &str, method_name: &str) -> reqwest::Url { + base.join(&format!("/bot{token}/{method}", token = token, method = method_name)) + .expect("failed to format url") +} + +/// Creates URL for downloading a file. See the [Telegram documentation]. +/// +/// [Telegram documentation]: https://core.telegram.org/bots/api#file +fn file_url(base: reqwest::Url, token: &str, file_path: &str) -> reqwest::Url { + base.join(&format!("file/bot{token}/{file}", token = token, file = file_path)) + .expect("failed to format url") +} + +#[cfg(test)] +mod tests { + use crate::net::*; + + #[test] + fn method_url_test() { + let url = method_url( + reqwest::Url::parse(TELEGRAM_API_URL).unwrap(), + "535362388:AAF7-g0gYncWnm5IyfZlpPRqRRv6kNAGlao", + "methodName", + ); + + assert_eq!( + url.as_str(), + "https://api.telegram.org/bot535362388:AAF7-g0gYncWnm5IyfZlpPRqRRv6kNAGlao/methodName" + ); + } + + #[test] + fn file_url_test() { + let url = file_url( + reqwest::Url::parse(TELEGRAM_API_URL).unwrap(), + "535362388:AAF7-g0gYncWnm5IyfZlpPRqRRv6kNAGlao", + "AgADAgADyqoxG2g8aEsu_KjjVsGF4-zetw8ABAEAAwIAA20AA_8QAwABFgQ", + ); + + assert_eq!( + url.as_str(), + "https://api.telegram.org/file/bot535362388:AAF7-g0gYncWnm5IyfZlpPRqRRv6kNAGlao/AgADAgADyqoxG2g8aEsu_KjjVsGF4-zetw8ABAEAAwIAA20AA_8QAwABFgQ" + ); + } +} diff --git a/crates/teloxide-core/src/net/download.rs b/crates/teloxide-core/src/net/download.rs new file mode 100644 index 00000000..ac419a79 --- /dev/null +++ b/crates/teloxide-core/src/net/download.rs @@ -0,0 +1,131 @@ +use std::future::Future; + +use bytes::Bytes; +use futures::{ + future::{ready, Either}, + stream::{once, unfold}, + FutureExt, Stream, StreamExt, +}; +use reqwest::{Client, Response, Url}; +use tokio::io::{AsyncWrite, AsyncWriteExt}; + +use crate::{errors::DownloadError, net::file_url}; + +/// A trait for downloading files from Telegram. +pub trait Download<'w> +/* FIXME(waffle): ideally, this lifetime ('w) shouldn't be here, but we can't help it without + * GATs */ +{ + /// An error returned from [`download_file`](Self::download_file). + type Err; + + /// A future returned from [`download_file`](Self::download_file). + type Fut: Future> + Send; + + /// Download a file from Telegram into `destination`. + /// + /// `path` can be obtained from [`GetFile`]. + /// + /// To download as a stream of chunks, see [`download_file_stream`]. + /// + /// ## Examples + /// + /// ```no_run + /// use teloxide_core::{ + /// net::Download, + /// requests::{Request, Requester}, + /// types::File, + /// Bot, + /// }; + /// use tokio::fs; + /// + /// # async fn run() -> Result<(), Box> { + /// let bot = Bot::new("TOKEN"); + /// + /// let file = bot.get_file("*file_id*").await?; + /// let mut dst = fs::File::create("/tmp/test.png").await?; + /// bot.download_file(&file.path, &mut dst).await?; + /// # Ok(()) } + /// ``` + /// + /// [`GetFile`]: crate::payloads::GetFile + /// [`download_file_stream`]: Self::download_file_stream + fn download_file( + &self, + path: &str, + destination: &'w mut (dyn AsyncWrite + Unpin + Send), + ) -> Self::Fut; + + /// An error returned from + /// [`download_file_stream`](Self::download_file_stream). + type StreamErr; + + /// A stream returned from [`download_file_stream`]. + /// + ///[`download_file_stream`]: (Self::download_file_stream) + type Stream: Stream> + Send; + + /// Download a file from Telegram as [`Stream`]. + /// + /// `path` can be obtained from the [`GetFile`]. + /// + /// To download into an [`AsyncWrite`] (e.g. [`tokio::fs::File`]), see + /// [`download_file`]. + /// + /// [`GetFile`]: crate::payloads::GetFile + /// [`AsyncWrite`]: tokio::io::AsyncWrite + /// [`tokio::fs::File`]: tokio::fs::File + /// [`download_file`]: Self::download_file + fn download_file_stream(&self, path: &str) -> Self::Stream; +} + +/// Download a file from Telegram into `dst`. +/// +/// Note: if you don't need to use a different (from you're bot) client and +/// don't need to get *all* performance (and you don't, c'mon it's very io-bound +/// job), then it's recommended to use [`Download::download_file`]. +pub fn download_file<'o, D>( + client: &Client, + api_url: Url, + token: &str, + path: &str, + dst: &'o mut D, +) -> impl Future> + 'o +where + D: ?Sized + AsyncWrite + Unpin, +{ + client.get(file_url(api_url, token, path)).send().then(move |r| async move { + let mut res = r?.error_for_status()?; + + while let Some(chunk) = res.chunk().await? { + dst.write_all(&chunk).await?; + } + + Ok(()) + }) +} + +/// Download a file from Telegram as [`Stream`]. +/// +/// Note: if you don't need to use a different (from you're bot) client and +/// don't need to get *all* performance (and you don't, c'mon it's very io-bound +/// job), then it's recommended to use [`Download::download_file_stream`]. +pub fn download_file_stream( + client: &Client, + api_url: Url, + token: &str, + path: &str, +) -> impl Stream> + 'static { + client.get(file_url(api_url, token, path)).send().into_stream().flat_map(|res| { + match res.and_then(Response::error_for_status) { + Ok(res) => Either::Left(unfold(res, |mut res| async { + match res.chunk().await { + Err(err) => Some((Err(err), res)), + Ok(Some(c)) => Some((Ok(c), res)), + Ok(None) => None, + } + })), + Err(err) => Either::Right(once(ready(Err(err)))), + } + }) +} diff --git a/crates/teloxide-core/src/net/request.rs b/crates/teloxide-core/src/net/request.rs new file mode 100644 index 00000000..7fa345ec --- /dev/null +++ b/crates/teloxide-core/src/net/request.rs @@ -0,0 +1,105 @@ +use std::time::Duration; + +use reqwest::{ + header::{HeaderValue, CONTENT_TYPE}, + Client, Response, +}; +use serde::de::DeserializeOwned; + +use crate::{net::TelegramResponse, requests::ResponseResult, RequestError}; + +const DELAY_ON_SERVER_ERROR: Duration = Duration::from_secs(10); + +pub async fn request_multipart( + client: &Client, + token: &str, + api_url: reqwest::Url, + method_name: &str, + params: reqwest::multipart::Form, + _timeout_hint: Option, +) -> ResponseResult +where + T: DeserializeOwned, +{ + // Workaround for [#460] + // + // Telegram has some methods that return either `Message` or `True` depending on + // the used arguments we model this as `...` and `..._inline` pairs of methods. + // + // Currently inline versions have wrong Payload::NAME (ie with the "Inline" + // suffix). This removes the suffix allowing to call the right telegram method. + // Note that currently there are no normal telegram methods ending in "Inline", + // so this is fine. + // + // [#460]: https://github.com/teloxide/teloxide/issues/460 + let method_name = method_name.trim_end_matches("Inline"); + + let request = client + .post(crate::net::method_url(api_url, token, method_name)) + .multipart(params) + .build()?; + + // FIXME: uncomment this, when reqwest starts setting default timeout early + // if let Some(timeout) = timeout_hint { + // *request.timeout_mut().get_or_insert(Duration::ZERO) += timeout; + // } + + let response = client.execute(request).await?; + + process_response(response).await +} + +pub async fn request_json( + client: &Client, + token: &str, + api_url: reqwest::Url, + method_name: &str, + params: Vec, + _timeout_hint: Option, +) -> ResponseResult +where + T: DeserializeOwned, +{ + // Workaround for [#460] + // + // Telegram has some methods that return either `Message` or `True` depending on + // the used arguments we model this as `...` and `..._inline` pairs of methods. + // + // Currently inline versions have wrong Payload::NAME (ie with the "Inline" + // suffix). This removes the suffix allowing to call the right telegram method. + // Note that currently there are no normal telegram methods ending in "Inline", + // so this is fine. + // + // [#460]: https://github.com/teloxide/teloxide/issues/460 + let method_name = method_name.trim_end_matches("Inline"); + + let request = client + .post(crate::net::method_url(api_url, token, method_name)) + .header(CONTENT_TYPE, HeaderValue::from_static("application/json")) + .body(params) + .build()?; + + // FIXME: uncomment this, when reqwest starts setting default timeout early + // if let Some(timeout) = timeout_hint { + // *request.timeout_mut().get_or_insert(Duration::ZERO) += timeout; + // } + + let response = client.execute(request).await?; + + process_response(response).await +} + +async fn process_response(response: Response) -> ResponseResult +where + T: DeserializeOwned, +{ + if response.status().is_server_error() { + tokio::time::sleep(DELAY_ON_SERVER_ERROR).await; + } + + let text = response.text().await?; + + serde_json::from_str::>(&text) + .map_err(|source| RequestError::InvalidJson { source, raw: text.into() })? + .into() +} diff --git a/crates/teloxide-core/src/net/telegram_response.rs b/crates/teloxide-core/src/net/telegram_response.rs new file mode 100644 index 00000000..4dfa5ba1 --- /dev/null +++ b/crates/teloxide-core/src/net/telegram_response.rs @@ -0,0 +1,74 @@ +use serde::Deserialize; + +use crate::{ + requests::ResponseResult, + types::{False, ResponseParameters, True}, + ApiError, RequestError, +}; + +#[derive(Deserialize)] +#[serde(untagged)] +pub(crate) enum TelegramResponse { + Ok { + /// A dummy field. Used only for deserialization. + #[allow(dead_code)] + ok: True, + + #[serde(rename = "result")] + response: R, + }, + Err { + /// A dummy field. Used only for deserialization. + #[allow(dead_code)] + ok: False, + + #[serde(rename = "description")] + error: ApiError, + + // // This field is present in the json sent by telegram, but isn't currently used anywhere + // // and as such - ignored + // error_code: u16, + #[serde(rename = "parameters")] + response_parameters: Option, + }, +} + +impl From> for ResponseResult { + fn from(this: TelegramResponse) -> ResponseResult { + match this { + TelegramResponse::Ok { response, .. } => Ok(response), + TelegramResponse::Err { response_parameters: Some(params), .. } => Err(match params { + ResponseParameters::RetryAfter(i) => RequestError::RetryAfter(i), + ResponseParameters::MigrateToChatId(to) => RequestError::MigrateToChatId(to), + }), + TelegramResponse::Err { error, .. } => Err(RequestError::Api(error)), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{errors::ApiError, types::Update}; + + #[test] + fn parse_terminated_by_other_get_updates() { + let s = r#"{"ok":false,"error_code":409,"description":"Conflict: terminated by other getUpdates request; make sure that only one bot instance is running"}"#; + let val = serde_json::from_str::>(s).unwrap(); + + assert!(matches!( + val, + TelegramResponse::Err { error: ApiError::TerminatedByOtherGetUpdates, .. } + )); + } + + #[test] + fn parse_unknown() { + let s = r#"{"ok":false,"error_code":111,"description":"Unknown description that won't match anything"}"#; + let val = serde_json::from_str::>(s).unwrap(); + + assert!( + matches!(val, TelegramResponse::Err { error: ApiError::Unknown(s), .. } if s == "Unknown description that won't match anything") + ); + } +} diff --git a/crates/teloxide-core/src/payloads.rs b/crates/teloxide-core/src/payloads.rs new file mode 100644 index 00000000..ee37f29c --- /dev/null +++ b/crates/teloxide-core/src/payloads.rs @@ -0,0 +1,284 @@ +//! Request data sent to Telegram. + +/// This module re-exports all the setters traits as `_`. +/// +/// When used with a glob import: +/// +/// ``` +/// use teloxide_core::payloads::setters::*; +/// ``` +/// +/// It allows you to use all the payloads setters, without polluting your +/// namespace. +pub mod setters; + +// START BLOCK payload_modules +// Generated by `codegen_payload_mods_and_reexports`, do not edit by hand. + +mod add_sticker_to_set; +mod answer_callback_query; +mod answer_inline_query; +mod answer_pre_checkout_query; +mod answer_shipping_query; +mod answer_web_app_query; +mod approve_chat_join_request; +mod ban_chat_member; +mod ban_chat_sender_chat; +mod close; +mod copy_message; +mod create_chat_invite_link; +mod create_invoice_link; +mod create_new_sticker_set; +mod decline_chat_join_request; +mod delete_chat_photo; +mod delete_chat_sticker_set; +mod delete_message; +mod delete_my_commands; +mod delete_sticker_from_set; +mod delete_webhook; +mod edit_chat_invite_link; +mod edit_message_caption; +mod edit_message_caption_inline; +mod edit_message_live_location; +mod edit_message_live_location_inline; +mod edit_message_media; +mod edit_message_media_inline; +mod edit_message_reply_markup; +mod edit_message_reply_markup_inline; +mod edit_message_text; +mod edit_message_text_inline; +mod export_chat_invite_link; +mod forward_message; +mod get_chat; +mod get_chat_administrators; +mod get_chat_member; +mod get_chat_member_count; +mod get_chat_members_count; +mod get_chat_menu_button; +mod get_custom_emoji_stickers; +mod get_file; +mod get_game_high_scores; +mod get_me; +mod get_my_commands; +mod get_my_default_administrator_rights; +mod get_sticker_set; +mod get_updates; +mod get_user_profile_photos; +mod get_webhook_info; +mod kick_chat_member; +mod leave_chat; +mod log_out; +mod pin_chat_message; +mod promote_chat_member; +mod restrict_chat_member; +mod revoke_chat_invite_link; +mod send_animation; +mod send_audio; +mod send_chat_action; +mod send_contact; +mod send_dice; +mod send_document; +mod send_game; +mod send_invoice; +mod send_location; +mod send_media_group; +mod send_message; +mod send_photo; +mod send_poll; +mod send_sticker; +mod send_venue; +mod send_video; +mod send_video_note; +mod send_voice; +mod set_chat_administrator_custom_title; +mod set_chat_description; +mod set_chat_menu_button; +mod set_chat_permissions; +mod set_chat_photo; +mod set_chat_sticker_set; +mod set_chat_title; +mod set_game_score; +mod set_game_score_inline; +mod set_my_commands; +mod set_my_default_administrator_rights; +mod set_passport_data_errors; +mod set_sticker_position_in_set; +mod set_sticker_set_thumb; +mod set_webhook; +mod stop_message_live_location; +mod stop_message_live_location_inline; +mod stop_poll; +mod unban_chat_member; +mod unban_chat_sender_chat; +mod unpin_all_chat_messages; +mod unpin_chat_message; +mod upload_sticker_file; + +pub use add_sticker_to_set::{AddStickerToSet, AddStickerToSetSetters}; +pub use answer_callback_query::{AnswerCallbackQuery, AnswerCallbackQuerySetters}; +pub use answer_inline_query::{AnswerInlineQuery, AnswerInlineQuerySetters}; +pub use answer_pre_checkout_query::{AnswerPreCheckoutQuery, AnswerPreCheckoutQuerySetters}; +pub use answer_shipping_query::{AnswerShippingQuery, AnswerShippingQuerySetters}; +pub use answer_web_app_query::{AnswerWebAppQuery, AnswerWebAppQuerySetters}; +pub use approve_chat_join_request::{ApproveChatJoinRequest, ApproveChatJoinRequestSetters}; +pub use ban_chat_member::{BanChatMember, BanChatMemberSetters}; +pub use ban_chat_sender_chat::{BanChatSenderChat, BanChatSenderChatSetters}; +pub use close::{Close, CloseSetters}; +pub use copy_message::{CopyMessage, CopyMessageSetters}; +pub use create_chat_invite_link::{CreateChatInviteLink, CreateChatInviteLinkSetters}; +pub use create_invoice_link::{CreateInvoiceLink, CreateInvoiceLinkSetters}; +pub use create_new_sticker_set::{CreateNewStickerSet, CreateNewStickerSetSetters}; +pub use decline_chat_join_request::{DeclineChatJoinRequest, DeclineChatJoinRequestSetters}; +pub use delete_chat_photo::{DeleteChatPhoto, DeleteChatPhotoSetters}; +pub use delete_chat_sticker_set::{DeleteChatStickerSet, DeleteChatStickerSetSetters}; +pub use delete_message::{DeleteMessage, DeleteMessageSetters}; +pub use delete_my_commands::{DeleteMyCommands, DeleteMyCommandsSetters}; +pub use delete_sticker_from_set::{DeleteStickerFromSet, DeleteStickerFromSetSetters}; +pub use delete_webhook::{DeleteWebhook, DeleteWebhookSetters}; +pub use edit_chat_invite_link::{EditChatInviteLink, EditChatInviteLinkSetters}; +pub use edit_message_caption::{EditMessageCaption, EditMessageCaptionSetters}; +pub use edit_message_caption_inline::{EditMessageCaptionInline, EditMessageCaptionInlineSetters}; +pub use edit_message_live_location::{EditMessageLiveLocation, EditMessageLiveLocationSetters}; +pub use edit_message_live_location_inline::{ + EditMessageLiveLocationInline, EditMessageLiveLocationInlineSetters, +}; +pub use edit_message_media::{EditMessageMedia, EditMessageMediaSetters}; +pub use edit_message_media_inline::{EditMessageMediaInline, EditMessageMediaInlineSetters}; +pub use edit_message_reply_markup::{EditMessageReplyMarkup, EditMessageReplyMarkupSetters}; +pub use edit_message_reply_markup_inline::{ + EditMessageReplyMarkupInline, EditMessageReplyMarkupInlineSetters, +}; +pub use edit_message_text::{EditMessageText, EditMessageTextSetters}; +pub use edit_message_text_inline::{EditMessageTextInline, EditMessageTextInlineSetters}; +pub use export_chat_invite_link::{ExportChatInviteLink, ExportChatInviteLinkSetters}; +pub use forward_message::{ForwardMessage, ForwardMessageSetters}; +pub use get_chat::{GetChat, GetChatSetters}; +pub use get_chat_administrators::{GetChatAdministrators, GetChatAdministratorsSetters}; +pub use get_chat_member::{GetChatMember, GetChatMemberSetters}; +pub use get_chat_member_count::{GetChatMemberCount, GetChatMemberCountSetters}; +pub use get_chat_members_count::{GetChatMembersCount, GetChatMembersCountSetters}; +pub use get_chat_menu_button::{GetChatMenuButton, GetChatMenuButtonSetters}; +pub use get_custom_emoji_stickers::{GetCustomEmojiStickers, GetCustomEmojiStickersSetters}; +pub use get_file::{GetFile, GetFileSetters}; +pub use get_game_high_scores::{GetGameHighScores, GetGameHighScoresSetters}; +pub use get_me::{GetMe, GetMeSetters}; +pub use get_my_commands::{GetMyCommands, GetMyCommandsSetters}; +pub use get_my_default_administrator_rights::{ + GetMyDefaultAdministratorRights, GetMyDefaultAdministratorRightsSetters, +}; +pub use get_sticker_set::{GetStickerSet, GetStickerSetSetters}; +pub use get_updates::{GetUpdates, GetUpdatesSetters}; +pub use get_user_profile_photos::{GetUserProfilePhotos, GetUserProfilePhotosSetters}; +pub use get_webhook_info::{GetWebhookInfo, GetWebhookInfoSetters}; +pub use kick_chat_member::{KickChatMember, KickChatMemberSetters}; +pub use leave_chat::{LeaveChat, LeaveChatSetters}; +pub use log_out::{LogOut, LogOutSetters}; +pub use pin_chat_message::{PinChatMessage, PinChatMessageSetters}; +pub use promote_chat_member::{PromoteChatMember, PromoteChatMemberSetters}; +pub use restrict_chat_member::{RestrictChatMember, RestrictChatMemberSetters}; +pub use revoke_chat_invite_link::{RevokeChatInviteLink, RevokeChatInviteLinkSetters}; +pub use send_animation::{SendAnimation, SendAnimationSetters}; +pub use send_audio::{SendAudio, SendAudioSetters}; +pub use send_chat_action::{SendChatAction, SendChatActionSetters}; +pub use send_contact::{SendContact, SendContactSetters}; +pub use send_dice::{SendDice, SendDiceSetters}; +pub use send_document::{SendDocument, SendDocumentSetters}; +pub use send_game::{SendGame, SendGameSetters}; +pub use send_invoice::{SendInvoice, SendInvoiceSetters}; +pub use send_location::{SendLocation, SendLocationSetters}; +pub use send_media_group::{SendMediaGroup, SendMediaGroupSetters}; +pub use send_message::{SendMessage, SendMessageSetters}; +pub use send_photo::{SendPhoto, SendPhotoSetters}; +pub use send_poll::{SendPoll, SendPollSetters}; +pub use send_sticker::{SendSticker, SendStickerSetters}; +pub use send_venue::{SendVenue, SendVenueSetters}; +pub use send_video::{SendVideo, SendVideoSetters}; +pub use send_video_note::{SendVideoNote, SendVideoNoteSetters}; +pub use send_voice::{SendVoice, SendVoiceSetters}; +pub use set_chat_administrator_custom_title::{ + SetChatAdministratorCustomTitle, SetChatAdministratorCustomTitleSetters, +}; +pub use set_chat_description::{SetChatDescription, SetChatDescriptionSetters}; +pub use set_chat_menu_button::{SetChatMenuButton, SetChatMenuButtonSetters}; +pub use set_chat_permissions::{SetChatPermissions, SetChatPermissionsSetters}; +pub use set_chat_photo::{SetChatPhoto, SetChatPhotoSetters}; +pub use set_chat_sticker_set::{SetChatStickerSet, SetChatStickerSetSetters}; +pub use set_chat_title::{SetChatTitle, SetChatTitleSetters}; +pub use set_game_score::{SetGameScore, SetGameScoreSetters}; +pub use set_game_score_inline::{SetGameScoreInline, SetGameScoreInlineSetters}; +pub use set_my_commands::{SetMyCommands, SetMyCommandsSetters}; +pub use set_my_default_administrator_rights::{ + SetMyDefaultAdministratorRights, SetMyDefaultAdministratorRightsSetters, +}; +pub use set_passport_data_errors::{SetPassportDataErrors, SetPassportDataErrorsSetters}; +pub use set_sticker_position_in_set::{SetStickerPositionInSet, SetStickerPositionInSetSetters}; +pub use set_sticker_set_thumb::{SetStickerSetThumb, SetStickerSetThumbSetters}; +pub use set_webhook::{SetWebhook, SetWebhookSetters}; +pub use stop_message_live_location::{StopMessageLiveLocation, StopMessageLiveLocationSetters}; +pub use stop_message_live_location_inline::{ + StopMessageLiveLocationInline, StopMessageLiveLocationInlineSetters, +}; +pub use stop_poll::{StopPoll, StopPollSetters}; +pub use unban_chat_member::{UnbanChatMember, UnbanChatMemberSetters}; +pub use unban_chat_sender_chat::{UnbanChatSenderChat, UnbanChatSenderChatSetters}; +pub use unpin_all_chat_messages::{UnpinAllChatMessages, UnpinAllChatMessagesSetters}; +pub use unpin_chat_message::{UnpinChatMessage, UnpinChatMessageSetters}; +pub use upload_sticker_file::{UploadStickerFile, UploadStickerFileSetters}; +// END BLOCK payload_modules + +/// Generates `mod`s and `pub use`s above. +#[test] +fn codegen_payload_mods_and_reexports() { + use crate::codegen::{ + add_hidden_preamble, ensure_file_contents, project_root, reformat, replace_block, schema, + }; + + let path = project_root().join("src/payloads.rs"); + let schema = schema::get(); + let mut block = String::new(); + + schema.methods.iter().for_each(|m| block.push_str(&format!("mod {};\n", m.names.2))); + + block.push('\n'); + + schema.methods.iter().for_each(|m| { + block.push_str(&format!( + "pub use {m}::{{{M}, {M}Setters}};\n", + m = m.names.2, + M = m.names.1 + )) + }); + + let contents = reformat(replace_block( + &path, + "payload_modules", + &add_hidden_preamble("codegen_payload_mods_and_reexports", block), + )); + + ensure_file_contents(&path, &contents); +} + +/// Generates contents of [`setters`] module. +#[test] +fn codegen_setters_reexports() { + use crate::codegen::{ + add_hidden_preamble, ensure_file_contents, project_root, reformat, schema, + }; + + let path = project_root().join("src/payloads/setters.rs"); + let schema = schema::get(); + let mut contents = String::new(); + + contents.push_str("#[doc(no_inline)] pub use crate::payloads::{"); + schema + .methods + .iter() + .for_each(|m| contents.push_str(&format!("{M}Setters as _,", M = m.names.1))); + contents.push_str("};\n"); + + let contents = reformat(add_hidden_preamble("codegen_setters_reexports", contents)); + ensure_file_contents(&path, &contents); +} + +#[cfg(test)] +mod codegen; diff --git a/crates/teloxide-core/src/payloads/add_sticker_to_set.rs b/crates/teloxide-core/src/payloads/add_sticker_to_set.rs new file mode 100644 index 00000000..3ef0900f --- /dev/null +++ b/crates/teloxide-core/src/payloads/add_sticker_to_set.rs @@ -0,0 +1,30 @@ +//! Generated by `codegen_payloads`, do not edit by hand. + +use serde::Serialize; + +use crate::types::{InputSticker, MaskPosition, True, UserId}; + +impl_payload! { + @[multipart = sticker] + /// Use this method to add a new sticker to a set created by the bot. Animated stickers can be added to animated sticker sets and only to them. Animated sticker sets can have up to 50 stickers. Static sticker sets can have up to 120 stickers. Returns _True_ on success. + #[derive(Debug, Clone, Serialize)] + pub AddStickerToSet (AddStickerToSetSetters) => True { + required { + /// User identifier of sticker file owner + pub user_id: UserId, + /// Sticker set name + pub name: String [into], + /// **PNG** or **TGS** image with the sticker, must be up to 512 kilobytes in size, dimensions must not exceed 512px, and either width or height must be exactly 512px. Pass a _file\_id_ as a String to send a file that already exists on the Telegram servers, pass an HTTP URL as a String for Telegram to get a file from the Internet, or upload a new one using multipart/form-data. [More info on Sending Files »] + /// + /// [More info on Sending Files »]: crate::types::InputFile + #[serde(flatten)] + pub sticker: InputSticker, + /// One or more emoji corresponding to the sticker + pub emojis: String [into], + } + optional { + /// A JSON-serialized object for position where the mask should be placed on faces + pub mask_position: MaskPosition, + } + } +} diff --git a/crates/teloxide-core/src/payloads/answer_callback_query.rs b/crates/teloxide-core/src/payloads/answer_callback_query.rs new file mode 100644 index 00000000..7dbb2a82 --- /dev/null +++ b/crates/teloxide-core/src/payloads/answer_callback_query.rs @@ -0,0 +1,38 @@ +//! Generated by `codegen_payloads`, do not edit by hand. + +use serde::Serialize; +use url::Url; + +use crate::types::True; + +impl_payload! { + /// Use this method to send answers to callback queries sent from [inline keyboards]. The answer will be displayed to the user as a notification at the top of the chat screen or as an alert. On success, True is returned. + /// + /// >Alternatively, the user can be redirected to the specified Game URL. For this option to work, you must first create a game for your bot via [@Botfather] and accept the terms. Otherwise, you may use links like `t.me/your_bot?start=XXXX` that open your bot with a parameter. + /// + /// [inline keyboards]: https://core.telegram.org/bots#inline-keyboards-and-on-the-fly-updating + /// [@Botfather]: https://t.me/botfather + #[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize)] + pub AnswerCallbackQuery (AnswerCallbackQuerySetters) => True { + required { + /// Unique identifier for the query to be answered + pub callback_query_id: String [into], + } + optional { + /// Text of the notification. If not specified, nothing will be shown to the user, 0-200 characters + pub text: String [into], + /// If true, an alert will be shown by the client instead of a notification at the top of the chat screen. Defaults to false. + pub show_alert: bool, + /// URL that will be opened by the user's client. If you have created a [`Game`] and accepted the conditions via [@Botfather], specify the URL that opens your game — note that this will only work if the query comes from a _[callback\_game]_ button. + /// + /// Otherwise, you may use links like `t.me/your\_bot?start=XXXX` that open your bot with a parameter. + /// + /// [callback_game]: https://core.telegram.org/bots/api#inlinekeyboardbutton + /// [@Botfather]: https://t.me/botfather + /// [`Game`]: crate::types::Game + pub url: Url, + /// The maximum amount of time in seconds that the result of the callback query may be cached client-side. Telegram apps will support caching starting in version 3.14. Defaults to 0. + pub cache_time: u32, + } + } +} diff --git a/crates/teloxide-core/src/payloads/answer_inline_query.rs b/crates/teloxide-core/src/payloads/answer_inline_query.rs new file mode 100644 index 00000000..55420240 --- /dev/null +++ b/crates/teloxide-core/src/payloads/answer_inline_query.rs @@ -0,0 +1,35 @@ +//! Generated by `codegen_payloads`, do not edit by hand. + +use serde::Serialize; + +use crate::types::{InlineQueryResult, True}; + +impl_payload! { + /// Use this method to send answers to an inline query. On success, _True_ is returned. No more than **50** results per query are allowed. + #[derive(Debug, PartialEq, Clone, Serialize)] + pub AnswerInlineQuery (AnswerInlineQuerySetters) => True { + required { + /// Unique identifier for the answered query + pub inline_query_id: String [into], + /// A JSON-serialized array of results for the inline query + pub results: Vec [collect], + } + optional { + /// The maximum amount of time in seconds that the result of the inline query may be cached on the server. Defaults to 300. + pub cache_time: u32, + /// Pass _True_, if results may be cached on the server side only for the user that sent the query. By default, results may be returned to any user who sends the same query + pub is_personal: bool, + /// Pass the offset that a client should send in the next query with the same text to receive more results. Pass an empty string if there are no more results or if you don't support pagination. Offset length can't exceed 64 bytes. + pub next_offset: String [into], + /// If passed, clients will display a button with specified text that switches the user to a private chat with the bot and sends the bot a start message with the parameter switch_pm_parameter + pub switch_pm_text: String [into], + /// [Deep-linking] parameter for the /start message sent to the bot when user presses the switch button. 1-64 characters, only `A-Z`, `a-z`, `0-9`, `_` and `-` are allowed. + /// + /// _Example_: An inline bot that sends YouTube videos can ask the user to connect the bot to their YouTube account to adapt search results accordingly. To do this, it displays a 'Connect your YouTube account' button above the results, or even before showing any. The user presses the button, switches to a private chat with the bot and, in doing so, passes a start parameter that instructs the bot to return an oauth link. Once done, the bot can offer a [switch_inline] button so that the user can easily return to the chat where they wanted to use the bot's inline capabilities. + /// + /// [Deep-linking]: https://core.telegram.org/bots#deep-linking + /// [switch_inline]: https://core.telegram.org/bots/api#inlinekeyboardmarkup + pub switch_pm_parameter: String [into], + } + } +} diff --git a/crates/teloxide-core/src/payloads/answer_pre_checkout_query.rs b/crates/teloxide-core/src/payloads/answer_pre_checkout_query.rs new file mode 100644 index 00000000..c30f826d --- /dev/null +++ b/crates/teloxide-core/src/payloads/answer_pre_checkout_query.rs @@ -0,0 +1,24 @@ +//! Generated by `codegen_payloads`, do not edit by hand. + +use serde::Serialize; + +use crate::types::True; + +impl_payload! { + /// Once the user has confirmed their payment and shipping details, the Bot API sends the final confirmation in the form of an [`Update`] with the field pre\_checkout\_query. Use this method to respond to such pre-checkout queries. On success, True is returned. **Note:** The Bot API must receive an answer within 10 seconds after the pre-checkout query was sent. + /// + /// [`Update`]: crate::types::Update + #[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize)] + pub AnswerPreCheckoutQuery (AnswerPreCheckoutQuerySetters) => True { + required { + /// Unique identifier for the query to be answered + pub pre_checkout_query_id: String [into], + /// Specify True if everything is alright (goods are available, etc.) and the bot is ready to proceed with the order. Use False if there are any problems. + pub ok: bool, + } + optional { + /// Required if ok is False. Error message in human readable form that explains the reason for failure to proceed with the checkout (e.g. "Sorry, somebody just bought the last of our amazing black T-shirts while you were busy filling out your payment details. Please choose a different color or garment!"). Telegram will display this message to the user. + pub error_message: String [into], + } + } +} diff --git a/crates/teloxide-core/src/payloads/answer_shipping_query.rs b/crates/teloxide-core/src/payloads/answer_shipping_query.rs new file mode 100644 index 00000000..349ccab5 --- /dev/null +++ b/crates/teloxide-core/src/payloads/answer_shipping_query.rs @@ -0,0 +1,26 @@ +//! Generated by `codegen_payloads`, do not edit by hand. + +use serde::Serialize; + +use crate::types::{ShippingOption, True}; + +impl_payload! { + /// If you sent an invoice requesting a shipping address and the parameter _is\_flexible_ was specified, the Bot API will send an [`Update`] with a shipping_query field to the bot. Use this method to reply to shipping queries. On success, True is returned. + /// + /// [`Update`]: crate::types::Update + #[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize)] + pub AnswerShippingQuery (AnswerShippingQuerySetters) => True { + required { + /// Unique identifier for the query to be answered + pub shipping_query_id: String [into], + /// Specify True if delivery to the specified address is possible and False if there are any problems (for example, if delivery to the specified address is not possible) + pub ok: bool, + } + optional { + /// Required if ok is True. A JSON-serialized array of available shipping options. + pub shipping_options: Vec [collect], + /// Required if ok is False. Error message in human readable form that explains why it is impossible to complete the order (e.g. 'Sorry, delivery to your desired address is unavailable'). Telegram will display this message to the user. + pub error_message: String [into], + } + } +} diff --git a/crates/teloxide-core/src/payloads/answer_web_app_query.rs b/crates/teloxide-core/src/payloads/answer_web_app_query.rs new file mode 100644 index 00000000..79d3e9fa --- /dev/null +++ b/crates/teloxide-core/src/payloads/answer_web_app_query.rs @@ -0,0 +1,20 @@ +//! Generated by `codegen_payloads`, do not edit by hand. + +use serde::Serialize; + +use crate::types::{InlineQueryResult, SentWebAppMessage}; + +impl_payload! { + /// Use this method to set the result of an interaction with a [Web App] and send a corresponding message on behalf of the user to the chat from which the query originated. + /// + /// [Web App]: https://core.telegram.org/bots/webapps + #[derive(Debug, PartialEq, Clone, Serialize)] + pub AnswerWebAppQuery (AnswerWebAppQuerySetters) => SentWebAppMessage { + required { + /// Unique identifier for the query to be answered + pub web_app_query_id: String [into], + /// A JSON-serialized object describing the message to be sent + pub result: InlineQueryResult, + } + } +} diff --git a/crates/teloxide-core/src/payloads/approve_chat_join_request.rs b/crates/teloxide-core/src/payloads/approve_chat_join_request.rs new file mode 100644 index 00000000..54618be9 --- /dev/null +++ b/crates/teloxide-core/src/payloads/approve_chat_join_request.rs @@ -0,0 +1,18 @@ +//! Generated by `codegen_payloads`, do not edit by hand. + +use serde::Serialize; + +use crate::types::{Recipient, True, UserId}; + +impl_payload! { + /// Use this method to approve a chat join request. The bot must be an administrator in the chat for this to work and must have the _can_invite_users_ administrator right. Returns _True_ on success. + #[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize)] + pub ApproveChatJoinRequest (ApproveChatJoinRequestSetters) => True { + required { + /// Unique identifier for the target chat or username of the target channel (in the format `@channelusername`) + pub chat_id: Recipient [into], + /// Unique identifier of the target user + pub user_id: UserId, + } + } +} diff --git a/crates/teloxide-core/src/payloads/ban_chat_member.rs b/crates/teloxide-core/src/payloads/ban_chat_member.rs new file mode 100644 index 00000000..81fe5090 --- /dev/null +++ b/crates/teloxide-core/src/payloads/ban_chat_member.rs @@ -0,0 +1,28 @@ +//! Generated by `codegen_payloads`, do not edit by hand. + +use chrono::{DateTime, Utc}; +use serde::Serialize; + +use crate::types::{Recipient, True, UserId}; + +impl_payload! { + /// Use this method to ban a user in a group, a supergroup or a channel. In the case of supergroups and channels, the user will not be able to return to the chat on their own using invite links, etc., unless [unbanned] first. The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. Returns _True_ on success. + /// + /// [unbanned]: crate::payloads::UnbanChatMember + #[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize)] + pub BanChatMember (BanChatMemberSetters) => True { + required { + /// Unique identifier for the target chat or username of the target channel (in the format `@channelusername`) + pub chat_id: Recipient [into], + /// Unique identifier of the target user + pub user_id: UserId, + } + optional { + /// Date when the user will be unbanned, unix time. If user is banned for more than 366 days or less than 30 seconds from the current time they are considered to be banned forever + #[serde(with = "crate::types::serde_opt_date_from_unix_timestamp")] + pub until_date: DateTime [into], + /// Pass True to delete all messages from the chat for the user that is being removed. If False, the user will be able to see messages in the group that were sent before the user was removed. Always True for supergroups and channels. + pub revoke_messages: bool, + } + } +} diff --git a/crates/teloxide-core/src/payloads/ban_chat_sender_chat.rs b/crates/teloxide-core/src/payloads/ban_chat_sender_chat.rs new file mode 100644 index 00000000..4722a600 --- /dev/null +++ b/crates/teloxide-core/src/payloads/ban_chat_sender_chat.rs @@ -0,0 +1,18 @@ +//! Generated by `codegen_payloads`, do not edit by hand. + +use serde::Serialize; + +use crate::types::{ChatId, Recipient, True}; + +impl_payload! { + /// Use this method to ban a channel chat in a supergroup or a channel. The owner of the chat will not be able to send messages and join live streams on behalf of the chat, unless it is unbanned first. The bot must be an administrator in the supergroup or channel for this to work and must have the appropriate administrator rights. + #[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize)] + pub BanChatSenderChat (BanChatSenderChatSetters) => True { + required { + /// Unique identifier for the target chat or username of the target channel (in the format `@channelusername`) + pub chat_id: Recipient [into], + /// Unique identifier of the target sender chat + pub sender_chat_id: ChatId [into], + } + } +} diff --git a/crates/teloxide-core/src/payloads/close.rs b/crates/teloxide-core/src/payloads/close.rs new file mode 100644 index 00000000..749dc22a --- /dev/null +++ b/crates/teloxide-core/src/payloads/close.rs @@ -0,0 +1,13 @@ +//! Generated by `codegen_payloads`, do not edit by hand. + +use serde::Serialize; + +use crate::types::True; + +impl_payload! { + /// Use this method to close the bot instance before moving it from one local server to another. You need to delete the webhook before calling this method to ensure that the bot isn't launched again after server restart. The method will return error 429 in the first 10 minutes after the bot is launched. Returns _True_ on success. Requires no parameters. + #[derive(Debug, PartialEq, Eq, Hash, Default, Clone, Serialize)] + pub Close (CloseSetters) => True { + + } +} diff --git a/crates/teloxide-core/src/payloads/codegen.rs b/crates/teloxide-core/src/payloads/codegen.rs new file mode 100644 index 00000000..771caa7d --- /dev/null +++ b/crates/teloxide-core/src/payloads/codegen.rs @@ -0,0 +1,256 @@ +use std::{borrow::Borrow, collections::HashSet, ops::Deref}; + +use itertools::Itertools; + +use crate::codegen::{ + add_preamble, + convert::{convert_for, Convert}, + ensure_files_contents, project_root, reformat, + schema::{self, Doc, Method, Param, Type}, + to_uppercase, +}; + +#[test] +fn codegen_payloads() { + let base_path = project_root().join("src/payloads/"); + let schema = schema::get(); + + let mut files = Vec::new(); + + for method in schema.methods { + let file_name = format!("{}.rs", method.names.2); + let path = base_path.join(&*file_name); + + let uses = uses(&method); + + let method_doc = render_doc(&method.doc, method.sibling.as_deref()); + let eq_hash_derive = eq_hash_suitable(&method).then(|| " Eq, Hash,").unwrap_or(""); + let default_derive = default_needed(&method).then(|| " Default,").unwrap_or(""); + + let return_ty = method.return_ty.to_string(); + + let required = params(method.params.iter().filter(|p| !matches!(&p.ty, Type::Option(_)))); + let required = match &*required { + "" => "".to_owned(), + _ => format!(" required {{\n{required}\n }}"), + }; + + let optional = params(method.params.iter().filter_map(|p| match &p.ty { + Type::Option(inner) => Some(Param { + name: p.name.clone(), + ty: inner.deref().clone(), + descr: p.descr.clone(), + }), + _ => None, + })); + let optional = match &*optional { + "" => "".to_owned(), + _ if required.is_empty() => format!(" optional {{\n{optional}\n }}"), + _ => format!("\n optional {{\n{optional}\n }}"), + }; + + let multipart = multipart_input_file_fields(&method) + .map(|field| format!(" @[multipart = {}]\n", field.join(", "))) + .unwrap_or_else(String::new); + + let derive = if !multipart.is_empty() + || matches!( + &*method.names.1, + "SendMediaGroup" | "EditMessageMedia" | "EditMessageMediaInline" + ) { + "#[derive(Debug, Clone, Serialize)]".to_owned() + } else { + format!("#[derive(Debug, PartialEq,{eq_hash_derive}{default_derive} Clone, Serialize)]") + }; + + let timeout_secs = match &*method.names.2 { + "get_updates" => " @[timeout_secs = timeout]\n", + _ => "", + }; + + let contents = format!( + "\ +{uses} + +impl_payload! {{ +{multipart}{timeout_secs}{method_doc} + {derive} + pub {Method} ({Method}Setters) => {return_ty} {{ +{required}{optional} + }} +}} +", + Method = method.names.1, + ); + + files.push((path, reformat(add_preamble("codegen_payloads", contents)))); + } + + ensure_files_contents(files.iter().map(|(p, c)| (&**p, &**c))) +} + +fn uses(method: &Method) -> String { + enum Use { + Prelude, + Crate(String), + External(String), + } + + fn ty_use(ty: &Type) -> Use { + match ty { + Type::True => Use::Crate(String::from("use crate::types::True;")), + Type::u8 + | Type::u16 + | Type::u32 + | Type::i32 + | Type::u64 + | Type::i64 + | Type::f64 + | Type::bool + | Type::String => Use::Prelude, + Type::Option(inner) | Type::ArrayOf(inner) => ty_use(inner), + Type::RawTy(raw) => Use::Crate(["use crate::types::", raw, ";"].concat()), + Type::Url => Use::External(String::from("use url::Url;")), + Type::DateTime => Use::External(String::from("use chrono::{DateTime, Utc};")), + } + } + + let mut crate_uses = HashSet::new(); + let mut external_uses = HashSet::new(); + + external_uses.insert(String::from("use serde::Serialize;")); + + core::iter::once(&method.return_ty) + .chain(method.params.iter().map(|p| &p.ty)) + .map(ty_use) + .for_each(|u| match u { + Use::Prelude => {} + Use::Crate(u) => { + crate_uses.insert(u); + } + Use::External(u) => { + external_uses.insert(u); + } + }); + + let external_uses = external_uses.into_iter().join("\n"); + + if crate_uses.is_empty() { + external_uses + } else { + let crate_uses = crate_uses.into_iter().join(""); + + format!("{external_uses}\n\n{crate_uses}",) + } +} + +fn render_doc(doc: &Doc, sibling: Option<&str>) -> String { + let links = match &doc.md_links { + links if links.is_empty() => String::new(), + links => { + let l: String = + links.iter().map(|(name, link)| format!("\n /// [{name}]: {link}")).collect(); + + format!("\n ///{l}") + } + }; + + let sibling_note = sibling + .map(|s| { + format!( + "\n /// \n /// See also: [`{s}`](crate::payloads::{s})", + s = to_uppercase(s) + ) + }) + .unwrap_or_default(); + + [" /// ", &doc.md.replace('\n', "\n /// "), &sibling_note, &links].concat() +} + +fn eq_hash_suitable(method: &Method) -> bool { + fn ty_eq_hash_suitable(ty: &Type) -> bool { + match ty { + Type::f64 => false, + Type::Option(inner) | Type::ArrayOf(inner) => ty_eq_hash_suitable(&*inner), + + Type::True + | Type::u8 + | Type::u16 + | Type::u32 + | Type::i32 + | Type::u64 + | Type::i64 + | Type::bool + | Type::String => true, + + Type::Url | Type::DateTime => true, + + Type::RawTy(raw) => raw != "MaskPosition" && raw != "InlineQueryResult", + } + } + + method.params.iter().all(|p| ty_eq_hash_suitable(&p.ty)) +} + +fn default_needed(method: &Method) -> bool { + method.params.iter().all(|p| matches!(p.ty, Type::Option(_))) +} + +fn params(params: impl Iterator>) -> String { + params + .map(|param| { + let param = param.borrow(); + let doc = render_doc(¶m.descr, None).replace('\n', "\n "); + let field = ¶m.name; + let ty = ¶m.ty; + let flatten = match ty { + Type::RawTy(s) if s == "MessageId" && field == "reply_to_message_id" => { + "\n #[serde(serialize_with = \ + \"crate::types::serialize_reply_to_message_id\")]" + } + Type::RawTy(s) + if s == "MessageId" + || s == "InputSticker" + || s == "TargetMessage" + || s == "StickerType" => + { + "\n #[serde(flatten)]" + } + _ => "", + }; + let with = match ty { + Type::DateTime => { + "\n #[serde(with = \ + \"crate::types::serde_opt_date_from_unix_timestamp\")]" + } + _ => "", + }; + let rename = match field.strip_suffix('_') { + Some(field) => format!("\n #[serde(rename = \"{field}\")]"), + None => "".to_owned(), + }; + let convert = match convert_for(ty) { + Convert::Id(_) => "", + Convert::Into(_) => " [into]", + Convert::Collect(_) => " [collect]", + }; + format!(" {doc}{flatten}{with}{rename}\n pub {field}: {ty}{convert},") + }) + .join("\n") +} + +fn multipart_input_file_fields(m: &Method) -> Option> { + let fields: Vec<_> = + m.params.iter().filter(|&p| ty_is_multiparty(&p.ty)).map(|p| &*p.name).collect(); + + if fields.is_empty() { + None + } else { + Some(fields) + } +} + +fn ty_is_multiparty(ty: &Type) -> bool { + matches!(ty, Type::RawTy(x) if x == "InputFile" || x == "InputSticker") + || matches!(ty, Type::Option(inner) if ty_is_multiparty(inner)) +} diff --git a/crates/teloxide-core/src/payloads/copy_message.rs b/crates/teloxide-core/src/payloads/copy_message.rs new file mode 100644 index 00000000..b614b79e --- /dev/null +++ b/crates/teloxide-core/src/payloads/copy_message.rs @@ -0,0 +1,49 @@ +//! Generated by `codegen_payloads`, do not edit by hand. + +use serde::Serialize; + +use crate::types::{MessageEntity, MessageId, ParseMode, Recipient, ReplyMarkup}; + +impl_payload! { + /// Use this method to copy messages of any kind. The method is analogous to the method forwardMessage, but the copied message doesn't have a link to the original message. Returns the [`MessageId`] of the sent message on success. + /// + /// [`MessageId`]: crate::types::MessageId + #[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize)] + pub CopyMessage (CopyMessageSetters) => MessageId { + required { + /// Unique identifier for the target chat or username of the target channel (in the format `@channelusername`) + pub chat_id: Recipient [into], + /// Unique identifier for the chat where the original message was sent (or channel username in the format `@channelusername`) + pub from_chat_id: Recipient [into], + /// Message identifier in the chat specified in _from\_chat\_id_ + #[serde(flatten)] + pub message_id: MessageId, + } + optional { + /// New caption for media, 0-1024 characters after entities parsing. If not specified, the original caption is kept + pub caption: String [into], + /// Mode for parsing entities in the photo caption. See [formatting options] for more details. + /// + /// [formatting options]: https://core.telegram.org/bots/api#formatting-options + pub parse_mode: ParseMode, + /// List of special entities that appear in the new caption, which can be specified instead of _parse\_mode_ + pub caption_entities: Vec [collect], + /// Sends the message [silently]. Users will receive a notification with no sound. + /// + /// [silently]: https://telegram.org/blog/channels-2-0#silent-messages + pub disable_notification: bool, + /// Protects the contents of sent messages from forwarding and saving + pub protect_content: bool, + /// If the message is a reply, ID of the original message + #[serde(serialize_with = "crate::types::serialize_reply_to_message_id")] + pub reply_to_message_id: MessageId, + /// Pass _True_, if the message should be sent even if the specified replied-to message is not found + pub allow_sending_without_reply: bool, + /// Additional interface options. A JSON-serialized object for an [inline keyboard], [custom reply keyboard], instructions to remove reply keyboard or to force a reply from the user. + /// + /// [inline keyboard]: https://core.telegram.org/bots#inline-keyboards-and-on-the-fly-updating + /// [custom reply keyboard]: https://core.telegram.org/bots#keyboards + pub reply_markup: ReplyMarkup [into], + } + } +} diff --git a/crates/teloxide-core/src/payloads/create_chat_invite_link.rs b/crates/teloxide-core/src/payloads/create_chat_invite_link.rs new file mode 100644 index 00000000..6e18a577 --- /dev/null +++ b/crates/teloxide-core/src/payloads/create_chat_invite_link.rs @@ -0,0 +1,31 @@ +//! Generated by `codegen_payloads`, do not edit by hand. + +use chrono::{DateTime, Utc}; +use serde::Serialize; + +use crate::types::{ChatInviteLink, Recipient}; + +impl_payload! { + /// Use this method to create an additional invite link for a chat. The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. The link can be revoked using the method [`RevokeChatInviteLink`]. Returns the new invite link as [`ChatInviteLink`] object. + /// + /// [`ChatInviteLink`]: crate::types::ChatInviteLink + /// [`RevokeChatInviteLink`]: crate::payloads::RevokeChatInviteLink + #[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize)] + pub CreateChatInviteLink (CreateChatInviteLinkSetters) => ChatInviteLink { + required { + /// Unique identifier for the target chat or username of the target channel (in the format `@channelusername`) + pub chat_id: Recipient [into], + } + optional { + /// Invite link name; 0-32 characters + pub name: String [into], + /// Point in time (Unix timestamp) when the link will expire + #[serde(with = "crate::types::serde_opt_date_from_unix_timestamp")] + pub expire_date: DateTime [into], + /// Maximum number of users that can be members of the chat simultaneously after joining the chat via this invite link; 1-99999 + pub member_limit: u32, + /// True, if users joining the chat via the link need to be approved by chat administrators. If True, member_limit can't be specified + pub creates_join_request: bool, + } + } +} diff --git a/crates/teloxide-core/src/payloads/create_invoice_link.rs b/crates/teloxide-core/src/payloads/create_invoice_link.rs new file mode 100644 index 00000000..72748157 --- /dev/null +++ b/crates/teloxide-core/src/payloads/create_invoice_link.rs @@ -0,0 +1,60 @@ +//! Generated by `codegen_payloads`, do not edit by hand. + +use serde::Serialize; + +use crate::types::LabeledPrice; + +impl_payload! { + /// Use this method to create a link for an invoice. Returns the created invoice link as String on success. + #[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize)] + pub CreateInvoiceLink (CreateInvoiceLinkSetters) => String { + required { + /// Product name, 1-32 characters + pub title: String [into], + /// Product description, 1-255 characters + pub description: String [into], + /// Bot-defined invoice payload, 1-128 bytes. This will not be displayed to the user, use for your internal processes. + pub payload: String [into], + /// Payments provider token, obtained via [Botfather] + /// + /// [Botfather]: https://t.me/botfather + pub provider_token: String [into], + /// Three-letter ISO 4217 currency code, see more on currencies + pub currency: String [into], + /// Price breakdown, a JSON-serialized list of components (e.g. product price, tax, discount, delivery cost, delivery tax, bonus, etc.) + pub prices: Vec [collect], + } + optional { + /// The maximum accepted amount for tips in the smallest units of the currency (integer, **not** float/double). For example, for a maximum tip of `US$ 1.45` pass `max_tip_amount = 145`. See the exp parameter in [`currencies.json`], it shows the number of digits past the decimal point for each currency (2 for the majority of currencies). Defaults to 0 + /// + /// [`currencies.json`]: https://core.telegram.org/bots/payments/currencies.json + pub max_tip_amount: u32, + /// A JSON-serialized array of suggested amounts of tips in the smallest units of the currency (integer, **not** float/double). At most 4 suggested tip amounts can be specified. The suggested tip amounts must be positive, passed in a strictly increased order and must not exceed _max_tip_amount_. + pub suggested_tip_amounts: Vec [collect], + /// A JSON-serialized data about the invoice, which will be shared with the payment provider. A detailed description of required fields should be provided by the payment provider. + pub provider_data: String [into], + /// URL of the product photo for the invoice. Can be a photo of the goods or a marketing image for a service. People like it better when they see what they are paying for. + pub photo_url: String [into], + /// Photo size in bytes + pub photo_size: String [into], + /// Photo width + pub photo_width: String [into], + /// Photo height + pub photo_height: String [into], + /// Pass _True_, if you require the user's full name to complete the order + pub need_name: bool, + /// Pass _True_, if you require the user's phone number to complete the order + pub need_phone_number: bool, + /// Pass _True_, if you require the user's email address to complete the order + pub need_email: bool, + /// Pass _True_, if you require the user's shipping address to complete the order + pub need_shipping_address: bool, + /// Pass _True_, if user's phone number should be sent to provider + pub send_phone_number_to_provider: bool, + /// Pass _True_, if user's email address should be sent to provider + pub send_email_to_provider: bool, + /// Pass _True_, if the final price depends on the shipping method + pub is_flexible: bool, + } + } +} diff --git a/crates/teloxide-core/src/payloads/create_new_sticker_set.rs b/crates/teloxide-core/src/payloads/create_new_sticker_set.rs new file mode 100644 index 00000000..2fa6b2b7 --- /dev/null +++ b/crates/teloxide-core/src/payloads/create_new_sticker_set.rs @@ -0,0 +1,35 @@ +//! Generated by `codegen_payloads`, do not edit by hand. + +use serde::Serialize; + +use crate::types::{InputSticker, MaskPosition, StickerType, True, UserId}; + +impl_payload! { + @[multipart = sticker] + /// Use this method to create a new sticker set owned by a user. The bot will be able to edit the sticker set thus created. You must use exactly one of the fields _png\_sticker_ or _tgs\_sticker_. Returns _True_ on success. + #[derive(Debug, Clone, Serialize)] + pub CreateNewStickerSet (CreateNewStickerSetSetters) => True { + required { + /// User identifier of sticker file owner + pub user_id: UserId, + /// Short name of sticker set, to be used in `t.me/addstickers/` URLs (e.g., _animals_). Can contain only english letters, digits and underscores. Must begin with a letter, can't contain consecutive underscores and must end in _“\_by\_”. _ is case insensitive. 1-64 characters. + pub name: String [into], + /// Sticker set title, 1-64 characters + pub title: String [into], + /// **PNG** image, **TGS** animation or **WEBM** video with the sticker, must be up to 512 kilobytes in size, dimensions must not exceed 512px, and either width or height must be exactly 512px. Pass a _file\_id_ as a String to send a file that already exists on the Telegram servers, pass an HTTP URL as a String for Telegram to get a file from the Internet, or upload a new one using multipart/form-data. [More info on Sending Files »] + /// + /// [More info on Sending Files »]: crate::types::InputFile + #[serde(flatten)] + pub sticker: InputSticker, + /// One or more emoji corresponding to the sticker + pub emojis: String [into], + } + optional { + /// Type of stickers in the set, pass “regular” or “mask”. Custom emoji sticker sets can't be created via the Bot API at the moment. By default, a regular sticker set is created. + #[serde(flatten)] + pub sticker_type: StickerType, + /// A JSON-serialized object for position where the mask should be placed on faces + pub mask_position: MaskPosition, + } + } +} diff --git a/crates/teloxide-core/src/payloads/decline_chat_join_request.rs b/crates/teloxide-core/src/payloads/decline_chat_join_request.rs new file mode 100644 index 00000000..3626f452 --- /dev/null +++ b/crates/teloxide-core/src/payloads/decline_chat_join_request.rs @@ -0,0 +1,18 @@ +//! Generated by `codegen_payloads`, do not edit by hand. + +use serde::Serialize; + +use crate::types::{Recipient, True, UserId}; + +impl_payload! { + /// Use this method to decline a chat join request. The bot must be an administrator in the chat for this to work and must have the _can_invite_users_ administrator right. Returns _True_ on success. + #[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize)] + pub DeclineChatJoinRequest (DeclineChatJoinRequestSetters) => True { + required { + /// Unique identifier for the target chat or username of the target channel (in the format `@channelusername`) + pub chat_id: Recipient [into], + /// Unique identifier of the target user + pub user_id: UserId, + } + } +} diff --git a/crates/teloxide-core/src/payloads/delete_chat_photo.rs b/crates/teloxide-core/src/payloads/delete_chat_photo.rs new file mode 100644 index 00000000..53ab7ce1 --- /dev/null +++ b/crates/teloxide-core/src/payloads/delete_chat_photo.rs @@ -0,0 +1,16 @@ +//! Generated by `codegen_payloads`, do not edit by hand. + +use serde::Serialize; + +use crate::types::Recipient; + +impl_payload! { + /// Use this method to delete a chat photo. Photos can't be changed for private chats. The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. Returns True on success. + #[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize)] + pub DeleteChatPhoto (DeleteChatPhotoSetters) => String { + required { + /// Unique identifier for the target chat or username of the target channel (in the format `@channelusername`) + pub chat_id: Recipient [into], + } + } +} diff --git a/crates/teloxide-core/src/payloads/delete_chat_sticker_set.rs b/crates/teloxide-core/src/payloads/delete_chat_sticker_set.rs new file mode 100644 index 00000000..222ae9ff --- /dev/null +++ b/crates/teloxide-core/src/payloads/delete_chat_sticker_set.rs @@ -0,0 +1,18 @@ +//! Generated by `codegen_payloads`, do not edit by hand. + +use serde::Serialize; + +use crate::types::{Recipient, True}; + +impl_payload! { + /// Use this method to delete a group sticker set from a supergroup. The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. Use the field `can_set_sticker_set` optionally returned in [`GetChat`] requests to check if the bot can use this method. Returns _True_ on success. + /// + /// [`GetChat`]: crate::payloads::GetChat + #[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize)] + pub DeleteChatStickerSet (DeleteChatStickerSetSetters) => True { + required { + /// Unique identifier for the target chat or username of the target channel (in the format `@channelusername`) + pub chat_id: Recipient [into], + } + } +} diff --git a/crates/teloxide-core/src/payloads/delete_message.rs b/crates/teloxide-core/src/payloads/delete_message.rs new file mode 100644 index 00000000..8503c4dd --- /dev/null +++ b/crates/teloxide-core/src/payloads/delete_message.rs @@ -0,0 +1,28 @@ +//! Generated by `codegen_payloads`, do not edit by hand. + +use serde::Serialize; + +use crate::types::{MessageId, Recipient, True}; + +impl_payload! { + /// Use this method to delete a message, including service messages, with the following limitations: + /// - A message can only be deleted if it was sent less than 48 hours ago. + /// - A dice message in a private chat can only be deleted if it was sent more than 24 hours ago. + /// - Bots can delete outgoing messages in private chats, groups, and supergroups. + /// - Bots can delete incoming messages in private chats. + /// - Bots granted can_post_messages permissions can delete outgoing messages in channels. + /// - If the bot is an administrator of a group, it can delete any message there. + /// - If the bot has can_delete_messages permission in a supergroup or a channel, it can delete any message there. + /// + /// Returns True on success. + #[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize)] + pub DeleteMessage (DeleteMessageSetters) => True { + required { + /// Unique identifier for the target chat or username of the target channel (in the format `@channelusername`). + pub chat_id: Recipient [into], + /// Identifier of the message to delete + #[serde(flatten)] + pub message_id: MessageId, + } + } +} diff --git a/crates/teloxide-core/src/payloads/delete_my_commands.rs b/crates/teloxide-core/src/payloads/delete_my_commands.rs new file mode 100644 index 00000000..6b91c126 --- /dev/null +++ b/crates/teloxide-core/src/payloads/delete_my_commands.rs @@ -0,0 +1,20 @@ +//! Generated by `codegen_payloads`, do not edit by hand. + +use serde::Serialize; + +use crate::types::{BotCommandScope, True}; + +impl_payload! { + /// Use this method to delete the list of the bot's commands for the given scope and user language. After deletion, [higher level commands] will be shown to affected users. Returns _True_ on success. + /// + /// [higher level commands]: https://core.telegram.org/bots/api#determining-list-of-commands + #[derive(Debug, PartialEq, Eq, Hash, Default, Clone, Serialize)] + pub DeleteMyCommands (DeleteMyCommandsSetters) => True { + optional { + /// A JSON-serialized object, describing scope of users for which the commands are relevant. Defaults to BotCommandScopeDefault. + pub scope: BotCommandScope, + /// A two-letter ISO 639-1 language code. If empty, commands will be applied to all users from the given scope, for whose language there are no dedicated commands + pub language_code: String [into], + } + } +} diff --git a/crates/teloxide-core/src/payloads/delete_sticker_from_set.rs b/crates/teloxide-core/src/payloads/delete_sticker_from_set.rs new file mode 100644 index 00000000..020b9616 --- /dev/null +++ b/crates/teloxide-core/src/payloads/delete_sticker_from_set.rs @@ -0,0 +1,16 @@ +//! Generated by `codegen_payloads`, do not edit by hand. + +use serde::Serialize; + +use crate::types::True; + +impl_payload! { + /// Use this method to delete a sticker from a set created by the bot. Returns _True_ on success. + #[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize)] + pub DeleteStickerFromSet (DeleteStickerFromSetSetters) => True { + required { + /// File identifier of the sticker + pub sticker: String [into], + } + } +} diff --git a/crates/teloxide-core/src/payloads/delete_webhook.rs b/crates/teloxide-core/src/payloads/delete_webhook.rs new file mode 100644 index 00000000..49ad8872 --- /dev/null +++ b/crates/teloxide-core/src/payloads/delete_webhook.rs @@ -0,0 +1,18 @@ +//! Generated by `codegen_payloads`, do not edit by hand. + +use serde::Serialize; + +use crate::types::True; + +impl_payload! { + /// Use this method to remove webhook integration if you decide to switch back to [`GetUpdates`]. Returns True on success. Requires no parameters. + /// + /// [`GetUpdates`]: crate::payloads::GetUpdates + #[derive(Debug, PartialEq, Eq, Hash, Default, Clone, Serialize)] + pub DeleteWebhook (DeleteWebhookSetters) => True { + optional { + /// Pass _True_ to drop all pending updates + pub drop_pending_updates: bool, + } + } +} diff --git a/crates/teloxide-core/src/payloads/edit_chat_invite_link.rs b/crates/teloxide-core/src/payloads/edit_chat_invite_link.rs new file mode 100644 index 00000000..7ff80f19 --- /dev/null +++ b/crates/teloxide-core/src/payloads/edit_chat_invite_link.rs @@ -0,0 +1,32 @@ +//! Generated by `codegen_payloads`, do not edit by hand. + +use chrono::{DateTime, Utc}; +use serde::Serialize; + +use crate::types::Recipient; + +impl_payload! { + /// Use this method to edit a non-primary invite link created by the bot. The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. Returns the edited invite link as a [`ChatInviteLink`] object. + /// + /// [`ChatInviteLink`]: crate::types::ChatInviteLink + #[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize)] + pub EditChatInviteLink (EditChatInviteLinkSetters) => String { + required { + /// Unique identifier for the target chat or username of the target channel (in the format `@channelusername`) + pub chat_id: Recipient [into], + /// The invite link to edit + pub invite_link: String [into], + } + optional { + /// Invite link name; 0-32 characters + pub name: String [into], + /// Point in time (Unix timestamp) when the link will expire + #[serde(with = "crate::types::serde_opt_date_from_unix_timestamp")] + pub expire_date: DateTime [into], + /// Maximum number of users that can be members of the chat simultaneously after joining the chat via this invite link; 1-99999 + pub member_limit: u32, + /// True, if users joining the chat via the link need to be approved by chat administrators. If True, member_limit can't be specified + pub creates_join_request: bool, + } + } +} diff --git a/crates/teloxide-core/src/payloads/edit_message_caption.rs b/crates/teloxide-core/src/payloads/edit_message_caption.rs new file mode 100644 index 00000000..4f993098 --- /dev/null +++ b/crates/teloxide-core/src/payloads/edit_message_caption.rs @@ -0,0 +1,35 @@ +//! Generated by `codegen_payloads`, do not edit by hand. + +use serde::Serialize; + +use crate::types::{InlineKeyboardMarkup, Message, MessageEntity, MessageId, ParseMode, Recipient}; + +impl_payload! { + /// Use this method to edit captions of messages. On success, the edited Message is returned. + /// + /// See also: [`EditMessageCaptionInline`](crate::payloads::EditMessageCaptionInline) + #[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize)] + pub EditMessageCaption (EditMessageCaptionSetters) => Message { + required { + /// Unique identifier for the target chat or username of the target channel (in the format `@channelusername`). + pub chat_id: Recipient [into], + /// Identifier of the message to edit + #[serde(flatten)] + pub message_id: MessageId, + } + optional { + /// New caption of the message, 0-1024 characters after entities parsing + pub caption: String [into], + /// Mode for parsing entities in the message text. See [formatting options] for more details. + /// + /// [formatting options]: https://core.telegram.org/bots/api#formatting-options + pub parse_mode: ParseMode, + /// List of special entities that appear in the caption, which can be specified instead of _parse\_mode_ + pub caption_entities: Vec [collect], + /// A JSON-serialized object for an [inline keyboard]. + /// + /// [inline keyboard]: https://core.telegram.org/bots#inline-keyboards-and-on-the-fly-updating + pub reply_markup: InlineKeyboardMarkup, + } + } +} diff --git a/crates/teloxide-core/src/payloads/edit_message_caption_inline.rs b/crates/teloxide-core/src/payloads/edit_message_caption_inline.rs new file mode 100644 index 00000000..f044f0de --- /dev/null +++ b/crates/teloxide-core/src/payloads/edit_message_caption_inline.rs @@ -0,0 +1,32 @@ +//! Generated by `codegen_payloads`, do not edit by hand. + +use serde::Serialize; + +use crate::types::{InlineKeyboardMarkup, MessageEntity, ParseMode, True}; + +impl_payload! { + /// Use this method to edit captions of messages. On success, _True_ is returned. + /// + /// See also: [`EditMessageCaption`](crate::payloads::EditMessageCaption) + #[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize)] + pub EditMessageCaptionInline (EditMessageCaptionInlineSetters) => True { + required { + /// Identifier of the inline message + pub inline_message_id: String [into], + } + optional { + /// New caption of the message, 0-1024 characters after entities parsing + pub caption: String [into], + /// Mode for parsing entities in the message text. See [formatting options] for more details. + /// + /// [formatting options]: https://core.telegram.org/bots/api#formatting-options + pub parse_mode: ParseMode, + /// List of special entities that appear in the caption, which can be specified instead of _parse\_mode_ + pub caption_entities: Vec [collect], + /// A JSON-serialized object for an [inline keyboard]. + /// + /// [inline keyboard]: https://core.telegram.org/bots#inline-keyboards-and-on-the-fly-updating + pub reply_markup: InlineKeyboardMarkup, + } + } +} diff --git a/crates/teloxide-core/src/payloads/edit_message_live_location.rs b/crates/teloxide-core/src/payloads/edit_message_live_location.rs new file mode 100644 index 00000000..b8df0e5a --- /dev/null +++ b/crates/teloxide-core/src/payloads/edit_message_live_location.rs @@ -0,0 +1,40 @@ +//! Generated by `codegen_payloads`, do not edit by hand. + +use serde::Serialize; + +use crate::types::{Message, MessageId, Recipient, ReplyMarkup}; + +impl_payload! { + /// Use this method to edit live location messages. A location can be edited until its live_period expires or editing is explicitly disabled by a call to [`StopMessageLiveLocation`]. On success, the edited Message is returned. + /// + /// See also: [`EditMessageLiveLocationInline`](crate::payloads::EditMessageLiveLocationInline) + /// + /// [`StopMessageLiveLocation`]: crate::payloads::StopMessageLiveLocation + #[derive(Debug, PartialEq, Clone, Serialize)] + pub EditMessageLiveLocation (EditMessageLiveLocationSetters) => Message { + required { + /// Unique identifier for the target chat or username of the target channel (in the format `@channelusername`) + pub chat_id: Recipient [into], + /// Identifier of the message to edit + #[serde(flatten)] + pub message_id: MessageId, + /// Latitude of new location + pub latitude: f64, + /// Longitude of new location + pub longitude: f64, + } + optional { + /// The radius of uncertainty for the location, measured in meters; 0-1500 + pub horizontal_accuracy: f64, + /// For live locations, a direction in which the user is moving, in degrees. Must be between 1 and 360 if specified. + pub heading: u16, + /// For live locations, a maximum distance for proximity alerts about approaching another chat member, in meters. Must be between 1 and 100000 if specified. + pub proximity_alert_radius: u32, + /// Additional interface options. A JSON-serialized object for an [inline keyboard], [custom reply keyboard], instructions to remove reply keyboard or to force a reply from the user. + /// + /// [inline keyboard]: https://core.telegram.org/bots#inline-keyboards-and-on-the-fly-updating + /// [custom reply keyboard]: https://core.telegram.org/bots#keyboards + pub reply_markup: ReplyMarkup [into], + } + } +} diff --git a/crates/teloxide-core/src/payloads/edit_message_live_location_inline.rs b/crates/teloxide-core/src/payloads/edit_message_live_location_inline.rs new file mode 100644 index 00000000..83883889 --- /dev/null +++ b/crates/teloxide-core/src/payloads/edit_message_live_location_inline.rs @@ -0,0 +1,37 @@ +//! Generated by `codegen_payloads`, do not edit by hand. + +use serde::Serialize; + +use crate::types::{Message, ReplyMarkup}; + +impl_payload! { + /// Use this method to edit live location messages. A location can be edited until its live_period expires or editing is explicitly disabled by a call to [`StopMessageLiveLocation`]. On success, True is returned. + /// + /// See also: [`EditMessageLiveLocation`](crate::payloads::EditMessageLiveLocation) + /// + /// [`StopMessageLiveLocation`]: crate::payloads::StopMessageLiveLocation + #[derive(Debug, PartialEq, Clone, Serialize)] + pub EditMessageLiveLocationInline (EditMessageLiveLocationInlineSetters) => Message { + required { + /// Identifier of the inline message + pub inline_message_id: String [into], + /// Latitude of new location + pub latitude: f64, + /// Longitude of new location + pub longitude: f64, + } + optional { + /// The radius of uncertainty for the location, measured in meters; 0-1500 + pub horizontal_accuracy: f64, + /// For live locations, a direction in which the user is moving, in degrees. Must be between 1 and 360 if specified. + pub heading: u16, + /// For live locations, a maximum distance for proximity alerts about approaching another chat member, in meters. Must be between 1 and 100000 if specified. + pub proximity_alert_radius: u32, + /// Additional interface options. A JSON-serialized object for an [inline keyboard], [custom reply keyboard], instructions to remove reply keyboard or to force a reply from the user. + /// + /// [inline keyboard]: https://core.telegram.org/bots#inline-keyboards-and-on-the-fly-updating + /// [custom reply keyboard]: https://core.telegram.org/bots#keyboards + pub reply_markup: ReplyMarkup [into], + } + } +} diff --git a/crates/teloxide-core/src/payloads/edit_message_media.rs b/crates/teloxide-core/src/payloads/edit_message_media.rs new file mode 100644 index 00000000..917a13b7 --- /dev/null +++ b/crates/teloxide-core/src/payloads/edit_message_media.rs @@ -0,0 +1,29 @@ +//! Generated by `codegen_payloads`, do not edit by hand. + +use serde::Serialize; + +use crate::types::{InlineKeyboardMarkup, InputMedia, Message, MessageId, Recipient}; + +impl_payload! { + /// Use this method to edit animation, audio, document, photo, or video messages. If a message is a part of a message album, then it can be edited only to a photo or a video. Otherwise, message type can be changed arbitrarily. When inline message is edited, new file can't be uploaded. Use previously uploaded file via its file_id or specify a URL. On success, the edited Message is returned. + /// + /// See also: [`EditMessageMediaInline`](crate::payloads::EditMessageMediaInline) + #[derive(Debug, Clone, Serialize)] + pub EditMessageMedia (EditMessageMediaSetters) => Message { + required { + /// Unique identifier for the target chat or username of the target channel (in the format `@channelusername`). + pub chat_id: Recipient [into], + /// Identifier of the message to edit + #[serde(flatten)] + pub message_id: MessageId, + /// A JSON-serialized object for a new media content of the message + pub media: InputMedia, + } + optional { + /// A JSON-serialized object for an [inline keyboard]. + /// + /// [inline keyboard]: https://core.telegram.org/bots#inline-keyboards-and-on-the-fly-updating + pub reply_markup: InlineKeyboardMarkup, + } + } +} diff --git a/crates/teloxide-core/src/payloads/edit_message_media_inline.rs b/crates/teloxide-core/src/payloads/edit_message_media_inline.rs new file mode 100644 index 00000000..1daf67f8 --- /dev/null +++ b/crates/teloxide-core/src/payloads/edit_message_media_inline.rs @@ -0,0 +1,26 @@ +//! Generated by `codegen_payloads`, do not edit by hand. + +use serde::Serialize; + +use crate::types::{InlineKeyboardMarkup, InputMedia, True}; + +impl_payload! { + /// Use this method to edit animation, audio, document, photo, or video messages. If a message is a part of a message album, then it can be edited only to a photo or a video. Otherwise, message type can be changed arbitrarily. When inline message is edited, new file can't be uploaded. Use previously uploaded file via its file_id or specify a URL. On success, _True_ is returned. + /// + /// See also: [`EditMessageMedia`](crate::payloads::EditMessageMedia) + #[derive(Debug, Clone, Serialize)] + pub EditMessageMediaInline (EditMessageMediaInlineSetters) => True { + required { + /// Identifier of the inline message + pub inline_message_id: String [into], + /// A JSON-serialized object for a new media content of the message + pub media: InputMedia, + } + optional { + /// A JSON-serialized object for an [inline keyboard]. + /// + /// [inline keyboard]: https://core.telegram.org/bots#inline-keyboards-and-on-the-fly-updating + pub reply_markup: InlineKeyboardMarkup, + } + } +} diff --git a/crates/teloxide-core/src/payloads/edit_message_reply_markup.rs b/crates/teloxide-core/src/payloads/edit_message_reply_markup.rs new file mode 100644 index 00000000..dd64252d --- /dev/null +++ b/crates/teloxide-core/src/payloads/edit_message_reply_markup.rs @@ -0,0 +1,27 @@ +//! Generated by `codegen_payloads`, do not edit by hand. + +use serde::Serialize; + +use crate::types::{InlineKeyboardMarkup, Message, MessageId, Recipient}; + +impl_payload! { + /// Use this method to edit only the reply markup of messages. On success, the edited Message is returned. + /// + /// See also: [`EditMessageMediaInline`](crate::payloads::EditMessageMediaInline) + #[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize)] + pub EditMessageReplyMarkup (EditMessageReplyMarkupSetters) => Message { + required { + /// Unique identifier for the target chat or username of the target channel (in the format `@channelusername`). + pub chat_id: Recipient [into], + /// Identifier of the message to edit + #[serde(flatten)] + pub message_id: MessageId, + } + optional { + /// A JSON-serialized object for an [inline keyboard]. + /// + /// [inline keyboard]: https://core.telegram.org/bots#inline-keyboards-and-on-the-fly-updating + pub reply_markup: InlineKeyboardMarkup, + } + } +} diff --git a/crates/teloxide-core/src/payloads/edit_message_reply_markup_inline.rs b/crates/teloxide-core/src/payloads/edit_message_reply_markup_inline.rs new file mode 100644 index 00000000..c478ebb6 --- /dev/null +++ b/crates/teloxide-core/src/payloads/edit_message_reply_markup_inline.rs @@ -0,0 +1,24 @@ +//! Generated by `codegen_payloads`, do not edit by hand. + +use serde::Serialize; + +use crate::types::{InlineKeyboardMarkup, True}; + +impl_payload! { + /// Use this method to edit only the reply markup of messages. On success, _True_ is returned. + /// + /// See also: [`EditMessageReplyMarkup`](crate::payloads::EditMessageReplyMarkup) + #[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize)] + pub EditMessageReplyMarkupInline (EditMessageReplyMarkupInlineSetters) => True { + required { + /// Identifier of the inline message + pub inline_message_id: String [into], + } + optional { + /// A JSON-serialized object for an [inline keyboard]. + /// + /// [inline keyboard]: https://core.telegram.org/bots#inline-keyboards-and-on-the-fly-updating + pub reply_markup: InlineKeyboardMarkup, + } + } +} diff --git a/crates/teloxide-core/src/payloads/edit_message_text.rs b/crates/teloxide-core/src/payloads/edit_message_text.rs new file mode 100644 index 00000000..ba4329c5 --- /dev/null +++ b/crates/teloxide-core/src/payloads/edit_message_text.rs @@ -0,0 +1,39 @@ +//! Generated by `codegen_payloads`, do not edit by hand. + +use serde::Serialize; + +use crate::types::{InlineKeyboardMarkup, Message, MessageEntity, MessageId, ParseMode, Recipient}; + +impl_payload! { + /// Use this method to edit text and [games] messages. On success, the edited Message is returned. + /// + /// See also: [`EditMessageTextInline`](crate::payloads::EditMessageTextInline) + /// + /// [games]: https://core.telegram.org/bots/api#games + #[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize)] + pub EditMessageText (EditMessageTextSetters) => Message { + required { + /// Unique identifier for the target chat or username of the target channel (in the format `@channelusername`). + pub chat_id: Recipient [into], + /// Identifier of the message to edit + #[serde(flatten)] + pub message_id: MessageId, + /// New text of the message, 1-4096 characters after entities parsing + pub text: String [into], + } + optional { + /// Mode for parsing entities in the message text. See [formatting options] for more details. + /// + /// [formatting options]: https://core.telegram.org/bots/api#formatting-options + pub parse_mode: ParseMode, + /// List of special entities that appear in message text, which can be specified instead of _parse\_mode_ + pub entities: Vec [collect], + /// Disables link previews for links in this message + pub disable_web_page_preview: bool, + /// A JSON-serialized object for an [inline keyboard]. + /// + /// [inline keyboard]: https://core.telegram.org/bots#inline-keyboards-and-on-the-fly-updating + pub reply_markup: InlineKeyboardMarkup, + } + } +} diff --git a/crates/teloxide-core/src/payloads/edit_message_text_inline.rs b/crates/teloxide-core/src/payloads/edit_message_text_inline.rs new file mode 100644 index 00000000..4dea6cd4 --- /dev/null +++ b/crates/teloxide-core/src/payloads/edit_message_text_inline.rs @@ -0,0 +1,36 @@ +//! Generated by `codegen_payloads`, do not edit by hand. + +use serde::Serialize; + +use crate::types::{InlineKeyboardMarkup, MessageEntity, ParseMode, True}; + +impl_payload! { + /// Use this method to edit text and [games] messages. On success, _True_ is returned. + /// + /// See also: [`EditMessageText`](crate::payloads::EditMessageText) + /// + /// [games]: https://core.telegram.org/bots/api#games + #[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize)] + pub EditMessageTextInline (EditMessageTextInlineSetters) => True { + required { + /// Identifier of the inline message + pub inline_message_id: String [into], + /// New text of the message, 1-4096 characters after entities parsing + pub text: String [into], + } + optional { + /// Mode for parsing entities in the message text. See [formatting options] for more details. + /// + /// [formatting options]: https://core.telegram.org/bots/api#formatting-options + pub parse_mode: ParseMode, + /// List of special entities that appear in message text, which can be specified instead of _parse\_mode_ + pub entities: Vec [collect], + /// Disables link previews for links in this message + pub disable_web_page_preview: bool, + /// A JSON-serialized object for an [inline keyboard]. + /// + /// [inline keyboard]: https://core.telegram.org/bots#inline-keyboards-and-on-the-fly-updating + pub reply_markup: InlineKeyboardMarkup, + } + } +} diff --git a/crates/teloxide-core/src/payloads/export_chat_invite_link.rs b/crates/teloxide-core/src/payloads/export_chat_invite_link.rs new file mode 100644 index 00000000..357eb829 --- /dev/null +++ b/crates/teloxide-core/src/payloads/export_chat_invite_link.rs @@ -0,0 +1,18 @@ +//! Generated by `codegen_payloads`, do not edit by hand. + +use serde::Serialize; + +use crate::types::Recipient; + +impl_payload! { + /// Use this method to generate a new invite link for a chat; any previously generated link is revoked. The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. Returns the new invite link as String on success. + /// + /// > Note: Each administrator in a chat generates their own invite links. Bots can't use invite links generated by other administrators. If you want your bot to work with invite links, it will need to generate its own link using exportChatInviteLink — after this the link will become available to the bot via the getChat method. If your bot needs to generate a new invite link replacing its previous one, use exportChatInviteLink again. + #[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize)] + pub ExportChatInviteLink (ExportChatInviteLinkSetters) => String { + required { + /// Unique identifier for the target chat or username of the target channel (in the format `@channelusername`) + pub chat_id: Recipient [into], + } + } +} diff --git a/crates/teloxide-core/src/payloads/forward_message.rs b/crates/teloxide-core/src/payloads/forward_message.rs new file mode 100644 index 00000000..1e4c079c --- /dev/null +++ b/crates/teloxide-core/src/payloads/forward_message.rs @@ -0,0 +1,31 @@ +//! Generated by `codegen_payloads`, do not edit by hand. + +use serde::Serialize; + +use crate::types::{Message, MessageId, Recipient}; + +impl_payload! { + /// Use this method to forward messages of any kind. On success, the sent [`Message`] is returned. + /// + /// [`Message`]: crate::types::Message + #[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize)] + pub ForwardMessage (ForwardMessageSetters) => Message { + required { + /// Unique identifier for the target chat or username of the target channel (in the format `@channelusername`) + pub chat_id: Recipient [into], + /// Unique identifier for the chat where the original message was sent (or channel username in the format `@channelusername`) + pub from_chat_id: Recipient [into], + /// Message identifier in the chat specified in _from\_chat\_id_ + #[serde(flatten)] + pub message_id: MessageId, + } + optional { + /// Sends the message [silently]. Users will receive a notification with no sound. + /// + /// [silently]: https://telegram.org/blog/channels-2-0#silent-messages + pub disable_notification: bool, + /// Protects the contents of sent messages from forwarding and saving + pub protect_content: bool, + } + } +} diff --git a/crates/teloxide-core/src/payloads/get_chat.rs b/crates/teloxide-core/src/payloads/get_chat.rs new file mode 100644 index 00000000..1d42f1f9 --- /dev/null +++ b/crates/teloxide-core/src/payloads/get_chat.rs @@ -0,0 +1,18 @@ +//! Generated by `codegen_payloads`, do not edit by hand. + +use serde::Serialize; + +use crate::types::{Chat, Recipient}; + +impl_payload! { + /// Use this method to get up to date information about the chat (current name of the user for one-on-one conversations, current username of a user, group or channel, etc.). Returns a [`Chat`] object on success. + /// + /// [`Chat`]: crate::types::Chat + #[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize)] + pub GetChat (GetChatSetters) => Chat { + required { + /// Unique identifier for the target chat or username of the target channel (in the format `@channelusername`) + pub chat_id: Recipient [into], + } + } +} diff --git a/crates/teloxide-core/src/payloads/get_chat_administrators.rs b/crates/teloxide-core/src/payloads/get_chat_administrators.rs new file mode 100644 index 00000000..1ad2f5ea --- /dev/null +++ b/crates/teloxide-core/src/payloads/get_chat_administrators.rs @@ -0,0 +1,18 @@ +//! Generated by `codegen_payloads`, do not edit by hand. + +use serde::Serialize; + +use crate::types::{ChatMember, Recipient}; + +impl_payload! { + /// Use this method to get a list of administrators in a chat. On success, returns an Array of [`ChatMember`] objects that contains information about all chat administrators except other bots. If the chat is a group or a supergroup and no administrators were appointed, only the creator will be returned. + /// + /// [`ChatMember`]: crate::types::ChatMember + #[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize)] + pub GetChatAdministrators (GetChatAdministratorsSetters) => Vec { + required { + /// Unique identifier for the target chat or username of the target channel (in the format `@channelusername`) + pub chat_id: Recipient [into], + } + } +} diff --git a/crates/teloxide-core/src/payloads/get_chat_member.rs b/crates/teloxide-core/src/payloads/get_chat_member.rs new file mode 100644 index 00000000..ff469de9 --- /dev/null +++ b/crates/teloxide-core/src/payloads/get_chat_member.rs @@ -0,0 +1,20 @@ +//! Generated by `codegen_payloads`, do not edit by hand. + +use serde::Serialize; + +use crate::types::{ChatMember, Recipient, UserId}; + +impl_payload! { + /// Use this method to get information about a member of a chat. Returns a [`ChatMember`] object on success. + /// + /// [`ChatMember`]: crate::types::ChatMember + #[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize)] + pub GetChatMember (GetChatMemberSetters) => ChatMember { + required { + /// Unique identifier for the target chat or username of the target channel (in the format `@channelusername`) + pub chat_id: Recipient [into], + /// Unique identifier of the target user + pub user_id: UserId, + } + } +} diff --git a/crates/teloxide-core/src/payloads/get_chat_member_count.rs b/crates/teloxide-core/src/payloads/get_chat_member_count.rs new file mode 100644 index 00000000..f481fe84 --- /dev/null +++ b/crates/teloxide-core/src/payloads/get_chat_member_count.rs @@ -0,0 +1,16 @@ +//! Generated by `codegen_payloads`, do not edit by hand. + +use serde::Serialize; + +use crate::types::Recipient; + +impl_payload! { + /// Use this method to get the number of members in a chat. Returns _Int_ on success. + #[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize)] + pub GetChatMemberCount (GetChatMemberCountSetters) => u32 { + required { + /// Unique identifier for the target chat or username of the target channel (in the format `@channelusername`) + pub chat_id: Recipient [into], + } + } +} diff --git a/crates/teloxide-core/src/payloads/get_chat_members_count.rs b/crates/teloxide-core/src/payloads/get_chat_members_count.rs new file mode 100644 index 00000000..ed598b2d --- /dev/null +++ b/crates/teloxide-core/src/payloads/get_chat_members_count.rs @@ -0,0 +1,16 @@ +//! Generated by `codegen_payloads`, do not edit by hand. + +use serde::Serialize; + +use crate::types::Recipient; + +impl_payload! { + /// Use this method to get the number of members in a chat. Returns _Int_ on success. + #[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize)] + pub GetChatMembersCount (GetChatMembersCountSetters) => u32 { + required { + /// Unique identifier for the target chat or username of the target channel (in the format `@channelusername`) + pub chat_id: Recipient [into], + } + } +} diff --git a/crates/teloxide-core/src/payloads/get_chat_menu_button.rs b/crates/teloxide-core/src/payloads/get_chat_menu_button.rs new file mode 100644 index 00000000..32a34821 --- /dev/null +++ b/crates/teloxide-core/src/payloads/get_chat_menu_button.rs @@ -0,0 +1,16 @@ +//! Generated by `codegen_payloads`, do not edit by hand. + +use serde::Serialize; + +use crate::types::{ChatId, MenuButton}; + +impl_payload! { + /// Use this method to get the current value of the bot's menu button in a private chat, or the default menu button. + #[derive(Debug, PartialEq, Eq, Hash, Default, Clone, Serialize)] + pub GetChatMenuButton (GetChatMenuButtonSetters) => MenuButton { + optional { + /// Unique identifier for the target private chat. If not specified, default bot's menu button will be returned + pub chat_id: ChatId [into], + } + } +} diff --git a/crates/teloxide-core/src/payloads/get_custom_emoji_stickers.rs b/crates/teloxide-core/src/payloads/get_custom_emoji_stickers.rs new file mode 100644 index 00000000..68cedfff --- /dev/null +++ b/crates/teloxide-core/src/payloads/get_custom_emoji_stickers.rs @@ -0,0 +1,16 @@ +//! Generated by `codegen_payloads`, do not edit by hand. + +use serde::Serialize; + +use crate::types::Sticker; + +impl_payload! { + /// Use this method to get information about custom emoji stickers by their identifiers. Returns an Array of Sticker objects. + #[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize)] + pub GetCustomEmojiStickers (GetCustomEmojiStickersSetters) => Vec { + required { + /// List of custom emoji identifiers. At most 200 custom emoji identifiers can be specified. + pub custom_emoji_ids: Vec [collect], + } + } +} diff --git a/crates/teloxide-core/src/payloads/get_file.rs b/crates/teloxide-core/src/payloads/get_file.rs new file mode 100644 index 00000000..20a4b53e --- /dev/null +++ b/crates/teloxide-core/src/payloads/get_file.rs @@ -0,0 +1,19 @@ +//! Generated by `codegen_payloads`, do not edit by hand. + +use serde::Serialize; + +use crate::types::File; + +impl_payload! { + /// Use this method to get basic info about a file and prepare it for downloading. For the moment, bots can download files of up to 20MB in size. On success, a [`File`] object is returned. The file can then be downloaded via the link `https://api.telegram.org/file/bot/`, where `` is taken from the response. It is guaranteed that the link will be valid for at least 1 hour. When the link expires, a new one can be requested by calling [`GetFile`] again. + /// + /// [`File`]: crate::types::File + /// [`GetFile`]: crate::payloads::GetFile + #[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize)] + pub GetFile (GetFileSetters) => File { + required { + /// File identifier to get info about + pub file_id: String [into], + } + } +} diff --git a/crates/teloxide-core/src/payloads/get_game_high_scores.rs b/crates/teloxide-core/src/payloads/get_game_high_scores.rs new file mode 100644 index 00000000..017104ab --- /dev/null +++ b/crates/teloxide-core/src/payloads/get_game_high_scores.rs @@ -0,0 +1,23 @@ +//! Generated by `codegen_payloads`, do not edit by hand. + +use serde::Serialize; + +use crate::types::{TargetMessage, True, UserId}; + +impl_payload! { + /// Use this method to get data for high score tables. Will return the score of the specified user and several of their neighbors in a game. On success, returns an Array of [`GameHighScore`] objects. + /// + /// > This method will currently return scores for the target user, plus two of their closest neighbors on each side. Will also return the top three users if the user and his neighbors are not among them. Please note that this behavior is subject to change. + /// + /// [`GameHighScore`]: crate::types::GameHighScore + #[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize)] + pub GetGameHighScores (GetGameHighScoresSetters) => True { + required { + /// User identifier + pub user_id: UserId, + /// Target message + #[serde(flatten)] + pub target: TargetMessage [into], + } + } +} diff --git a/crates/teloxide-core/src/payloads/get_me.rs b/crates/teloxide-core/src/payloads/get_me.rs new file mode 100644 index 00000000..07323fba --- /dev/null +++ b/crates/teloxide-core/src/payloads/get_me.rs @@ -0,0 +1,15 @@ +//! Generated by `codegen_payloads`, do not edit by hand. + +use serde::Serialize; + +use crate::types::Me; + +impl_payload! { + /// A simple method for testing your bot's auth token. Requires no parameters. Returns basic information about the bot in form of a [`User`] object. + /// + /// [`User`]: crate::types::User + #[derive(Debug, PartialEq, Eq, Hash, Default, Clone, Serialize)] + pub GetMe (GetMeSetters) => Me { + + } +} diff --git a/crates/teloxide-core/src/payloads/get_my_commands.rs b/crates/teloxide-core/src/payloads/get_my_commands.rs new file mode 100644 index 00000000..387da9a5 --- /dev/null +++ b/crates/teloxide-core/src/payloads/get_my_commands.rs @@ -0,0 +1,20 @@ +//! Generated by `codegen_payloads`, do not edit by hand. + +use serde::Serialize; + +use crate::types::{BotCommand, BotCommandScope}; + +impl_payload! { + /// Use this method to get the current list of the bot's commands. Requires no parameters. Returns Array of [`BotCommand`] on success. + /// + /// [`BotCommand`]: crate::types::BotCommand + #[derive(Debug, PartialEq, Eq, Hash, Default, Clone, Serialize)] + pub GetMyCommands (GetMyCommandsSetters) => Vec { + optional { + /// A JSON-serialized object, describing scope of users for which the commands are relevant. Defaults to BotCommandScopeDefault. + pub scope: BotCommandScope, + /// A two-letter ISO 639-1 language code. If empty, commands will be applied to all users from the given scope, for whose language there are no dedicated commands + pub language_code: String [into], + } + } +} diff --git a/crates/teloxide-core/src/payloads/get_my_default_administrator_rights.rs b/crates/teloxide-core/src/payloads/get_my_default_administrator_rights.rs new file mode 100644 index 00000000..867d145f --- /dev/null +++ b/crates/teloxide-core/src/payloads/get_my_default_administrator_rights.rs @@ -0,0 +1,16 @@ +//! Generated by `codegen_payloads`, do not edit by hand. + +use serde::Serialize; + +use crate::types::ChatAdministratorRights; + +impl_payload! { + /// Use this method to get the current value of the bot's menu button in a private chat, or the default menu button. + #[derive(Debug, PartialEq, Eq, Hash, Default, Clone, Serialize)] + pub GetMyDefaultAdministratorRights (GetMyDefaultAdministratorRightsSetters) => ChatAdministratorRights { + optional { + /// Pass _True_ to get default administrator rights of the bot in channels. Otherwise, default administrator rights of the bot for groups and supergroups will be returned. + pub for_channels: bool, + } + } +} diff --git a/crates/teloxide-core/src/payloads/get_sticker_set.rs b/crates/teloxide-core/src/payloads/get_sticker_set.rs new file mode 100644 index 00000000..a750a58e --- /dev/null +++ b/crates/teloxide-core/src/payloads/get_sticker_set.rs @@ -0,0 +1,16 @@ +//! Generated by `codegen_payloads`, do not edit by hand. + +use serde::Serialize; + +use crate::types::StickerSet; + +impl_payload! { + /// Use this method to get a sticker set. On success, a StickerSet object is returned. + #[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize)] + pub GetStickerSet (GetStickerSetSetters) => StickerSet { + required { + /// Name of the sticker set + pub name: String [into], + } + } +} diff --git a/crates/teloxide-core/src/payloads/get_updates.rs b/crates/teloxide-core/src/payloads/get_updates.rs new file mode 100644 index 00000000..43340c9b --- /dev/null +++ b/crates/teloxide-core/src/payloads/get_updates.rs @@ -0,0 +1,32 @@ +//! Generated by `codegen_payloads`, do not edit by hand. + +use serde::Serialize; + +use crate::types::{AllowedUpdate, Update}; + +impl_payload! { + @[timeout_secs = timeout] + /// Use this method to receive incoming updates using long polling ([wiki]). An Array of [`Update`] objects is returned. + /// + /// [wiki]: https://en.wikipedia.org/wiki/Push_technology#Long_polling + /// [`Update`]: crate::types::Update + #[derive(Debug, PartialEq, Eq, Hash, Default, Clone, Serialize)] + pub GetUpdates (GetUpdatesSetters) => Vec { + optional { + /// Identifier of the first update to be returned. Must be greater by one than the highest among the identifiers of previously received updates. By default, updates starting with the earliest unconfirmed update are returned. An update is considered confirmed as soon as [`GetUpdates`] is called with an offset higher than its update_id. The negative offset can be specified to retrieve updates starting from -offset update from the end of the updates queue. All previous updates will forgotten. + /// + /// [`GetUpdates`]: crate::payloads::GetUpdates + pub offset: i32, + /// Limits the number of updates to be retrieved. Values between 1-100 are accepted. Defaults to 100. + pub limit: u8, + /// Timeout in seconds for long polling. Defaults to 0, i.e. usual short polling. Should be positive, short polling should be used for testing purposes only. + pub timeout: u32, + /// A JSON-serialized list of the update types you want your bot to receive. For example, specify [“message”, “edited_channel_post”, “callback_query”] to only receive updates of these types. See [`Update`] for a complete list of available update types. Specify an empty list to receive all update types except chat_member (default). If not specified, the previous setting will be used. + /// + /// Please note that this parameter doesn't affect updates created before the call to the getUpdates, so unwanted updates may be received for a short period of time. + /// + /// [`Update`]: crate::types::Update + pub allowed_updates: Vec [collect], + } + } +} diff --git a/crates/teloxide-core/src/payloads/get_user_profile_photos.rs b/crates/teloxide-core/src/payloads/get_user_profile_photos.rs new file mode 100644 index 00000000..272eb71a --- /dev/null +++ b/crates/teloxide-core/src/payloads/get_user_profile_photos.rs @@ -0,0 +1,24 @@ +//! Generated by `codegen_payloads`, do not edit by hand. + +use serde::Serialize; + +use crate::types::{UserId, UserProfilePhotos}; + +impl_payload! { + /// Use this method to get a list of profile pictures for a user. Returns a [`UserProfilePhotos`] object. + /// + /// [`UserProfilePhotos`]: crate::types::UserProfilePhotos + #[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize)] + pub GetUserProfilePhotos (GetUserProfilePhotosSetters) => UserProfilePhotos { + required { + /// Unique identifier of the target user + pub user_id: UserId, + } + optional { + /// Sequential number of the first photo to be returned. By default, all photos are returned. + pub offset: u32, + /// Limits the number of photos to be retrieved. Values between 1-100 are accepted. Defaults to 100. + pub limit: u8, + } + } +} diff --git a/crates/teloxide-core/src/payloads/get_webhook_info.rs b/crates/teloxide-core/src/payloads/get_webhook_info.rs new file mode 100644 index 00000000..bd8ace2e --- /dev/null +++ b/crates/teloxide-core/src/payloads/get_webhook_info.rs @@ -0,0 +1,16 @@ +//! Generated by `codegen_payloads`, do not edit by hand. + +use serde::Serialize; + +use crate::types::WebhookInfo; + +impl_payload! { + /// Use this method to get current webhook status. Requires no parameters. On success, returns a [`WebhookInfo`] object. If the bot is using [`GetUpdates`], will return an object with the _url_ field empty. + /// + /// [`WebhookInfo`]: crate::types::WebhookInfo + /// [`GetUpdates`]: crate::payloads::GetUpdates + #[derive(Debug, PartialEq, Eq, Hash, Default, Clone, Serialize)] + pub GetWebhookInfo (GetWebhookInfoSetters) => WebhookInfo { + + } +} diff --git a/crates/teloxide-core/src/payloads/kick_chat_member.rs b/crates/teloxide-core/src/payloads/kick_chat_member.rs new file mode 100644 index 00000000..87193da5 --- /dev/null +++ b/crates/teloxide-core/src/payloads/kick_chat_member.rs @@ -0,0 +1,28 @@ +//! Generated by `codegen_payloads`, do not edit by hand. + +use chrono::{DateTime, Utc}; +use serde::Serialize; + +use crate::types::{Recipient, True, UserId}; + +impl_payload! { + /// Use this method to kick a user from a group, a supergroup or a channel. In the case of supergroups and channels, the user will not be able to return to the group on their own using invite links, etc., unless [unbanned] first. The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. Returns _True_ on success. + /// + /// [unbanned]: crate::payloads::UnbanChatMember + #[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize)] + pub KickChatMember (KickChatMemberSetters) => True { + required { + /// Unique identifier for the target chat or username of the target channel (in the format `@channelusername`) + pub chat_id: Recipient [into], + /// Unique identifier of the target user + pub user_id: UserId, + } + optional { + /// Date when the user will be unbanned, unix time. If user is banned for more than 366 days or less than 30 seconds from the current time they are considered to be banned forever + #[serde(with = "crate::types::serde_opt_date_from_unix_timestamp")] + pub until_date: DateTime [into], + /// Pass True to delete all messages from the chat for the user that is being removed. If False, the user will be able to see messages in the group that were sent before the user was removed. Always True for supergroups and channels. + pub revoke_messages: bool, + } + } +} diff --git a/crates/teloxide-core/src/payloads/leave_chat.rs b/crates/teloxide-core/src/payloads/leave_chat.rs new file mode 100644 index 00000000..cafd0af3 --- /dev/null +++ b/crates/teloxide-core/src/payloads/leave_chat.rs @@ -0,0 +1,16 @@ +//! Generated by `codegen_payloads`, do not edit by hand. + +use serde::Serialize; + +use crate::types::{Recipient, True}; + +impl_payload! { + /// Use this method for your bot to leave a group, supergroup or channel. Returns _True_ on success. + #[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize)] + pub LeaveChat (LeaveChatSetters) => True { + required { + /// Unique identifier for the target chat or username of the target channel (in the format `@channelusername`) + pub chat_id: Recipient [into], + } + } +} diff --git a/crates/teloxide-core/src/payloads/log_out.rs b/crates/teloxide-core/src/payloads/log_out.rs new file mode 100644 index 00000000..a138f45a --- /dev/null +++ b/crates/teloxide-core/src/payloads/log_out.rs @@ -0,0 +1,13 @@ +//! Generated by `codegen_payloads`, do not edit by hand. + +use serde::Serialize; + +use crate::types::True; + +impl_payload! { + /// Use this method to log out from the cloud Bot API server before launching the bot locally. You **must** log out the bot before running it locally, otherwise there is no guarantee that the bot will receive updates. After a successful call, you can immediately log in on a local server, but will not be able to log in back to the cloud Bot API server for 10 minutes. Returns _True_ on success. Requires no parameters. + #[derive(Debug, PartialEq, Eq, Hash, Default, Clone, Serialize)] + pub LogOut (LogOutSetters) => True { + + } +} diff --git a/crates/teloxide-core/src/payloads/pin_chat_message.rs b/crates/teloxide-core/src/payloads/pin_chat_message.rs new file mode 100644 index 00000000..fd217bac --- /dev/null +++ b/crates/teloxide-core/src/payloads/pin_chat_message.rs @@ -0,0 +1,23 @@ +//! Generated by `codegen_payloads`, do not edit by hand. + +use serde::Serialize; + +use crate::types::{MessageId, Recipient, True}; + +impl_payload! { + /// Use this method to pin a message in a group, a supergroup, or a channel. The bot must be an administrator in the chat for this to work and must have the 'can_pin_messages' admin right in the supergroup or 'can_edit_messages' admin right in the channel. Returns _True_ on success. + #[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize)] + pub PinChatMessage (PinChatMessageSetters) => True { + required { + /// Unique identifier for the target chat or username of the target channel (in the format `@channelusername`) + pub chat_id: Recipient [into], + /// Identifier of a message to pin + #[serde(flatten)] + pub message_id: MessageId, + } + optional { + /// Pass True, if it is not necessary to send a notification to all chat members about the new pinned message. Notifications are always disabled in channels. + pub disable_notification: bool, + } + } +} diff --git a/crates/teloxide-core/src/payloads/promote_chat_member.rs b/crates/teloxide-core/src/payloads/promote_chat_member.rs new file mode 100644 index 00000000..ece800f6 --- /dev/null +++ b/crates/teloxide-core/src/payloads/promote_chat_member.rs @@ -0,0 +1,42 @@ +//! Generated by `codegen_payloads`, do not edit by hand. + +use serde::Serialize; + +use crate::types::{Recipient, True, UserId}; + +impl_payload! { + /// Use this method to promote or demote a user in a supergroup or a channel. The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. Pass _False_ for all boolean parameters to demote a user. Returns _True_ on success. + #[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize)] + pub PromoteChatMember (PromoteChatMemberSetters) => True { + required { + /// Unique identifier for the target chat or username of the target channel (in the format `@channelusername`) + pub chat_id: Recipient [into], + /// Unique identifier of the target user + pub user_id: UserId, + } + optional { + /// Pass True, if the administrator's presence in the chat is hidden + pub is_anonymous: bool, + /// Pass True, if the administrator can access the chat event log, chat statistics, message statistics in channels, see channel members, see anonymous administrators in supergroups and ignore slow mode. Implied by any other administrator privilege + pub can_manage_chat: bool, + /// Pass True, if the administrator can change chat title, photo and other settings + pub can_change_info: bool, + /// Pass True, if the administrator can create channel posts, channels only + pub can_post_messages: bool, + /// Pass True, if the administrator can edit messages of other users and can pin messages, channels only + pub can_edit_messages: bool, + /// Pass True, if the administrator can delete messages of other users + pub can_delete_messages: bool, + /// Pass True, if the administrator can manage video chats, supergroups only + pub can_manage_video_chats: bool, + /// Pass True, if the administrator can invite new users to the chat + pub can_invite_users: bool, + /// Pass True, if the administrator can restrict, ban or unban chat members + pub can_restrict_members: bool, + /// Pass True, if the administrator can pin messages, supergroups only + pub can_pin_messages: bool, + /// Pass True, if the administrator can add new administrators with a subset of their own privileges or demote administrators that he has promoted, directly or indirectly (promoted by administrators that were appointed by him) + pub can_promote_members: bool, + } + } +} diff --git a/crates/teloxide-core/src/payloads/restrict_chat_member.rs b/crates/teloxide-core/src/payloads/restrict_chat_member.rs new file mode 100644 index 00000000..282af1dd --- /dev/null +++ b/crates/teloxide-core/src/payloads/restrict_chat_member.rs @@ -0,0 +1,26 @@ +//! Generated by `codegen_payloads`, do not edit by hand. + +use chrono::{DateTime, Utc}; +use serde::Serialize; + +use crate::types::{ChatPermissions, Recipient, True, UserId}; + +impl_payload! { + /// Use this method to restrict a user in a supergroup. The bot must be an administrator in the supergroup for this to work and must have the appropriate admin rights. Pass _True_ for all permissions to lift restrictions from a user. Returns _True_ on success. + #[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize)] + pub RestrictChatMember (RestrictChatMemberSetters) => True { + required { + /// Unique identifier for the target chat or username of the target channel (in the format `@channelusername`) + pub chat_id: Recipient [into], + /// Unique identifier of the target user + pub user_id: UserId, + /// A JSON-serialized object for new user permissions + pub permissions: ChatPermissions, + } + optional { + /// Date when the user will be unbanned, unix time. If user is banned for more than 366 days or less than 30 seconds from the current time they are considered to be banned forever + #[serde(with = "crate::types::serde_opt_date_from_unix_timestamp")] + pub until_date: DateTime [into], + } + } +} diff --git a/crates/teloxide-core/src/payloads/revoke_chat_invite_link.rs b/crates/teloxide-core/src/payloads/revoke_chat_invite_link.rs new file mode 100644 index 00000000..9d1b5085 --- /dev/null +++ b/crates/teloxide-core/src/payloads/revoke_chat_invite_link.rs @@ -0,0 +1,20 @@ +//! Generated by `codegen_payloads`, do not edit by hand. + +use serde::Serialize; + +use crate::types::Recipient; + +impl_payload! { + /// Use this method to revoke an invite link created by the bot. If the primary link is revoked, a new link is automatically generated. The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. Returns the revoked invite link as [`ChatInviteLink`] object. + /// + /// [`ChatInviteLink`]: crate::types::ChatInviteLink + #[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize)] + pub RevokeChatInviteLink (RevokeChatInviteLinkSetters) => String { + required { + /// Unique identifier for the target chat or username of the target channel (in the format `@channelusername`) + pub chat_id: Recipient [into], + /// The invite link to revoke + pub invite_link: String [into], + } + } +} diff --git a/crates/teloxide-core/src/payloads/send_animation.rs b/crates/teloxide-core/src/payloads/send_animation.rs new file mode 100644 index 00000000..118e1044 --- /dev/null +++ b/crates/teloxide-core/src/payloads/send_animation.rs @@ -0,0 +1,61 @@ +//! Generated by `codegen_payloads`, do not edit by hand. + +use serde::Serialize; + +use crate::types::{ + InputFile, Message, MessageEntity, MessageId, ParseMode, Recipient, ReplyMarkup, +}; + +impl_payload! { + @[multipart = animation, thumb] + /// Use this method to send animation files (GIF or H.264/MPEG-4 AVC video without sound). On success, the sent [`Message`] is returned. Bots can currently send animation files of up to 50 MB in size, this limit may be changed in the future. + /// + /// [`Message`]: crate::types::Message + #[derive(Debug, Clone, Serialize)] + pub SendAnimation (SendAnimationSetters) => Message { + required { + /// Unique identifier for the target chat or username of the target channel (in the format `@channelusername`) + pub chat_id: Recipient [into], + /// Animation to send. Pass a file_id as String to send a video that exists on the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get a video from the Internet, or upload a new video using multipart/form-data. [More info on Sending Files »] + /// + /// [More info on Sending Files »]: crate::types::InputFile + pub animation: InputFile, + } + optional { + /// Duration of the animation in seconds + pub duration: u32, + /// Animation width + pub width: u32, + /// Animation height + pub height: u32, + /// Thumbnail of the file sent; can be ignored if thumbnail generation for the file is supported server-side. The thumbnail should be in JPEG format and less than 200 kB in size. A thumbnail's width and height should not exceed 320. Ignored if the file is not uploaded using multipart/form-data. Thumbnails can't be reused and can be only uploaded as a new file, so you can pass “attach://” if the thumbnail was uploaded using multipart/form-data under . [More info on Sending Files »] + /// + /// [More info on Sending Files »]: crate::types::InputFile + pub thumb: InputFile, + /// Animation caption (may also be used when resending videos by _file\_id_), 0-1024 characters after entities parsing + pub caption: String [into], + /// Mode for parsing entities in the animation caption. See [formatting options] for more details. + /// + /// [formatting options]: https://core.telegram.org/bots/api#formatting-options + pub parse_mode: ParseMode, + /// List of special entities that appear in the photo caption, which can be specified instead of _parse\_mode_ + pub caption_entities: Vec [collect], + /// Sends the message [silently]. Users will receive a notification with no sound. + /// + /// [silently]: https://telegram.org/blog/channels-2-0#silent-messages + pub disable_notification: bool, + /// Protects the contents of sent messages from forwarding and saving + pub protect_content: bool, + /// If the message is a reply, ID of the original message + #[serde(serialize_with = "crate::types::serialize_reply_to_message_id")] + pub reply_to_message_id: MessageId, + /// Pass _True_, if the message should be sent even if the specified replied-to message is not found + pub allow_sending_without_reply: bool, + /// Additional interface options. A JSON-serialized object for an [inline keyboard], [custom reply keyboard], instructions to remove reply keyboard or to force a reply from the user. + /// + /// [inline keyboard]: https://core.telegram.org/bots#inline-keyboards-and-on-the-fly-updating + /// [custom reply keyboard]: https://core.telegram.org/bots#keyboards + pub reply_markup: ReplyMarkup [into], + } + } +} diff --git a/crates/teloxide-core/src/payloads/send_audio.rs b/crates/teloxide-core/src/payloads/send_audio.rs new file mode 100644 index 00000000..05b5c6ca --- /dev/null +++ b/crates/teloxide-core/src/payloads/send_audio.rs @@ -0,0 +1,64 @@ +//! Generated by `codegen_payloads`, do not edit by hand. + +use serde::Serialize; + +use crate::types::{ + InputFile, Message, MessageEntity, MessageId, ParseMode, Recipient, ReplyMarkup, +}; + +impl_payload! { + @[multipart = audio, thumb] + /// Use this method to send audio files, if you want Telegram clients to display them in the music player. Your audio must be in the .MP3 or .M4A format. On success, the sent [`Message`] is returned. Bots can currently send audio files of up to 50 MB in size, this limit may be changed in the future. + /// + /// For sending voice messages, use the [`SendVoice`] method instead. + /// + /// [`Message`]: crate::types::Message + /// [`SendVoice`]: crate::payloads::SendVoice + #[derive(Debug, Clone, Serialize)] + pub SendAudio (SendAudioSetters) => Message { + required { + /// Unique identifier for the target chat or username of the target channel (in the format `@channelusername`) + pub chat_id: Recipient [into], + /// Audio file to send. Pass a file_id as String to send an audio file that exists on the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get an audio file from the Internet, or upload a new one using multipart/form-data. [More info on Sending Files »] + /// + /// [More info on Sending Files »]: crate::types::InputFile + pub audio: InputFile, + } + optional { + /// Audio caption, 0-1024 characters after entities parsing + pub caption: String [into], + /// Mode for parsing entities in the audio caption. See [formatting options] for more details. + /// + /// [formatting options]: https://core.telegram.org/bots/api#formatting-options + pub parse_mode: ParseMode, + /// List of special entities that appear in the photo caption, which can be specified instead of _parse\_mode_ + pub caption_entities: Vec [collect], + /// Duration of the audio in seconds + pub duration: u32, + /// Performer + pub performer: String [into], + /// Track name + pub title: String [into], + /// Thumbnail of the file sent; can be ignored if thumbnail generation for the file is supported server-side. The thumbnail should be in JPEG format and less than 200 kB in size. A thumbnail's width and height should not exceed 320. Ignored if the file is not uploaded using multipart/form-data. Thumbnails can't be reused and can be only uploaded as a new file, so you can pass “attach://” if the thumbnail was uploaded using multipart/form-data under . [More info on Sending Files »] + /// + /// [More info on Sending Files »]: crate::types::InputFile + pub thumb: InputFile, + /// Sends the message [silently]. Users will receive a notification with no sound. + /// + /// [silently]: https://telegram.org/blog/channels-2-0#silent-messages + pub disable_notification: bool, + /// Protects the contents of sent messages from forwarding and saving + pub protect_content: bool, + /// If the message is a reply, ID of the original message + #[serde(serialize_with = "crate::types::serialize_reply_to_message_id")] + pub reply_to_message_id: MessageId, + /// Pass _True_, if the message should be sent even if the specified replied-to message is not found + pub allow_sending_without_reply: bool, + /// Additional interface options. A JSON-serialized object for an [inline keyboard], [custom reply keyboard], instructions to remove reply keyboard or to force a reply from the user. + /// + /// [inline keyboard]: https://core.telegram.org/bots#inline-keyboards-and-on-the-fly-updating + /// [custom reply keyboard]: https://core.telegram.org/bots#keyboards + pub reply_markup: ReplyMarkup [into], + } + } +} diff --git a/crates/teloxide-core/src/payloads/send_chat_action.rs b/crates/teloxide-core/src/payloads/send_chat_action.rs new file mode 100644 index 00000000..a3acd43e --- /dev/null +++ b/crates/teloxide-core/src/payloads/send_chat_action.rs @@ -0,0 +1,33 @@ +//! Generated by `codegen_payloads`, do not edit by hand. + +use serde::Serialize; + +use crate::types::{ChatAction, Recipient, True}; + +impl_payload! { + /// Use this method when you need to tell the user that something is happening on the bot's side. The status is set for 5 seconds or less (when a message arrives from your bot, Telegram clients clear its typing status). Returns True on success. + /// + /// > Example: The [ImageBot] needs some time to process a request and upload the image. Instead of sending a text message along the lines of “Retrieving image, please wait…”, the bot may use sendChatAction with action = upload_photo. The user will see a “sending photo” status for the bot. + /// + /// We only recommend using this method when a response from the bot will take a **noticeable** amount of time to arrive. + /// + /// [ImageBot]: https://t.me/imagebot + #[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize)] + pub SendChatAction (SendChatActionSetters) => True { + required { + /// Unique identifier for the target chat or username of the target channel (in the format `@channelusername`) + pub chat_id: Recipient [into], + /// Type of action to broadcast. Choose one, depending on what the user is about to receive: typing for [text messages], upload_photo for [photos], record_video or upload_video for [videos], record_audio or upload_audio for [audio files], upload_document for [general files], choose_sticker for [stickers], find_location for [location data], record_video_note or upload_video_note for [video notes]. + /// + /// [text messages]: crate::payloads::SendMessage + /// [photos]: crate::payloads::SendPhoto + /// [videos]: crate::payloads::SendVideo + /// [audio files]: crate::payloads::SendAudio + /// [general files]: crate::payloads::SendDocument + /// [stickers]: crate::payloads::SendSticker + /// [location data]: crate::payloads::SendLocation + /// [video notes]: crate::payloads::SendVideoNote + pub action: ChatAction, + } + } +} diff --git a/crates/teloxide-core/src/payloads/send_contact.rs b/crates/teloxide-core/src/payloads/send_contact.rs new file mode 100644 index 00000000..d96b1faa --- /dev/null +++ b/crates/teloxide-core/src/payloads/send_contact.rs @@ -0,0 +1,46 @@ +//! Generated by `codegen_payloads`, do not edit by hand. + +use serde::Serialize; + +use crate::types::{Message, MessageId, Recipient, ReplyMarkup}; + +impl_payload! { + /// Use this method to send phone contacts. On success, the sent [`Message`] is returned. + /// + /// [`Message`]: crate::types::Message + #[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize)] + pub SendContact (SendContactSetters) => Message { + required { + /// Unique identifier for the target chat or username of the target channel (in the format `@channelusername`) + pub chat_id: Recipient [into], + /// Contact's phone number + pub phone_number: String [into], + /// Contact's first name + pub first_name: String [into], + } + optional { + /// Contact's last name + pub last_name: String [into], + /// Additional data about the contact in the form of a [vCard], 0-2048 bytes + /// + /// [vCard]: https://en.wikipedia.org/wiki/VCard + pub vcard: String [into], + /// Sends the message [silently]. Users will receive a notification with no sound. + /// + /// [silently]: https://telegram.org/blog/channels-2-0#silent-messages + pub disable_notification: bool, + /// Protects the contents of sent messages from forwarding and saving + pub protect_content: bool, + /// If the message is a reply, ID of the original message + #[serde(serialize_with = "crate::types::serialize_reply_to_message_id")] + pub reply_to_message_id: MessageId, + /// Pass _True_, if the message should be sent even if the specified replied-to message is not found + pub allow_sending_without_reply: bool, + /// Additional interface options. A JSON-serialized object for an [inline keyboard], [custom reply keyboard], instructions to remove reply keyboard or to force a reply from the user. + /// + /// [inline keyboard]: https://core.telegram.org/bots#inline-keyboards-and-on-the-fly-updating + /// [custom reply keyboard]: https://core.telegram.org/bots#keyboards + pub reply_markup: ReplyMarkup [into], + } + } +} diff --git a/crates/teloxide-core/src/payloads/send_dice.rs b/crates/teloxide-core/src/payloads/send_dice.rs new file mode 100644 index 00000000..5a673cb9 --- /dev/null +++ b/crates/teloxide-core/src/payloads/send_dice.rs @@ -0,0 +1,38 @@ +//! Generated by `codegen_payloads`, do not edit by hand. + +use serde::Serialize; + +use crate::types::{DiceEmoji, Message, MessageId, Recipient, ReplyMarkup}; + +impl_payload! { + /// Use this method to send an animated emoji that will display a random value. On success, the sent [`Message`] is returned. + /// + /// [`Message`]: crate::types::Message + #[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize)] + pub SendDice (SendDiceSetters) => Message { + required { + /// Unique identifier for the target chat or username of the target channel (in the format `@channelusername`) + pub chat_id: Recipient [into], + } + optional { + /// Emoji on which the dice throw animation is based. Currently, must be one of “🎲”, “🎯”, “🏀”, “⚽”, “🎳”, or “🎰”. Dice can have values 1-6 for “🎲”, “🎯” and “🎳”, values 1-5 for “🏀” and “⚽”, and values 1-64 for “🎰”. Defaults to “🎲” + pub emoji: DiceEmoji, + /// Sends the message [silently]. Users will receive a notification with no sound. + /// + /// [silently]: https://telegram.org/blog/channels-2-0#silent-messages + pub disable_notification: bool, + /// Protects the contents of sent messages from forwarding and saving + pub protect_content: bool, + /// If the message is a reply, ID of the original message + #[serde(serialize_with = "crate::types::serialize_reply_to_message_id")] + pub reply_to_message_id: MessageId, + /// Pass _True_, if the message should be sent even if the specified replied-to message is not found + pub allow_sending_without_reply: bool, + /// Additional interface options. A JSON-serialized object for an [inline keyboard], [custom reply keyboard], instructions to remove reply keyboard or to force a reply from the user. + /// + /// [inline keyboard]: https://core.telegram.org/bots#inline-keyboards-and-on-the-fly-updating + /// [custom reply keyboard]: https://core.telegram.org/bots#keyboards + pub reply_markup: ReplyMarkup [into], + } + } +} diff --git a/crates/teloxide-core/src/payloads/send_document.rs b/crates/teloxide-core/src/payloads/send_document.rs new file mode 100644 index 00000000..aaa684de --- /dev/null +++ b/crates/teloxide-core/src/payloads/send_document.rs @@ -0,0 +1,57 @@ +//! Generated by `codegen_payloads`, do not edit by hand. + +use serde::Serialize; + +use crate::types::{ + InputFile, Message, MessageEntity, MessageId, ParseMode, Recipient, ReplyMarkup, +}; + +impl_payload! { + @[multipart = document, thumb] + /// Use this method to send general files. On success, the sent [`Message`] is returned. Bots can currently send files of any type of up to 50 MB in size, this limit may be changed in the future. + /// + /// [`Message`]: crate::types::Message + #[derive(Debug, Clone, Serialize)] + pub SendDocument (SendDocumentSetters) => Message { + required { + /// Unique identifier for the target chat or username of the target channel (in the format `@channelusername`) + pub chat_id: Recipient [into], + /// File to send. Pass a file_id as String to send a file that exists on the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get a file from the Internet, or upload a new one using multipart/form-data. [More info on Sending Files »] + /// + /// [More info on Sending Files »]: crate::types::InputFile + pub document: InputFile, + } + optional { + /// Thumbnail of the file sent; can be ignored if thumbnail generation for the file is supported server-side. The thumbnail should be in JPEG format and less than 200 kB in size. A thumbnail's width and height should not exceed 320. Ignored if the file is not uploaded using multipart/form-data. Thumbnails can't be reused and can be only uploaded as a new file, so you can pass “attach://” if the thumbnail was uploaded using multipart/form-data under . [More info on Sending Files »] + /// + /// [More info on Sending Files »]: crate::types::InputFile + pub thumb: InputFile, + /// Document caption (may also be used when resending documents by _file\_id_), 0-1024 characters after entities parsing + pub caption: String [into], + /// Mode for parsing entities in the audio caption. See [formatting options] for more details. + /// + /// [formatting options]: https://core.telegram.org/bots/api#formatting-options + pub parse_mode: ParseMode, + /// List of special entities that appear in the photo caption, which can be specified instead of _parse\_mode_ + pub caption_entities: Vec [collect], + /// Disables automatic server-side content type detection for files uploaded using multipart/form-data. + pub disable_content_type_detection: bool, + /// Sends the message [silently]. Users will receive a notification with no sound. + /// + /// [silently]: https://telegram.org/blog/channels-2-0#silent-messages + pub disable_notification: bool, + /// Protects the contents of sent messages from forwarding and saving + pub protect_content: bool, + /// If the message is a reply, ID of the original message + #[serde(serialize_with = "crate::types::serialize_reply_to_message_id")] + pub reply_to_message_id: MessageId, + /// Pass _True_, if the message should be sent even if the specified replied-to message is not found + pub allow_sending_without_reply: bool, + /// Additional interface options. A JSON-serialized object for an [inline keyboard], [custom reply keyboard], instructions to remove reply keyboard or to force a reply from the user. + /// + /// [inline keyboard]: https://core.telegram.org/bots#inline-keyboards-and-on-the-fly-updating + /// [custom reply keyboard]: https://core.telegram.org/bots#keyboards + pub reply_markup: ReplyMarkup [into], + } + } +} diff --git a/crates/teloxide-core/src/payloads/send_game.rs b/crates/teloxide-core/src/payloads/send_game.rs new file mode 100644 index 00000000..b348bfb3 --- /dev/null +++ b/crates/teloxide-core/src/payloads/send_game.rs @@ -0,0 +1,36 @@ +//! Generated by `codegen_payloads`, do not edit by hand. + +use serde::Serialize; + +use crate::types::{Message, ReplyMarkup}; + +impl_payload! { + /// Use this method to send a game. On success, the sent [`Message`] is returned. + /// + /// [`Message`]: crate::types::Message + #[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize)] + pub SendGame (SendGameSetters) => Message { + required { + /// Unique identifier for the target chat + pub chat_id: u32, + /// Short name of the game, serves as the unique identifier for the game. Set up your games via Botfather. + pub game_short_name: String [into], + } + optional { + /// Sends the message [silently]. Users will receive a notification with no sound. + /// + /// [silently]: https://telegram.org/blog/channels-2-0#silent-messages + pub disable_notification: bool, + /// Protects the contents of sent messages from forwarding and saving + pub protect_content: bool, + /// If the message is a reply, ID of the original message + pub reply_to_message_id: i32, + /// Pass _True_, if the message should be sent even if the specified replied-to message is not found + pub allow_sending_without_reply: bool, + /// A JSON-serialized object for an [inline keyboard]. If empty, one 'Play game_title' button will be shown. If not empty, the first button must launch the game. + /// + /// [inline keyboard]: https://core.telegram.org/bots#inline-keyboards-and-on-the-fly-updating + pub reply_markup: ReplyMarkup [into], + } + } +} diff --git a/crates/teloxide-core/src/payloads/send_invoice.rs b/crates/teloxide-core/src/payloads/send_invoice.rs new file mode 100644 index 00000000..85322d96 --- /dev/null +++ b/crates/teloxide-core/src/payloads/send_invoice.rs @@ -0,0 +1,81 @@ +//! Generated by `codegen_payloads`, do not edit by hand. + +use serde::Serialize; +use url::Url; + +use crate::types::{InlineKeyboardMarkup, LabeledPrice, Message, Recipient}; + +impl_payload! { + /// Use this method to send invoices. On success, the sent [`Message`] is returned. + /// + /// [`Message`]: crate::types::Message + #[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize)] + pub SendInvoice (SendInvoiceSetters) => Message { + required { + /// Unique identifier for the target private chat + pub chat_id: Recipient [into], + /// Product name, 1-32 characters + pub title: String [into], + /// Product description, 1-255 characters + pub description: String [into], + /// Bot-defined invoice payload, 1-128 bytes. This will not be displayed to the user, use for your internal processes. + pub payload: String [into], + /// Payments provider token, obtained via [Botfather] + /// + /// [Botfather]: https://t.me/botfather + pub provider_token: String [into], + /// Three-letter ISO 4217 currency code, see more on currencies + pub currency: String [into], + /// Price breakdown, a JSON-serialized list of components (e.g. product price, tax, discount, delivery cost, delivery tax, bonus, etc.) + pub prices: Vec [collect], + } + optional { + /// The maximum accepted amount for tips in the smallest units of the currency (integer, **not** float/double). For example, for a maximum tip of `US$ 1.45` pass `max_tip_amount = 145`. See the exp parameter in [`currencies.json`], it shows the number of digits past the decimal point for each currency (2 for the majority of currencies). Defaults to 0 + /// + /// [`currencies.json`]: https://core.telegram.org/bots/payments/currencies.json + pub max_tip_amount: u32, + /// A JSON-serialized array of suggested amounts of tips in the smallest units of the currency (integer, **not** float/double). At most 4 suggested tip amounts can be specified. The suggested tip amounts must be positive, passed in a strictly increased order and must not exceed _max_tip_amount_. + pub suggested_tip_amounts: Vec [collect], + /// Unique deep-linking parameter. If left empty, **forwarded copies** of the sent message will have a Pay button, allowing multiple users to pay directly from the forwarded message, using the same invoice. If non-empty, forwarded copies of the sent message will have a URL button with a deep link to the bot (instead of a Pay button), with the value used as the start parameter + pub start_parameter: String [into], + /// A JSON-serialized data about the invoice, which will be shared with the payment provider. A detailed description of required fields should be provided by the payment provider. + pub provider_data: String [into], + /// URL of the product photo for the invoice. Can be a photo of the goods or a marketing image for a service. People like it better when they see what they are paying for. + pub photo_url: Url, + /// Photo size in bytes + pub photo_size: String [into], + /// Photo width + pub photo_width: String [into], + /// Photo height + pub photo_height: String [into], + /// Pass _True_, if you require the user's full name to complete the order + pub need_name: bool, + /// Pass _True_, if you require the user's phone number to complete the order + pub need_phone_number: bool, + /// Pass _True_, if you require the user's email address to complete the order + pub need_email: bool, + /// Pass _True_, if you require the user's shipping address to complete the order + pub need_shipping_address: bool, + /// Pass _True_, if user's phone number should be sent to provider + pub send_phone_number_to_provider: bool, + /// Pass _True_, if user's email address should be sent to provider + pub send_email_to_provider: bool, + /// Pass _True_, if the final price depends on the shipping method + pub is_flexible: bool, + /// Sends the message [silently]. Users will receive a notification with no sound. + /// + /// [silently]: https://telegram.org/blog/channels-2-0#silent-messages + pub disable_notification: bool, + /// Protects the contents of sent messages from forwarding and saving + pub protect_content: bool, + /// If the message is a reply, ID of the original message + pub reply_to_message_id: i32, + /// Pass _True_, if the message should be sent even if the specified replied-to message is not found + pub allow_sending_without_reply: bool, + /// A JSON-serialized object for an [inline keyboard]. If empty, one 'Pay `total price`' button will be shown. If not empty, the first button must be a Pay button. + /// + /// [inline keyboard]: https://core.telegram.org/bots#inline-keyboards-and-on-the-fly-updating + pub reply_markup: InlineKeyboardMarkup, + } + } +} diff --git a/crates/teloxide-core/src/payloads/send_location.rs b/crates/teloxide-core/src/payloads/send_location.rs new file mode 100644 index 00000000..69a84cee --- /dev/null +++ b/crates/teloxide-core/src/payloads/send_location.rs @@ -0,0 +1,50 @@ +//! Generated by `codegen_payloads`, do not edit by hand. + +use serde::Serialize; + +use crate::types::{Message, MessageId, Recipient, ReplyMarkup}; + +impl_payload! { + /// Use this method to send point on the map. On success, the sent [`Message`] is returned. + /// + /// [`Message`]: crate::types::Message + #[derive(Debug, PartialEq, Clone, Serialize)] + pub SendLocation (SendLocationSetters) => Message { + required { + /// Unique identifier for the target chat or username of the target channel (in the format `@channelusername`) + pub chat_id: Recipient [into], + /// Latitude of the location + pub latitude: f64, + /// Longitude of the location + pub longitude: f64, + } + optional { + /// The radius of uncertainty for the location, measured in meters; 0-1500 + pub horizontal_accuracy: f64, + /// Period in seconds for which the location will be updated (see [Live Locations], should be between 60 and 86400. + /// + /// [Live Locations]: https://telegram.org/blog/live-locations + pub live_period: u32, + /// For live locations, a direction in which the user is moving, in degrees. Must be between 1 and 360 if specified. + pub heading: u16, + /// For live locations, a maximum distance for proximity alerts about approaching another chat member, in meters. Must be between 1 and 100000 if specified. + pub proximity_alert_radius: u32, + /// Sends the message [silently]. Users will receive a notification with no sound. + /// + /// [silently]: https://telegram.org/blog/channels-2-0#silent-messages + pub disable_notification: bool, + /// Protects the contents of sent messages from forwarding and saving + pub protect_content: bool, + /// If the message is a reply, ID of the original message + #[serde(serialize_with = "crate::types::serialize_reply_to_message_id")] + pub reply_to_message_id: MessageId, + /// Pass _True_, if the message should be sent even if the specified replied-to message is not found + pub allow_sending_without_reply: bool, + /// Additional interface options. A JSON-serialized object for an [inline keyboard], [custom reply keyboard], instructions to remove reply keyboard or to force a reply from the user. + /// + /// [inline keyboard]: https://core.telegram.org/bots#inline-keyboards-and-on-the-fly-updating + /// [custom reply keyboard]: https://core.telegram.org/bots#keyboards + pub reply_markup: ReplyMarkup [into], + } + } +} diff --git a/crates/teloxide-core/src/payloads/send_media_group.rs b/crates/teloxide-core/src/payloads/send_media_group.rs new file mode 100644 index 00000000..9d2b14fa --- /dev/null +++ b/crates/teloxide-core/src/payloads/send_media_group.rs @@ -0,0 +1,33 @@ +//! Generated by `codegen_payloads`, do not edit by hand. + +use serde::Serialize; + +use crate::types::{InputMedia, Message, MessageId, Recipient}; + +impl_payload! { + /// Use this method to send a group of photos, videos, documents or audios as an album. Documents and audio files can be only grouped in an album with messages of the same type. On success, an array of [`Message`]s that were sent is returned. + /// + /// [`Message`]: crate::types::Message + #[derive(Debug, Clone, Serialize)] + pub SendMediaGroup (SendMediaGroupSetters) => Vec { + required { + /// Unique identifier for the target chat or username of the target channel (in the format `@channelusername`) + pub chat_id: Recipient [into], + /// A JSON-serialized array describing messages to be sent, must include 2-10 items + pub media: Vec [collect], + } + optional { + /// Sends the message [silently]. Users will receive a notification with no sound. + /// + /// [silently]: https://telegram.org/blog/channels-2-0#silent-messages + pub disable_notification: bool, + /// Protects the contents of sent messages from forwarding and saving + pub protect_content: bool, + /// If the message is a reply, ID of the original message + #[serde(serialize_with = "crate::types::serialize_reply_to_message_id")] + pub reply_to_message_id: MessageId, + /// Pass _True_, if the message should be sent even if the specified replied-to message is not found + pub allow_sending_without_reply: bool, + } + } +} diff --git a/crates/teloxide-core/src/payloads/send_message.rs b/crates/teloxide-core/src/payloads/send_message.rs new file mode 100644 index 00000000..20899142 --- /dev/null +++ b/crates/teloxide-core/src/payloads/send_message.rs @@ -0,0 +1,46 @@ +//! Generated by `codegen_payloads`, do not edit by hand. + +use serde::Serialize; + +use crate::types::{Message, MessageEntity, MessageId, ParseMode, Recipient, ReplyMarkup}; + +impl_payload! { + /// Use this method to send text messages. On success, the sent [`Message`] is returned. + /// + /// [`Message`]: crate::types::Message + #[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize)] + pub SendMessage (SendMessageSetters) => Message { + required { + /// Unique identifier for the target chat or username of the target channel (in the format `@channelusername`) + pub chat_id: Recipient [into], + /// Text of the message to be sent, 1-4096 characters after entities parsing + pub text: String [into], + } + optional { + /// Mode for parsing entities in the message text. See [formatting options] for more details. + /// + /// [formatting options]: https://core.telegram.org/bots/api#formatting-options + pub parse_mode: ParseMode, + /// List of special entities that appear in the message text, which can be specified instead of _parse\_mode_ + pub entities: Vec [collect], + /// Disables link previews for links in this message + pub disable_web_page_preview: bool, + /// Sends the message [silently]. Users will receive a notification with no sound. + /// + /// [silently]: https://telegram.org/blog/channels-2-0#silent-messages + pub disable_notification: bool, + /// Protects the contents of sent messages from forwarding and saving + pub protect_content: bool, + /// If the message is a reply, ID of the original message + #[serde(serialize_with = "crate::types::serialize_reply_to_message_id")] + pub reply_to_message_id: MessageId, + /// Pass _True_, if the message should be sent even if the specified replied-to message is not found + pub allow_sending_without_reply: bool, + /// Additional interface options. A JSON-serialized object for an [inline keyboard], [custom reply keyboard], instructions to remove reply keyboard or to force a reply from the user. + /// + /// [inline keyboard]: https://core.telegram.org/bots#inline-keyboards-and-on-the-fly-updating + /// [custom reply keyboard]: https://core.telegram.org/bots#keyboards + pub reply_markup: ReplyMarkup [into], + } + } +} diff --git a/crates/teloxide-core/src/payloads/send_photo.rs b/crates/teloxide-core/src/payloads/send_photo.rs new file mode 100644 index 00000000..50af80dd --- /dev/null +++ b/crates/teloxide-core/src/payloads/send_photo.rs @@ -0,0 +1,51 @@ +//! Generated by `codegen_payloads`, do not edit by hand. + +use serde::Serialize; + +use crate::types::{ + InputFile, Message, MessageEntity, MessageId, ParseMode, Recipient, ReplyMarkup, +}; + +impl_payload! { + @[multipart = photo] + /// Use this method to send photos. On success, the sent [`Message`] is returned. + /// + /// [`Message`]: crate::types::Message + #[derive(Debug, Clone, Serialize)] + pub SendPhoto (SendPhotoSetters) => Message { + required { + /// Unique identifier for the target chat or username of the target channel (in the format `@channelusername`) + pub chat_id: Recipient [into], + /// Photo to send. Pass a file_id as String to send a photo that exists on the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get a photo from the Internet, or upload a new photo using multipart/form-data. [More info on Sending Files »] + /// + /// [More info on Sending Files »]: crate::types::InputFile + pub photo: InputFile, + } + optional { + /// Photo caption (may also be used when resending photos by _file\_id_), 0-1024 characters after entities parsing + pub caption: String [into], + /// Mode for parsing entities in the photo caption. See [formatting options] for more details. + /// + /// [formatting options]: https://core.telegram.org/bots/api#formatting-options + pub parse_mode: ParseMode, + /// List of special entities that appear in the photo caption, which can be specified instead of _parse\_mode_ + pub caption_entities: Vec [collect], + /// Sends the message [silently]. Users will receive a notification with no sound. + /// + /// [silently]: https://telegram.org/blog/channels-2-0#silent-messages + pub disable_notification: bool, + /// Protects the contents of sent messages from forwarding and saving + pub protect_content: bool, + /// If the message is a reply, ID of the original message + #[serde(serialize_with = "crate::types::serialize_reply_to_message_id")] + pub reply_to_message_id: MessageId, + /// Pass _True_, if the message should be sent even if the specified replied-to message is not found + pub allow_sending_without_reply: bool, + /// Additional interface options. A JSON-serialized object for an [inline keyboard], [custom reply keyboard], instructions to remove reply keyboard or to force a reply from the user. + /// + /// [inline keyboard]: https://core.telegram.org/bots#inline-keyboards-and-on-the-fly-updating + /// [custom reply keyboard]: https://core.telegram.org/bots#keyboards + pub reply_markup: ReplyMarkup [into], + } + } +} diff --git a/crates/teloxide-core/src/payloads/send_poll.rs b/crates/teloxide-core/src/payloads/send_poll.rs new file mode 100644 index 00000000..bdc1f7f7 --- /dev/null +++ b/crates/teloxide-core/src/payloads/send_poll.rs @@ -0,0 +1,67 @@ +//! Generated by `codegen_payloads`, do not edit by hand. + +use chrono::{DateTime, Utc}; +use serde::Serialize; + +use crate::types::{ + Message, MessageEntity, MessageId, ParseMode, PollType, Recipient, ReplyMarkup, +}; + +impl_payload! { + /// Use this method to send phone contacts. On success, the sent [`Message`] is returned. + /// + /// [`Message`]: crate::types::Message + #[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize)] + pub SendPoll (SendPollSetters) => Message { + required { + /// Unique identifier for the target chat or username of the target channel (in the format `@channelusername`) + pub chat_id: Recipient [into], + /// Poll question, 1-300 characters + pub question: String [into], + /// A JSON-serialized list of answer options, 2-10 strings 1-100 characters each + pub options: Vec [collect], + } + optional { + /// True, if the poll needs to be anonymous, defaults to True + pub is_anonymous: bool, + /// Poll type, “quiz” or “regular”, defaults to “regular” + #[serde(rename = "type")] + pub type_: PollType, + /// True, if the poll allows multiple answers, ignored for polls in quiz mode, defaults to False + pub allows_multiple_answers: bool, + /// 0-based identifier of the correct answer option, required for polls in quiz mode + pub correct_option_id: u8, + /// Text that is shown when a user chooses an incorrect answer or taps on the lamp icon in a quiz-style poll, 0-200 characters with at most 2 line feeds after entities parsing + pub explanation: String [into], + /// Mode for parsing entities in the message text. See [formatting options] for more details. + /// + /// [formatting options]: https://core.telegram.org/bots/api#formatting-options + pub explanation_parse_mode: ParseMode, + /// List of special entities that appear in the poll explanation, which can be specified instead of _parse\_mode_ + pub explanation_entities: Vec [collect], + /// Amount of time in seconds the poll will be active after creation, 5-600. Can't be used together with close_date. + pub open_period: u16, + /// Point in time (Unix timestamp) when the poll will be automatically closed. Must be at least 5 and no more than 600 seconds in the future. Can't be used together with open_period. + #[serde(with = "crate::types::serde_opt_date_from_unix_timestamp")] + pub close_date: DateTime [into], + /// Pass True, if the poll needs to be immediately closed. This can be useful for poll preview. + pub is_closed: bool, + /// Sends the message [silently]. Users will receive a notification with no sound. + /// + /// [silently]: https://telegram.org/blog/channels-2-0#silent-messages + pub disable_notification: bool, + /// Protects the contents of sent messages from forwarding and saving + pub protect_content: bool, + /// If the message is a reply, ID of the original message + #[serde(serialize_with = "crate::types::serialize_reply_to_message_id")] + pub reply_to_message_id: MessageId, + /// Pass _True_, if the message should be sent even if the specified replied-to message is not found + pub allow_sending_without_reply: bool, + /// Additional interface options. A JSON-serialized object for an [inline keyboard], [custom reply keyboard], instructions to remove reply keyboard or to force a reply from the user. + /// + /// [inline keyboard]: https://core.telegram.org/bots#inline-keyboards-and-on-the-fly-updating + /// [custom reply keyboard]: https://core.telegram.org/bots#keyboards + pub reply_markup: ReplyMarkup [into], + } + } +} diff --git a/crates/teloxide-core/src/payloads/send_sticker.rs b/crates/teloxide-core/src/payloads/send_sticker.rs new file mode 100644 index 00000000..2ebf8bdd --- /dev/null +++ b/crates/teloxide-core/src/payloads/send_sticker.rs @@ -0,0 +1,40 @@ +//! Generated by `codegen_payloads`, do not edit by hand. + +use serde::Serialize; + +use crate::types::{InputFile, Message, Recipient, ReplyMarkup}; + +impl_payload! { + @[multipart = sticker] + /// Use this method to send static .WEBP or [animated] .TGS stickers. On success, the sent Message is returned. + /// + /// [animated]: https://telegram.org/blog/animated-stickers + #[derive(Debug, Clone, Serialize)] + pub SendSticker (SendStickerSetters) => Message { + required { + /// Unique identifier for the target chat or username of the target channel (in the format `@channelusername`). + pub chat_id: Recipient [into], + /// Sticker to send. Pass a file_id as String to send a photo that exists on the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get a photo from the Internet, or upload a new photo using multipart/form-data. [More info on Sending Files »] + /// + /// [More info on Sending Files »]: crate::types::InputFile + pub sticker: InputFile, + } + optional { + /// Sends the message [silently]. Users will receive a notification with no sound. + /// + /// [silently]: https://telegram.org/blog/channels-2-0#silent-messages + pub disable_notification: bool, + /// Protects the contents of sent messages from forwarding and saving + pub protect_content: bool, + /// If the message is a reply, ID of the original message + pub reply_to_message_id: i32, + /// Pass _True_, if the message should be sent even if the specified replied-to message is not found + pub allow_sending_without_reply: bool, + /// Additional interface options. A JSON-serialized object for an [inline keyboard], [custom reply keyboard], instructions to remove reply keyboard or to force a reply from the user. + /// + /// [inline keyboard]: https://core.telegram.org/bots#inline-keyboards-and-on-the-fly-updating + /// [custom reply keyboard]: https://core.telegram.org/bots#keyboards + pub reply_markup: ReplyMarkup [into], + } + } +} diff --git a/crates/teloxide-core/src/payloads/send_venue.rs b/crates/teloxide-core/src/payloads/send_venue.rs new file mode 100644 index 00000000..d368aede --- /dev/null +++ b/crates/teloxide-core/src/payloads/send_venue.rs @@ -0,0 +1,54 @@ +//! Generated by `codegen_payloads`, do not edit by hand. + +use serde::Serialize; + +use crate::types::{Message, MessageId, Recipient, ReplyMarkup}; + +impl_payload! { + /// Use this method to send information about a venue. On success, the sent [`Message`] is returned. + /// + /// [`Message`]: crate::types::Message + #[derive(Debug, PartialEq, Clone, Serialize)] + pub SendVenue (SendVenueSetters) => Message { + required { + /// Unique identifier for the target chat or username of the target channel (in the format `@channelusername`) + pub chat_id: Recipient [into], + /// Latitude of new location + pub latitude: f64, + /// Longitude of new location + pub longitude: f64, + /// Name of the venue + pub title: String [into], + /// Address of the venue + pub address: String [into], + } + optional { + /// Foursquare identifier of the venue + pub foursquare_id: String [into], + /// Foursquare type of the venue, if known. (For example, “arts_entertainment/default”, “arts_entertainment/aquarium” or “food/icecream”.) + pub foursquare_type: String [into], + /// Google Places identifier of the venue + pub google_place_id: String [into], + /// Google Places type of the venue. (See [supported types].) + /// + /// [supported types]: https://developers.google.com/places/web-service/supported_types + pub google_place_type: String [into], + /// Sends the message [silently]. Users will receive a notification with no sound. + /// + /// [silently]: https://telegram.org/blog/channels-2-0#silent-messages + pub disable_notification: bool, + /// Protects the contents of sent messages from forwarding and saving + pub protect_content: bool, + /// If the message is a reply, ID of the original message + #[serde(serialize_with = "crate::types::serialize_reply_to_message_id")] + pub reply_to_message_id: MessageId, + /// Pass _True_, if the message should be sent even if the specified replied-to message is not found + pub allow_sending_without_reply: bool, + /// Additional interface options. A JSON-serialized object for an [inline keyboard], [custom reply keyboard], instructions to remove reply keyboard or to force a reply from the user. + /// + /// [inline keyboard]: https://core.telegram.org/bots#inline-keyboards-and-on-the-fly-updating + /// [custom reply keyboard]: https://core.telegram.org/bots#keyboards + pub reply_markup: ReplyMarkup [into], + } + } +} diff --git a/crates/teloxide-core/src/payloads/send_video.rs b/crates/teloxide-core/src/payloads/send_video.rs new file mode 100644 index 00000000..f267cbf3 --- /dev/null +++ b/crates/teloxide-core/src/payloads/send_video.rs @@ -0,0 +1,64 @@ +//! Generated by `codegen_payloads`, do not edit by hand. + +use serde::Serialize; + +use crate::types::{ + InputFile, Message, MessageEntity, MessageId, ParseMode, Recipient, ReplyMarkup, +}; + +impl_payload! { + @[multipart = video, thumb] + /// Use this method to send video files, Telegram clients support mp4 videos (other formats may be sent as [`Document`]). On success, the sent [`Message`] is returned. Bots can currently send video files of up to 50 MB in size, this limit may be changed in the future. + /// + /// [`Document`]: crate::types::Document + /// [`Message`]: crate::types::Message + #[derive(Debug, Clone, Serialize)] + pub SendVideo (SendVideoSetters) => Message { + required { + /// Unique identifier for the target chat or username of the target channel (in the format `@channelusername`) + pub chat_id: Recipient [into], + /// Video to send. Pass a file_id as String to send a video that exists on the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get a video from the Internet, or upload a new video using multipart/form-data. [More info on Sending Files »] + /// + /// [More info on Sending Files »]: crate::types::InputFile + pub video: InputFile, + } + optional { + /// Duration of the video in seconds + pub duration: u32, + /// Video width + pub width: u32, + /// Video height + pub height: u32, + /// Thumbnail of the file sent; can be ignored if thumbnail generation for the file is supported server-side. The thumbnail should be in JPEG format and less than 200 kB in size. A thumbnail's width and height should not exceed 320. Ignored if the file is not uploaded using multipart/form-data. Thumbnails can't be reused and can be only uploaded as a new file, so you can pass “attach://” if the thumbnail was uploaded using multipart/form-data under . [More info on Sending Files »] + /// + /// [More info on Sending Files »]: crate::types::InputFile + pub thumb: InputFile, + /// Video caption (may also be used when resending videos by _file\_id_), 0-1024 characters after entities parsing + pub caption: String [into], + /// Mode for parsing entities in the video caption. See [formatting options] for more details. + /// + /// [formatting options]: https://core.telegram.org/bots/api#formatting-options + pub parse_mode: ParseMode, + /// List of special entities that appear in the caption, which can be specified instead of _parse\_mode_ + pub caption_entities: Vec [collect], + /// Pass _True_, if the uploaded video is suitable for streaming + pub supports_streaming: bool, + /// Sends the message [silently]. Users will receive a notification with no sound. + /// + /// [silently]: https://telegram.org/blog/channels-2-0#silent-messages + pub disable_notification: bool, + /// Protects the contents of sent messages from forwarding and saving + pub protect_content: bool, + /// If the message is a reply, ID of the original message + #[serde(serialize_with = "crate::types::serialize_reply_to_message_id")] + pub reply_to_message_id: MessageId, + /// Pass _True_, if the message should be sent even if the specified replied-to message is not found + pub allow_sending_without_reply: bool, + /// Additional interface options. A JSON-serialized object for an [inline keyboard], [custom reply keyboard], instructions to remove reply keyboard or to force a reply from the user. + /// + /// [inline keyboard]: https://core.telegram.org/bots#inline-keyboards-and-on-the-fly-updating + /// [custom reply keyboard]: https://core.telegram.org/bots#keyboards + pub reply_markup: ReplyMarkup [into], + } + } +} diff --git a/crates/teloxide-core/src/payloads/send_video_note.rs b/crates/teloxide-core/src/payloads/send_video_note.rs new file mode 100644 index 00000000..2c1e4484 --- /dev/null +++ b/crates/teloxide-core/src/payloads/send_video_note.rs @@ -0,0 +1,50 @@ +//! Generated by `codegen_payloads`, do not edit by hand. + +use serde::Serialize; + +use crate::types::{InputFile, Message, MessageId, Recipient, ReplyMarkup}; + +impl_payload! { + @[multipart = video_note, thumb] + /// As of [v.4.0], Telegram clients support rounded square mp4 videos of up to 1 minute long. Use this method to send video messages. On success, the sent [`Message`] is returned. + /// + /// [v.4.0]: https://core.telegram.org/bots/api#document + /// [`Message`]: crate::types::Message + #[derive(Debug, Clone, Serialize)] + pub SendVideoNote (SendVideoNoteSetters) => Message { + required { + /// Unique identifier for the target chat or username of the target channel (in the format `@channelusername`) + pub chat_id: Recipient [into], + /// Video note to send. Pass a file_id as String to send a video note that exists on the Telegram servers (recommended) or upload a new video using multipart/form-data. [More info on Sending Files »]. Sending video notes by a URL is currently unsupported + /// + /// [More info on Sending Files »]: crate::types::InputFile + pub video_note: InputFile, + } + optional { + /// Duration of the video in seconds + pub duration: u32, + /// Video width and height, i.e. diameter of the video message + pub length: u32, + /// Thumbnail of the file sent; can be ignored if thumbnail generation for the file is supported server-side. The thumbnail should be in JPEG format and less than 200 kB in size. A thumbnail's width and height should not exceed 320. Ignored if the file is not uploaded using multipart/form-data. Thumbnails can't be reused and can be only uploaded as a new file, so you can pass “attach://” if the thumbnail was uploaded using multipart/form-data under . [More info on Sending Files »] + /// + /// [More info on Sending Files »]: crate::types::InputFile + pub thumb: InputFile, + /// Sends the message [silently]. Users will receive a notification with no sound. + /// + /// [silently]: https://telegram.org/blog/channels-2-0#silent-messages + pub disable_notification: bool, + /// Protects the contents of sent messages from forwarding and saving + pub protect_content: bool, + /// If the message is a reply, ID of the original message + #[serde(serialize_with = "crate::types::serialize_reply_to_message_id")] + pub reply_to_message_id: MessageId, + /// Pass _True_, if the message should be sent even if the specified replied-to message is not found + pub allow_sending_without_reply: bool, + /// Additional interface options. A JSON-serialized object for an [inline keyboard], [custom reply keyboard], instructions to remove reply keyboard or to force a reply from the user. + /// + /// [inline keyboard]: https://core.telegram.org/bots#inline-keyboards-and-on-the-fly-updating + /// [custom reply keyboard]: https://core.telegram.org/bots#keyboards + pub reply_markup: ReplyMarkup [into], + } + } +} diff --git a/crates/teloxide-core/src/payloads/send_voice.rs b/crates/teloxide-core/src/payloads/send_voice.rs new file mode 100644 index 00000000..e1cbb888 --- /dev/null +++ b/crates/teloxide-core/src/payloads/send_voice.rs @@ -0,0 +1,53 @@ +//! Generated by `codegen_payloads`, do not edit by hand. + +use serde::Serialize; + +use crate::types::{ + InputFile, Message, MessageEntity, MessageId, ParseMode, Recipient, ReplyMarkup, +}; + +impl_payload! { + @[multipart = voice] + /// Use this method to send audio files, if you want Telegram clients to display the file as a playable voice message. For this to work, your audio must be in an .OGG file encoded with OPUS (other formats may be sent as [`Audio`] or [`Document`]). On success, the sent [`Message`] is returned. Bots can currently send voice messages of up to 50 MB in size, this limit may be changed in the future. + /// + /// [`Document`]: crate::types::Document + /// [`Audio`]: crate::types::Audio + /// [`Message`]: crate::types::Message + #[derive(Debug, Clone, Serialize)] + pub SendVoice (SendVoiceSetters) => Message { + required { + /// Unique identifier for the target chat or username of the target channel (in the format `@channelusername`) + pub chat_id: Recipient [into], + /// Audio file to send. Pass a file_id as String to send an audio file that exists on the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get an audio file from the Internet, or upload a new one using multipart/form-data. [More info on Sending Files »] + /// + /// [More info on Sending Files »]: crate::types::InputFile + pub voice: InputFile, + } + optional { + /// Voice message caption, 0-1024 characters after entities parsing + pub caption: String [into], + /// Mode for parsing entities in the voice message caption. See [formatting options] for more details. + /// + /// [formatting options]: https://core.telegram.org/bots/api#formatting-options + pub parse_mode: ParseMode, + /// List of special entities that appear in the photo caption, which can be specified instead of _parse\_mode_ + pub caption_entities: Vec [collect], + /// Duration of the voice message in seconds + pub duration: u32, + /// Sends the message [silently]. Users will receive a notification with no sound. + /// + /// [silently]: https://telegram.org/blog/channels-2-0#silent-messages + pub disable_notification: bool, + /// If the message is a reply, ID of the original message + #[serde(serialize_with = "crate::types::serialize_reply_to_message_id")] + pub reply_to_message_id: MessageId, + /// Pass _True_, if the message should be sent even if the specified replied-to message is not found + pub allow_sending_without_reply: bool, + /// Additional interface options. A JSON-serialized object for an [inline keyboard], [custom reply keyboard], instructions to remove reply keyboard or to force a reply from the user. + /// + /// [inline keyboard]: https://core.telegram.org/bots#inline-keyboards-and-on-the-fly-updating + /// [custom reply keyboard]: https://core.telegram.org/bots#keyboards + pub reply_markup: ReplyMarkup [into], + } + } +} diff --git a/crates/teloxide-core/src/payloads/set_chat_administrator_custom_title.rs b/crates/teloxide-core/src/payloads/set_chat_administrator_custom_title.rs new file mode 100644 index 00000000..fbdc85d7 --- /dev/null +++ b/crates/teloxide-core/src/payloads/set_chat_administrator_custom_title.rs @@ -0,0 +1,20 @@ +//! Generated by `codegen_payloads`, do not edit by hand. + +use serde::Serialize; + +use crate::types::{Recipient, True, UserId}; + +impl_payload! { + /// Use this method to set a custom title for an administrator in a supergroup promoted by the bot. Returns _True_on success. + #[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize)] + pub SetChatAdministratorCustomTitle (SetChatAdministratorCustomTitleSetters) => True { + required { + /// Unique identifier for the target chat or username of the target channel (in the format `@channelusername`) + pub chat_id: Recipient [into], + /// Unique identifier of the target user + pub user_id: UserId, + /// New custom title for the administrator; 0-16 characters, emoji are not allowed + pub custom_title: String [into], + } + } +} diff --git a/crates/teloxide-core/src/payloads/set_chat_description.rs b/crates/teloxide-core/src/payloads/set_chat_description.rs new file mode 100644 index 00000000..89976ecd --- /dev/null +++ b/crates/teloxide-core/src/payloads/set_chat_description.rs @@ -0,0 +1,20 @@ +//! Generated by `codegen_payloads`, do not edit by hand. + +use serde::Serialize; + +use crate::types::{Recipient, True}; + +impl_payload! { + /// Use this method to change the description of a group, a supergroup or a channel. The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. Returns _True_ on success. + #[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize)] + pub SetChatDescription (SetChatDescriptionSetters) => True { + required { + /// Unique identifier for the target chat or username of the target channel (in the format `@channelusername`) + pub chat_id: Recipient [into], + } + optional { + /// New chat description, 0-255 characters + pub description: String [into], + } + } +} diff --git a/crates/teloxide-core/src/payloads/set_chat_menu_button.rs b/crates/teloxide-core/src/payloads/set_chat_menu_button.rs new file mode 100644 index 00000000..b87b6393 --- /dev/null +++ b/crates/teloxide-core/src/payloads/set_chat_menu_button.rs @@ -0,0 +1,18 @@ +//! Generated by `codegen_payloads`, do not edit by hand. + +use serde::Serialize; + +use crate::types::{ChatId, MenuButton, True}; + +impl_payload! { + /// Use this method to change the bot's menu button in a private chat, or the default menu button. + #[derive(Debug, PartialEq, Eq, Hash, Default, Clone, Serialize)] + pub SetChatMenuButton (SetChatMenuButtonSetters) => True { + optional { + /// Unique identifier for the target private chat. If not specified, default bot's menu button will be changed. + pub chat_id: ChatId [into], + /// An object for the new bot's menu button. Defaults to MenuButtonDefault + pub menu_button: MenuButton, + } + } +} diff --git a/crates/teloxide-core/src/payloads/set_chat_permissions.rs b/crates/teloxide-core/src/payloads/set_chat_permissions.rs new file mode 100644 index 00000000..debfc9a9 --- /dev/null +++ b/crates/teloxide-core/src/payloads/set_chat_permissions.rs @@ -0,0 +1,18 @@ +//! Generated by `codegen_payloads`, do not edit by hand. + +use serde::Serialize; + +use crate::types::{ChatPermissions, Recipient, True}; + +impl_payload! { + /// Use this method to set default chat permissions for all members. The bot must be an administrator in the group or a supergroup for this to work and must have the _can_restrict_members_ admin rights. Returns _True_ on success. + #[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize)] + pub SetChatPermissions (SetChatPermissionsSetters) => True { + required { + /// Unique identifier for the target chat or username of the target channel (in the format `@channelusername`) + pub chat_id: Recipient [into], + /// New default chat permissions + pub permissions: ChatPermissions, + } + } +} diff --git a/crates/teloxide-core/src/payloads/set_chat_photo.rs b/crates/teloxide-core/src/payloads/set_chat_photo.rs new file mode 100644 index 00000000..7aa7ec7b --- /dev/null +++ b/crates/teloxide-core/src/payloads/set_chat_photo.rs @@ -0,0 +1,19 @@ +//! Generated by `codegen_payloads`, do not edit by hand. + +use serde::Serialize; + +use crate::types::{InputFile, Recipient, True}; + +impl_payload! { + @[multipart = photo] + /// Use this method to set a new profile photo for the chat. Photos can't be changed for private chats. The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. Returns _True_ on success. + #[derive(Debug, Clone, Serialize)] + pub SetChatPhoto (SetChatPhotoSetters) => True { + required { + /// Unique identifier for the target chat or username of the target channel (in the format `@channelusername`) + pub chat_id: Recipient [into], + /// New chat photo, uploaded using multipart/form-data + pub photo: InputFile, + } + } +} diff --git a/crates/teloxide-core/src/payloads/set_chat_sticker_set.rs b/crates/teloxide-core/src/payloads/set_chat_sticker_set.rs new file mode 100644 index 00000000..a865a008 --- /dev/null +++ b/crates/teloxide-core/src/payloads/set_chat_sticker_set.rs @@ -0,0 +1,18 @@ +//! Generated by `codegen_payloads`, do not edit by hand. + +use serde::Serialize; + +use crate::types::{Recipient, True}; + +impl_payload! { + /// Use this method to set a new group sticker set for a supergroup. The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. Use the field _can\_set\_sticker\_set_ optionally returned in getChat requests to check if the bot can use this method. Returns _True_ on success. + #[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize)] + pub SetChatStickerSet (SetChatStickerSetSetters) => True { + required { + /// Unique identifier for the target chat or username of the target channel (in the format `@channelusername`) + pub chat_id: Recipient [into], + /// Name of the sticker set to be set as the group sticker set + pub sticker_set_name: String [into], + } + } +} diff --git a/crates/teloxide-core/src/payloads/set_chat_title.rs b/crates/teloxide-core/src/payloads/set_chat_title.rs new file mode 100644 index 00000000..b78ef9cc --- /dev/null +++ b/crates/teloxide-core/src/payloads/set_chat_title.rs @@ -0,0 +1,18 @@ +//! Generated by `codegen_payloads`, do not edit by hand. + +use serde::Serialize; + +use crate::types::{Recipient, True}; + +impl_payload! { + /// Use this method to change the title of a chat. Titles can't be changed for private chats. The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. Returns _True_ on success. + #[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize)] + pub SetChatTitle (SetChatTitleSetters) => True { + required { + /// Unique identifier for the target chat or username of the target channel (in the format `@channelusername`) + pub chat_id: Recipient [into], + /// New chat title, 1-255 characters + pub title: String [into], + } + } +} diff --git a/crates/teloxide-core/src/payloads/set_game_score.rs b/crates/teloxide-core/src/payloads/set_game_score.rs new file mode 100644 index 00000000..eb34ed04 --- /dev/null +++ b/crates/teloxide-core/src/payloads/set_game_score.rs @@ -0,0 +1,33 @@ +//! Generated by `codegen_payloads`, do not edit by hand. + +use serde::Serialize; + +use crate::types::{Message, MessageId, UserId}; + +impl_payload! { + /// Use this method to set the score of the specified user in a game. On success, returns the edited [`Message`]. Returns an error, if the new score is not greater than the user's current score in the chat and force is False. + /// + /// See also: [`SetGameScoreInline`](crate::payloads::SetGameScoreInline) + /// + /// [`Message`]: crate::types::Message + #[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize)] + pub SetGameScore (SetGameScoreSetters) => Message { + required { + /// User identifier + pub user_id: UserId, + /// New score + pub score: u64, + /// Unique identifier for the target chat + pub chat_id: u32, + /// Identifier of the message to edit + #[serde(flatten)] + pub message_id: MessageId, + } + optional { + /// Pass True, if the high score is allowed to decrease. This can be useful when fixing mistakes or banning cheaters + pub force: bool, + /// Pass True, if the game message should not be automatically edited to include the current scoreboard + pub disable_edit_message: bool, + } + } +} diff --git a/crates/teloxide-core/src/payloads/set_game_score_inline.rs b/crates/teloxide-core/src/payloads/set_game_score_inline.rs new file mode 100644 index 00000000..513e4f57 --- /dev/null +++ b/crates/teloxide-core/src/payloads/set_game_score_inline.rs @@ -0,0 +1,28 @@ +//! Generated by `codegen_payloads`, do not edit by hand. + +use serde::Serialize; + +use crate::types::{Message, UserId}; + +impl_payload! { + /// Use this method to set the score of the specified user in a game. On success, returns _True_. Returns an error, if the new score is not greater than the user's current score in the chat and force is False. + /// + /// See also: [`SetGameScore`](crate::payloads::SetGameScore) + #[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize)] + pub SetGameScoreInline (SetGameScoreInlineSetters) => Message { + required { + /// User identifier + pub user_id: UserId, + /// New score + pub score: u64, + /// Identifier of the inline message + pub inline_message_id: String [into], + } + optional { + /// Pass True, if the high score is allowed to decrease. This can be useful when fixing mistakes or banning cheaters + pub force: bool, + /// Pass True, if the game message should not be automatically edited to include the current scoreboard + pub disable_edit_message: bool, + } + } +} diff --git a/crates/teloxide-core/src/payloads/set_my_commands.rs b/crates/teloxide-core/src/payloads/set_my_commands.rs new file mode 100644 index 00000000..7dd6f276 --- /dev/null +++ b/crates/teloxide-core/src/payloads/set_my_commands.rs @@ -0,0 +1,22 @@ +//! Generated by `codegen_payloads`, do not edit by hand. + +use serde::Serialize; + +use crate::types::{BotCommand, BotCommandScope, True}; + +impl_payload! { + /// Use this method to change the list of the bot's commands. Returns _True_ on success. + #[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize)] + pub SetMyCommands (SetMyCommandsSetters) => True { + required { + /// A JSON-serialized list of bot commands to be set as the list of the bot's commands. At most 100 commands can be specified. + pub commands: Vec [collect], + } + optional { + /// A JSON-serialized object, describing scope of users for which the commands are relevant. Defaults to BotCommandScopeDefault. + pub scope: BotCommandScope, + /// A two-letter ISO 639-1 language code. If empty, commands will be applied to all users from the given scope, for whose language there are no dedicated commands + pub language_code: String [into], + } + } +} diff --git a/crates/teloxide-core/src/payloads/set_my_default_administrator_rights.rs b/crates/teloxide-core/src/payloads/set_my_default_administrator_rights.rs new file mode 100644 index 00000000..25f64f38 --- /dev/null +++ b/crates/teloxide-core/src/payloads/set_my_default_administrator_rights.rs @@ -0,0 +1,18 @@ +//! Generated by `codegen_payloads`, do not edit by hand. + +use serde::Serialize; + +use crate::types::{ChatAdministratorRights, True}; + +impl_payload! { + /// Use this method to change the default administrator rights requested by the bot when it's added as an administrator to groups or channels. These rights will be suggested to users, but they are are free to modify the list before adding the bot. + #[derive(Debug, PartialEq, Eq, Hash, Default, Clone, Serialize)] + pub SetMyDefaultAdministratorRights (SetMyDefaultAdministratorRightsSetters) => True { + optional { + /// A JSON-serialized object describing new default administrator rights. If not specified, the default administrator rights will be cleared. + pub rights: ChatAdministratorRights, + /// Pass _True_ to change the default administrator rights of the bot in channels. Otherwise, the default administrator rights of the bot for groups and supergroups will be changed. + pub for_channels: bool, + } + } +} diff --git a/crates/teloxide-core/src/payloads/set_passport_data_errors.rs b/crates/teloxide-core/src/payloads/set_passport_data_errors.rs new file mode 100644 index 00000000..64f43fbe --- /dev/null +++ b/crates/teloxide-core/src/payloads/set_passport_data_errors.rs @@ -0,0 +1,20 @@ +//! Generated by `codegen_payloads`, do not edit by hand. + +use serde::Serialize; + +use crate::types::{PassportElementError, True, UserId}; + +impl_payload! { + /// Informs a user that some of the Telegram Passport elements they provided contains errors. The user will not be able to re-submit their Passport to you until the errors are fixed (the contents of the field for which you returned the error must change). Returns _True_ on success. + /// + /// Use this if the data submitted by the user doesn't satisfy the standards your service requires for any reason. For example, if a birthday date seems invalid, a submitted document is blurry, a scan shows evidence of tampering, etc. Supply some details in the error message to make sure the user knows how to correct the issues. + #[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize)] + pub SetPassportDataErrors (SetPassportDataErrorsSetters) => True { + required { + /// User identifier + pub user_id: UserId, + /// A JSON-serialized array describing the errors + pub errors: Vec [collect], + } + } +} diff --git a/crates/teloxide-core/src/payloads/set_sticker_position_in_set.rs b/crates/teloxide-core/src/payloads/set_sticker_position_in_set.rs new file mode 100644 index 00000000..281593ea --- /dev/null +++ b/crates/teloxide-core/src/payloads/set_sticker_position_in_set.rs @@ -0,0 +1,18 @@ +//! Generated by `codegen_payloads`, do not edit by hand. + +use serde::Serialize; + +use crate::types::True; + +impl_payload! { + /// Use this method to move a sticker in a set created by the bot to a specific position. Returns _True_ on success. + #[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize)] + pub SetStickerPositionInSet (SetStickerPositionInSetSetters) => True { + required { + /// File identifier of the sticker + pub sticker: String [into], + /// New sticker position in the set, zero-based + pub position: u32, + } + } +} diff --git a/crates/teloxide-core/src/payloads/set_sticker_set_thumb.rs b/crates/teloxide-core/src/payloads/set_sticker_set_thumb.rs new file mode 100644 index 00000000..4fc93547 --- /dev/null +++ b/crates/teloxide-core/src/payloads/set_sticker_set_thumb.rs @@ -0,0 +1,25 @@ +//! Generated by `codegen_payloads`, do not edit by hand. + +use serde::Serialize; + +use crate::types::{InputFile, True, UserId}; + +impl_payload! { + @[multipart = thumb] + /// Use this method to set the thumbnail of a sticker set. Animated thumbnails can be set for animated sticker sets only. Returns _True_ on success. + #[derive(Debug, Clone, Serialize)] + pub SetStickerSetThumb (SetStickerSetThumbSetters) => True { + required { + /// Name of the sticker set + pub name: String [into], + /// User identifier of sticker file owner + pub user_id: UserId, + } + optional { + /// A **PNG** image with the thumbnail, must be up to 128 kilobytes in size and have width and height exactly 100px, or a **TGS** animation with the thumbnail up to 32 kilobytes in size; see https://core.telegram.org/animated_stickers#technical-requirements for animated sticker technical requirements. Pass a _file\_id_ as a String to send a file that already exists on the Telegram servers, pass an HTTP URL as a String for Telegram to get a file from the Internet, or upload a new one using multipart/form-data. [More info on Sending Files »]. Animated sticker set thumbnail can't be uploaded via HTTP URL. + /// + /// [More info on Sending Files »]: crate::types::InputFile + pub thumb: InputFile, + } + } +} diff --git a/crates/teloxide-core/src/payloads/set_webhook.rs b/crates/teloxide-core/src/payloads/set_webhook.rs new file mode 100644 index 00000000..4171b8f2 --- /dev/null +++ b/crates/teloxide-core/src/payloads/set_webhook.rs @@ -0,0 +1,42 @@ +//! Generated by `codegen_payloads`, do not edit by hand. + +use serde::Serialize; +use url::Url; + +use crate::types::{AllowedUpdate, InputFile, True}; + +impl_payload! { + @[multipart = certificate] + /// Use this method to specify a url and receive incoming updates via an outgoing webhook. Whenever there is an update for the bot, we will send an HTTPS POST request to the specified url, containing a JSON-serialized [`Update`]. In case of an unsuccessful request, we will give up after a reasonable amount of attempts. Returns True on success. + /// + /// If you'd like to make sure that the Webhook request comes from Telegram, we recommend using a secret path in the URL, e.g. `https://www.example.com/`. Since nobody else knows your bot's token, you can be pretty sure it's us. + /// + /// [`Update`]: crate::types::Update + #[derive(Debug, Clone, Serialize)] + pub SetWebhook (SetWebhookSetters) => True { + required { + /// HTTPS url to send updates to. Use an empty string to remove webhook integration + pub url: Url, + } + optional { + /// Upload your public key certificate so that the root certificate in use can be checked. See our [self-signed guide] for details. + /// + /// [self-signed guide]: https://core.telegram.org/bots/self-signed + pub certificate: InputFile, + /// The fixed IP address which will be used to send webhook requests instead of the IP address resolved through DNS + pub ip_address: String [into], + /// Maximum allowed number of simultaneous HTTPS connections to the webhook for update delivery, 1-100. Defaults to 40. Use lower values to limit the load on your bot's server, and higher values to increase your bot's throughput. + pub max_connections: u8, + /// A JSON-serialized list of the update types you want your bot to receive. For example, specify [“message”, “edited_channel_post”, “callback_query”] to only receive updates of these types. See [`Update`] for a complete list of available update types. Specify an empty list to receive all updates regardless of type (default). If not specified, the previous setting will be used. + /// + /// Please note that this parameter doesn't affect updates created before the call to the setWebhook, so unwanted updates may be received for a short period of time. + /// + /// [`Update`]: crate::types::Update + pub allowed_updates: Vec [collect], + /// Pass _True_ to drop all pending updates + pub drop_pending_updates: bool, + /// A secret token to be sent in a header “X-Telegram-Bot-Api-Secret-Token” in every webhook request, 1-256 characters. Only characters `A-Z`, `a-z`, `0-9`, `_` and `-` are allowed. The header is useful to ensure that the request comes from a webhook set by you. + pub secret_token: String [into], + } + } +} diff --git a/crates/teloxide-core/src/payloads/setters.rs b/crates/teloxide-core/src/payloads/setters.rs new file mode 100644 index 00000000..d9e68c24 --- /dev/null +++ b/crates/teloxide-core/src/payloads/setters.rs @@ -0,0 +1,41 @@ +// Generated by `codegen_setters_reexports`, do not edit by hand. + +#[doc(no_inline)] +pub use crate::payloads::{ + AddStickerToSetSetters as _, AnswerCallbackQuerySetters as _, AnswerInlineQuerySetters as _, + AnswerPreCheckoutQuerySetters as _, AnswerShippingQuerySetters as _, + AnswerWebAppQuerySetters as _, ApproveChatJoinRequestSetters as _, BanChatMemberSetters as _, + BanChatSenderChatSetters as _, CloseSetters as _, CopyMessageSetters as _, + CreateChatInviteLinkSetters as _, CreateInvoiceLinkSetters as _, + CreateNewStickerSetSetters as _, DeclineChatJoinRequestSetters as _, + DeleteChatPhotoSetters as _, DeleteChatStickerSetSetters as _, DeleteMessageSetters as _, + DeleteMyCommandsSetters as _, DeleteStickerFromSetSetters as _, DeleteWebhookSetters as _, + EditChatInviteLinkSetters as _, EditMessageCaptionInlineSetters as _, + EditMessageCaptionSetters as _, EditMessageLiveLocationInlineSetters as _, + EditMessageLiveLocationSetters as _, EditMessageMediaInlineSetters as _, + EditMessageMediaSetters as _, EditMessageReplyMarkupInlineSetters as _, + EditMessageReplyMarkupSetters as _, EditMessageTextInlineSetters as _, + EditMessageTextSetters as _, ExportChatInviteLinkSetters as _, ForwardMessageSetters as _, + GetChatAdministratorsSetters as _, GetChatMemberCountSetters as _, GetChatMemberSetters as _, + GetChatMembersCountSetters as _, GetChatMenuButtonSetters as _, GetChatSetters as _, + GetCustomEmojiStickersSetters as _, GetFileSetters as _, GetGameHighScoresSetters as _, + GetMeSetters as _, GetMyCommandsSetters as _, GetMyDefaultAdministratorRightsSetters as _, + GetStickerSetSetters as _, GetUpdatesSetters as _, GetUserProfilePhotosSetters as _, + GetWebhookInfoSetters as _, KickChatMemberSetters as _, LeaveChatSetters as _, + LogOutSetters as _, PinChatMessageSetters as _, PromoteChatMemberSetters as _, + RestrictChatMemberSetters as _, RevokeChatInviteLinkSetters as _, SendAnimationSetters as _, + SendAudioSetters as _, SendChatActionSetters as _, SendContactSetters as _, + SendDiceSetters as _, SendDocumentSetters as _, SendGameSetters as _, SendInvoiceSetters as _, + SendLocationSetters as _, SendMediaGroupSetters as _, SendMessageSetters as _, + SendPhotoSetters as _, SendPollSetters as _, SendStickerSetters as _, SendVenueSetters as _, + SendVideoNoteSetters as _, SendVideoSetters as _, SendVoiceSetters as _, + SetChatAdministratorCustomTitleSetters as _, SetChatDescriptionSetters as _, + SetChatMenuButtonSetters as _, SetChatPermissionsSetters as _, SetChatPhotoSetters as _, + SetChatStickerSetSetters as _, SetChatTitleSetters as _, SetGameScoreInlineSetters as _, + SetGameScoreSetters as _, SetMyCommandsSetters as _, + SetMyDefaultAdministratorRightsSetters as _, SetPassportDataErrorsSetters as _, + SetStickerPositionInSetSetters as _, SetStickerSetThumbSetters as _, SetWebhookSetters as _, + StopMessageLiveLocationInlineSetters as _, StopMessageLiveLocationSetters as _, + StopPollSetters as _, UnbanChatMemberSetters as _, UnbanChatSenderChatSetters as _, + UnpinAllChatMessagesSetters as _, UnpinChatMessageSetters as _, UploadStickerFileSetters as _, +}; diff --git a/crates/teloxide-core/src/payloads/stop_message_live_location.rs b/crates/teloxide-core/src/payloads/stop_message_live_location.rs new file mode 100644 index 00000000..970fc87c --- /dev/null +++ b/crates/teloxide-core/src/payloads/stop_message_live_location.rs @@ -0,0 +1,35 @@ +//! Generated by `codegen_payloads`, do not edit by hand. + +use serde::Serialize; + +use crate::types::{Message, MessageId, Recipient, ReplyMarkup}; + +impl_payload! { + /// Use this method to edit live location messages. A location can be edited until its live_period expires or editing is explicitly disabled by a call to [`StopMessageLiveLocation`]. On success, the edited Message is returned. + /// + /// See also: [`StopMessageLiveLocationInline`](crate::payloads::StopMessageLiveLocationInline) + /// + /// [`Message`]: crate::types::Message + /// [`StopMessageLiveLocation`]: crate::payloads::StopMessageLiveLocation + #[derive(Debug, PartialEq, Clone, Serialize)] + pub StopMessageLiveLocation (StopMessageLiveLocationSetters) => Message { + required { + /// Unique identifier for the target chat or username of the target channel (in the format `@channelusername`) + pub chat_id: Recipient [into], + /// Identifier of the message to edit + #[serde(flatten)] + pub message_id: MessageId, + /// Latitude of new location + pub latitude: f64, + /// Longitude of new location + pub longitude: f64, + } + optional { + /// Additional interface options. A JSON-serialized object for an [inline keyboard], [custom reply keyboard], instructions to remove reply keyboard or to force a reply from the user. + /// + /// [inline keyboard]: https://core.telegram.org/bots#inline-keyboards-and-on-the-fly-updating + /// [custom reply keyboard]: https://core.telegram.org/bots#keyboards + pub reply_markup: ReplyMarkup [into], + } + } +} diff --git a/crates/teloxide-core/src/payloads/stop_message_live_location_inline.rs b/crates/teloxide-core/src/payloads/stop_message_live_location_inline.rs new file mode 100644 index 00000000..2191c774 --- /dev/null +++ b/crates/teloxide-core/src/payloads/stop_message_live_location_inline.rs @@ -0,0 +1,31 @@ +//! Generated by `codegen_payloads`, do not edit by hand. + +use serde::Serialize; + +use crate::types::{Message, ReplyMarkup}; + +impl_payload! { + /// Use this method to edit live location messages. A location can be edited until its live_period expires or editing is explicitly disabled by a call to [`StopMessageLiveLocation`]. On success, True is returned. + /// + /// See also: [`StopMessageLiveLocation`](crate::payloads::StopMessageLiveLocation) + /// + /// [`StopMessageLiveLocation`]: crate::payloads::StopMessageLiveLocation + #[derive(Debug, PartialEq, Clone, Serialize)] + pub StopMessageLiveLocationInline (StopMessageLiveLocationInlineSetters) => Message { + required { + /// Identifier of the inline message + pub inline_message_id: String [into], + /// Latitude of new location + pub latitude: f64, + /// Longitude of new location + pub longitude: f64, + } + optional { + /// Additional interface options. A JSON-serialized object for an [inline keyboard], [custom reply keyboard], instructions to remove reply keyboard or to force a reply from the user. + /// + /// [inline keyboard]: https://core.telegram.org/bots#inline-keyboards-and-on-the-fly-updating + /// [custom reply keyboard]: https://core.telegram.org/bots#keyboards + pub reply_markup: ReplyMarkup [into], + } + } +} diff --git a/crates/teloxide-core/src/payloads/stop_poll.rs b/crates/teloxide-core/src/payloads/stop_poll.rs new file mode 100644 index 00000000..05ab001d --- /dev/null +++ b/crates/teloxide-core/src/payloads/stop_poll.rs @@ -0,0 +1,25 @@ +//! Generated by `codegen_payloads`, do not edit by hand. + +use serde::Serialize; + +use crate::types::{InlineKeyboardMarkup, MessageId, Poll, Recipient}; + +impl_payload! { + /// Use this method to stop a poll which was sent by the bot. On success, the stopped Poll with the final results is returned. + #[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize)] + pub StopPoll (StopPollSetters) => Poll { + required { + /// Unique identifier for the target chat or username of the target channel (in the format `@channelusername`). + pub chat_id: Recipient [into], + /// Identifier of the message to edit + #[serde(flatten)] + pub message_id: MessageId, + } + optional { + /// A JSON-serialized object for an [inline keyboard]. + /// + /// [inline keyboard]: https://core.telegram.org/bots#inline-keyboards-and-on-the-fly-updating + pub reply_markup: InlineKeyboardMarkup, + } + } +} diff --git a/crates/teloxide-core/src/payloads/unban_chat_member.rs b/crates/teloxide-core/src/payloads/unban_chat_member.rs new file mode 100644 index 00000000..fb20e0e0 --- /dev/null +++ b/crates/teloxide-core/src/payloads/unban_chat_member.rs @@ -0,0 +1,22 @@ +//! Generated by `codegen_payloads`, do not edit by hand. + +use serde::Serialize; + +use crate::types::{Recipient, True, UserId}; + +impl_payload! { + /// Use this method to unban a previously kicked user in a supergroup or channel. The user will **not** return to the group or channel automatically, but will be able to join via link, etc. The bot must be an administrator for this to work. By default, this method guarantees that after the call the user is not a member of the chat, but will be able to join it. So if the user is a member of the chat they will also be **removed** from the chat. If you don't want this, use the parameter _only\_if\_banned_. Returns _True_ on success. + #[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize)] + pub UnbanChatMember (UnbanChatMemberSetters) => True { + required { + /// Unique identifier for the target chat or username of the target channel (in the format `@channelusername`) + pub chat_id: Recipient [into], + /// Unique identifier of the target user + pub user_id: UserId, + } + optional { + /// Do nothing if the user is not banned + pub only_if_banned: bool, + } + } +} diff --git a/crates/teloxide-core/src/payloads/unban_chat_sender_chat.rs b/crates/teloxide-core/src/payloads/unban_chat_sender_chat.rs new file mode 100644 index 00000000..7287e099 --- /dev/null +++ b/crates/teloxide-core/src/payloads/unban_chat_sender_chat.rs @@ -0,0 +1,18 @@ +//! Generated by `codegen_payloads`, do not edit by hand. + +use serde::Serialize; + +use crate::types::{ChatId, Recipient, True}; + +impl_payload! { + /// Use this method to unban a previously banned channel chat in a supergroup or channel. The bot must be an administrator for this to work and must have the appropriate administrator rights. + #[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize)] + pub UnbanChatSenderChat (UnbanChatSenderChatSetters) => True { + required { + /// Unique identifier for the target chat or username of the target channel (in the format `@channelusername`) + pub chat_id: Recipient [into], + /// Unique identifier of the target sender chat + pub sender_chat_id: ChatId [into], + } + } +} diff --git a/crates/teloxide-core/src/payloads/unpin_all_chat_messages.rs b/crates/teloxide-core/src/payloads/unpin_all_chat_messages.rs new file mode 100644 index 00000000..a1d8fa0a --- /dev/null +++ b/crates/teloxide-core/src/payloads/unpin_all_chat_messages.rs @@ -0,0 +1,16 @@ +//! Generated by `codegen_payloads`, do not edit by hand. + +use serde::Serialize; + +use crate::types::{Recipient, True}; + +impl_payload! { + /// Use this method to clear the list of pinned messages in a chat. If the chat is not a private chat, the bot must be an administrator in the chat for this to work and must have the 'can_pin_messages' admin right in a supergroup or 'can_edit_messages' admin right in a channel. Returns _True_ on success. + #[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize)] + pub UnpinAllChatMessages (UnpinAllChatMessagesSetters) => True { + required { + /// Unique identifier for the target chat or username of the target channel (in the format `@channelusername`) + pub chat_id: Recipient [into], + } + } +} diff --git a/crates/teloxide-core/src/payloads/unpin_chat_message.rs b/crates/teloxide-core/src/payloads/unpin_chat_message.rs new file mode 100644 index 00000000..94771ea0 --- /dev/null +++ b/crates/teloxide-core/src/payloads/unpin_chat_message.rs @@ -0,0 +1,21 @@ +//! Generated by `codegen_payloads`, do not edit by hand. + +use serde::Serialize; + +use crate::types::{MessageId, Recipient, True}; + +impl_payload! { + /// Use this method to remove a message from the list of pinned messages in a chat. If the chat is not a private chat, the bot must be an administrator in the chat for this to work and must have the 'can_pin_messages' admin right in a supergroup or 'can_edit_messages' admin right in a channel. Returns _True_ on success. + #[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize)] + pub UnpinChatMessage (UnpinChatMessageSetters) => True { + required { + /// Unique identifier for the target chat or username of the target channel (in the format `@channelusername`) + pub chat_id: Recipient [into], + } + optional { + /// Identifier of a message to unpin. If not specified, the most recent pinned message (by sending date) will be unpinned. + #[serde(flatten)] + pub message_id: MessageId, + } + } +} diff --git a/crates/teloxide-core/src/payloads/upload_sticker_file.rs b/crates/teloxide-core/src/payloads/upload_sticker_file.rs new file mode 100644 index 00000000..d856dc1d --- /dev/null +++ b/crates/teloxide-core/src/payloads/upload_sticker_file.rs @@ -0,0 +1,21 @@ +//! Generated by `codegen_payloads`, do not edit by hand. + +use serde::Serialize; + +use crate::types::{FileMeta, InputFile, UserId}; + +impl_payload! { + @[multipart = png_sticker] + /// Use this method to upload a .PNG file with a sticker for later use in _createNewStickerSet_ and _addStickerToSet_ methods (can be used multiple times). Returns the uploaded File on success. + #[derive(Debug, Clone, Serialize)] + pub UploadStickerFile (UploadStickerFileSetters) => FileMeta { + required { + /// User identifier of sticker file owner + pub user_id: UserId, + /// PNG image with the sticker, must be up to 512 kilobytes in size, dimensions must not exceed 512px, and either width or height must be exactly 512px. [More info on Sending Files »] + /// + /// [More info on Sending Files »]: crate::types::InputFile + pub png_sticker: InputFile, + } + } +} diff --git a/crates/teloxide-core/src/prelude.rs b/crates/teloxide-core/src/prelude.rs new file mode 100644 index 00000000..e54840cc --- /dev/null +++ b/crates/teloxide-core/src/prelude.rs @@ -0,0 +1,9 @@ +//! Commonly used items. + +#[doc(no_inline)] +pub use crate::{ + payloads::setters::*, + requests::{Request, Requester, RequesterExt}, + types::{ChatId, UserId}, + Bot, +}; diff --git a/crates/teloxide-core/src/requests.rs b/crates/teloxide-core/src/requests.rs new file mode 100644 index 00000000..4ed846f3 --- /dev/null +++ b/crates/teloxide-core/src/requests.rs @@ -0,0 +1,23 @@ +//! Telegram API requests. + +pub use self::{ + has_payload::HasPayload, json::JsonRequest, multipart::MultipartRequest, + multipart_payload::MultipartPayload, payload::Payload, request::Request, requester::Requester, + requester_ext::RequesterExt, +}; + +/// A type that is returned after making a request to Telegram. +pub type ResponseResult = Result; + +/// An output type of [`Payload`] in [`HasPayload`]. +pub type Output = <::Payload as Payload>::Output; + +mod has_payload; +mod json; +mod multipart; +pub(crate) mod multipart_payload; +mod payload; +mod request; +mod requester; +mod requester_ext; +mod utils; diff --git a/crates/teloxide-core/src/requests/has_payload.rs b/crates/teloxide-core/src/requests/has_payload.rs new file mode 100644 index 00000000..d7d12100 --- /dev/null +++ b/crates/teloxide-core/src/requests/has_payload.rs @@ -0,0 +1,72 @@ +use either::Either; + +use crate::requests::Payload; + +/// Represents types having payload inside. +/// +/// This trait is something between [`DerefMut`] and [`BorrowMut`] — it allows +/// only one implementation per type (the [output type] is associated, not +/// generic), has implementations for all types `P` such `P: `[`Payload`], but +/// has no magic compiler support like [`DerefMut`] does nor does it require +/// any laws about `Eq`, `Ord` and `Hash` as [`BorrowMut`] does. +/// +/// Also the [output type] is bounded by the [`Payload`] trait. +/// +/// This trait is mostly used to implement payload setters (on both payloads & +/// requests), so you probably won't find yourself using it directly. +/// +/// [`DerefMut`]: std::ops::DerefMut +/// [`BorrowMut`]: std::borrow::BorrowMut +/// [`Payload`]: crate::requests::Payload +/// [output type]: HasPayload::Payload +pub trait HasPayload { + /// The type of the payload contained. + type Payload: Payload; + + /// Gain mutable access to the underlying payload. + fn payload_mut(&mut self) -> &mut Self::Payload; + + /// Gain immutable access to the underlying payload. + fn payload_ref(&self) -> &Self::Payload; + + /// Update payload with a function + fn with_payload_mut(mut self, f: F) -> Self + where + Self: Sized, + F: FnOnce(&mut Self::Payload), + { + f(self.payload_mut()); + self + } +} + +impl

HasPayload for P +where + P: Payload, +{ + type Payload = Self; + + fn payload_mut(&mut self) -> &mut Self::Payload { + self + } + + fn payload_ref(&self) -> &Self::Payload { + self + } +} + +impl HasPayload for Either +where + L: HasPayload, + R: HasPayload, +{ + type Payload = L::Payload; + + fn payload_mut(&mut self) -> &mut Self::Payload { + self.as_mut().either(<_>::payload_mut, <_>::payload_mut) + } + + fn payload_ref(&self) -> &Self::Payload { + self.as_ref().either(<_>::payload_ref, <_>::payload_ref) + } +} diff --git a/crates/teloxide-core/src/requests/json.rs b/crates/teloxide-core/src/requests/json.rs new file mode 100644 index 00000000..5898cfa2 --- /dev/null +++ b/crates/teloxide-core/src/requests/json.rs @@ -0,0 +1,117 @@ +use std::future::IntoFuture; + +use serde::{de::DeserializeOwned, Serialize}; + +use crate::{ + bot::Bot, + requests::{HasPayload, Payload, Request, ResponseResult}, + RequestError, +}; + +/// A ready-to-send Telegram request whose payload is sent using [JSON]. +/// +/// [JSON]: https://core.telegram.org/bots/api#making-requests +#[must_use = "Requests are lazy and do nothing unless sent"] +#[derive(Clone)] +pub struct JsonRequest

{ + bot: Bot, + payload: P, +} + +impl

JsonRequest

{ + pub const fn new(bot: Bot, payload: P) -> Self { + Self { bot, payload } + } +} + +impl

Request for JsonRequest

+where + // FIXME(waffle): + // this is required on stable because of + // https://github.com/rust-lang/rust/issues/76882 + // when it's resolved or `type_alias_impl_trait` feature + // stabilized, we should remove 'static restriction + // + // (though critically, currently we have no + // non-'static payloads) + P: 'static, + P: Payload + Serialize, + P::Output: DeserializeOwned, +{ + type Err = RequestError; + type Send = Send

; + type SendRef = SendRef

; + + fn send(self) -> Self::Send { + Send::new(self) + } + + fn send_ref(&self) -> Self::SendRef { + SendRef::new(self) + } +} + +impl

IntoFuture for JsonRequest

+where + P: 'static, + P: Payload + Serialize, + P::Output: DeserializeOwned, +{ + type Output = Result; + type IntoFuture = ::Send; + + fn into_future(self) -> Self::IntoFuture { + self.send() + } +} + +impl

HasPayload for JsonRequest

+where + P: Payload, +{ + type Payload = P; + + fn payload_mut(&mut self) -> &mut Self::Payload { + &mut self.payload + } + + fn payload_ref(&self) -> &Self::Payload { + &self.payload + } +} + +impl core::ops::Deref for JsonRequest

{ + type Target = P; + + fn deref(&self) -> &Self::Target { + self.payload_ref() + } +} + +impl core::ops::DerefMut for JsonRequest

{ + fn deref_mut(&mut self) -> &mut Self::Target { + self.payload_mut() + } +} + +req_future! { + def: |it: JsonRequest| { + it.bot.execute_json(&it.payload) + } + pub Send (inner0) -> ResponseResult + where + U: 'static, + U: Payload + Serialize, + U::Output: DeserializeOwned, +} + +req_future! { + def: |it: &JsonRequest| { + it.bot.execute_json(&it.payload) + } + pub SendRef (inner1) -> ResponseResult + where + U: 'static, + U: Payload + Serialize, + U::Output: DeserializeOwned, +} diff --git a/crates/teloxide-core/src/requests/multipart.rs b/crates/teloxide-core/src/requests/multipart.rs new file mode 100644 index 00000000..6578acd9 --- /dev/null +++ b/crates/teloxide-core/src/requests/multipart.rs @@ -0,0 +1,128 @@ +use std::future::IntoFuture; + +use serde::{de::DeserializeOwned, Serialize}; + +use crate::{ + bot::Bot, + requests::{HasPayload, MultipartPayload, Payload, Request, ResponseResult}, + RequestError, +}; + +/// A ready-to-send Telegram request whose payload is sent using +/// [multipart/form-data]. +/// +/// [multipart/form-data]: https://core.telegram.org/bots/api#making-requests +#[must_use = "Requests are lazy and do nothing unless sent"] +#[derive(Clone)] +pub struct MultipartRequest

{ + bot: Bot, + payload: P, +} + +impl

MultipartRequest

{ + pub const fn new(bot: Bot, payload: P) -> Self { + Self { bot, payload } + } +} + +impl

Request for MultipartRequest

+where + // FIXME(waffle): + // this is required on stable because of + // https://github.com/rust-lang/rust/issues/76882 + // when it's resolved or `type_alias_impl_trait` feature + // stabilized, we should remove 'static restriction + // + // (though critically, currently we have no + // non-'static payloads) + P: 'static, + P: Payload + MultipartPayload + Serialize, + P::Output: DeserializeOwned, +{ + type Err = RequestError; + type Send = Send

; + type SendRef = SendRef

; + + fn send(self) -> Self::Send { + Send::new(self) + } + + fn send_ref(&self) -> Self::SendRef { + SendRef::new(self) + } +} + +impl

IntoFuture for MultipartRequest

+where + P: 'static, + P: Payload + MultipartPayload + Serialize, + P::Output: DeserializeOwned, +{ + type Output = Result; + type IntoFuture = ::Send; + + fn into_future(self) -> Self::IntoFuture { + self.send() + } +} + +impl

HasPayload for MultipartRequest

+where + P: Payload, +{ + type Payload = P; + + fn payload_mut(&mut self) -> &mut Self::Payload { + &mut self.payload + } + + fn payload_ref(&self) -> &Self::Payload { + &self.payload + } +} + +impl

core::ops::Deref for MultipartRequest

+where + P: 'static, + P: Payload + MultipartPayload, + P::Output: DeserializeOwned, +{ + type Target = P; + + fn deref(&self) -> &Self::Target { + self.payload_ref() + } +} + +impl

core::ops::DerefMut for MultipartRequest

+where + P: 'static, + P: Payload + MultipartPayload, + P::Output: DeserializeOwned, +{ + fn deref_mut(&mut self) -> &mut Self::Target { + self.payload_mut() + } +} + +req_future! { + def: |it: MultipartRequest| { + it.bot.execute_multipart(&mut {it.payload}) + } + pub Send (inner0) -> ResponseResult + where + U: 'static, + U: Payload + MultipartPayload + Serialize, + U::Output: DeserializeOwned, +} + +req_future! { + def: |it: &MultipartRequest| { + it.bot.execute_multipart_ref(&it.payload) + } + pub SendRef (inner1) -> ResponseResult + where + U: 'static, + U: Payload + MultipartPayload + Serialize, + U::Output: DeserializeOwned, +} diff --git a/crates/teloxide-core/src/requests/multipart_payload.rs b/crates/teloxide-core/src/requests/multipart_payload.rs new file mode 100644 index 00000000..1b5b9242 --- /dev/null +++ b/crates/teloxide-core/src/requests/multipart_payload.rs @@ -0,0 +1,43 @@ +use crate::{ + payloads, + requests::Payload, + types::{InputFile, InputFileLike, InputMedia}, +}; + +/// Payloads that need to be sent as `multipart/form-data` because they contain +/// files inside. +pub trait MultipartPayload: Payload { + fn copy_files(&self, into: &mut dyn FnMut(InputFile)); + + fn move_files(&mut self, into: &mut dyn FnMut(InputFile)); +} + +impl MultipartPayload for payloads::SendMediaGroup { + fn copy_files(&self, into: &mut dyn FnMut(InputFile)) { + self.media.iter().flat_map(InputMedia::files).for_each(|f| f.copy_into(into)) + } + + fn move_files(&mut self, into: &mut dyn FnMut(InputFile)) { + self.media.iter_mut().flat_map(InputMedia::files_mut).for_each(|f| f.move_into(into)) + } +} + +impl MultipartPayload for payloads::EditMessageMedia { + fn copy_files(&self, into: &mut dyn FnMut(InputFile)) { + self.media.files().for_each(|f| f.copy_into(into)) + } + + fn move_files(&mut self, into: &mut dyn FnMut(InputFile)) { + self.media.files_mut().for_each(|f| f.move_into(into)) + } +} + +impl MultipartPayload for payloads::EditMessageMediaInline { + fn copy_files(&self, into: &mut dyn FnMut(InputFile)) { + self.media.files().for_each(|f| f.copy_into(into)) + } + + fn move_files(&mut self, into: &mut dyn FnMut(InputFile)) { + self.media.files_mut().for_each(|f| f.move_into(into)) + } +} diff --git a/crates/teloxide-core/src/requests/payload.rs b/crates/teloxide-core/src/requests/payload.rs new file mode 100644 index 00000000..4148da33 --- /dev/null +++ b/crates/teloxide-core/src/requests/payload.rs @@ -0,0 +1,34 @@ +use std::time::Duration; + +/// Payload of a request. +/// +/// Simply speaking, structures implementing this trait represent arguments of +/// a Telegram bot API method. +/// +/// Also, this trait provides some additional information needed to send a +/// request to Telegram. +#[cfg_attr(all(any(docsrs, dep_docsrs), feature = "nightly"), doc(notable_trait))] +pub trait Payload { + /// The return type of a Telegram method. + /// + /// Note: it should not include `Result` wrappers (e.g. it should be simply + /// [`Message`], [`True`] or something else). + /// + /// [`Message`]: crate::types::Message + /// [`True`]: crate::types::True + type Output; + + /// Name of a Telegram method. + /// + /// It is case insensitive, though must not include underscores. (e.g. + /// `GetMe`, `GETME`, `getme`, `getMe` are ok, but `get_me` is not ok). + const NAME: &'static str; + + /// If this payload may take long time to execute (e.g. [`GetUpdates`] with + /// big `timeout`), the **minimum** timeout that should be used. + /// + /// [`GetUpdates`]: crate::payloads::GetUpdates + fn timeout_hint(&self) -> Option { + None + } +} diff --git a/crates/teloxide-core/src/requests/request.rs b/crates/teloxide-core/src/requests/request.rs new file mode 100644 index 00000000..bd5113af --- /dev/null +++ b/crates/teloxide-core/src/requests/request.rs @@ -0,0 +1,133 @@ +use std::future::{Future, IntoFuture}; + +// use either::Either; +// use futures::future; + +use crate::requests::{HasPayload, Output}; + +/// A ready-to-send Telegram request. +// FIXME(waffle): Write better doc for the trait +/// +/// ## Implementation notes +/// +/// It is not recommended to do any kind of _work_ in `send` or `send_ref`. +/// Instead it's recommended to do all the (possible) stuff in the returned +/// future. In other words — keep it lazy. +/// +/// This is crucial for request wrappers which may want to cancel and/or never +/// send the underlying request. E.g.: [`Throttle`]'s `send_ref` calls +/// `B::send_ref` while _not_ meaning to really send the request at the moment. +/// +/// [`Throttle`]: crate::adaptors::Throttle +#[cfg_attr(all(any(docsrs, dep_docsrs), feature = "nightly"), doc(notable_trait))] +pub trait Request +where + Self: HasPayload, + Self: IntoFuture, Self::Err>, IntoFuture = Self::Send>, +{ + /// The type of an error that may happen while sending a request to + /// Telegram. + type Err: std::error::Error + Send; + + /// The type of the future returned by the [`send`](Request::send) method. + type Send: Future, Self::Err>> + Send; + + /// A type of the future returned by the [`send_ref`](Request::send_ref) + /// method. + // Note: it intentionally forbids borrowing from `self` though we couldn't allow + // borrowing without GATs anyway. + type SendRef: Future, Self::Err>> + Send; + + /// Send this request. + /// + /// ## Examples + /// + /// ``` + /// # async { + /// use teloxide_core::{ + /// payloads::GetMe, + /// requests::{JsonRequest, Request}, + /// types::Me, + /// Bot, + /// }; + /// + /// let bot = Bot::new("TOKEN"); + /// + /// // Note: it's recommended to `Requester` instead of creating requests directly + /// let method = GetMe::new(); + /// let request = JsonRequest::new(bot, method); + /// let request_clone = request.clone(); + /// let _: Me = request.send().await.unwrap(); + /// + /// // You can also just await requests, without calling `send`: + /// let _: Me = request_clone.await.unwrap(); + /// # }; + /// ``` + #[must_use = "Futures are lazy and do nothing unless polled or awaited"] + fn send(self) -> Self::Send; + + /// Send this request by reference. + /// + /// This method is analogous to [`send`](Request::send), but it doesn't take + /// the ownership of `self`. This allows to send the same (or slightly + /// different) requests over and over. + /// + /// Also, it is expected that calling this method is better than just + /// cloning requests. (Because instead of copying all the data + /// and then serializing it, this method should just serialize the data.) + /// + /// ## Examples + /// + /// ``` + /// # async { + /// use teloxide_core::{prelude::*, requests::Request, types::ChatId, Bot}; + /// + /// let bot = Bot::new("TOKEN"); + /// # let chat_ids = vec![1i64, 2, 3, 4].into_iter().map(ChatId).map(Into::into).collect::>(); + /// + /// let mut req = bot.send_message(ChatId(0xAAAAAAAA), "Hi there!"); + /// for chat_id in chat_ids { + /// req.chat_id = chat_id; + /// req.send_ref().await.unwrap(); + /// } + /// # }; + /// ``` + #[must_use = "Futures are lazy and do nothing unless polled or awaited"] + fn send_ref(&self) -> Self::SendRef; + + #[cfg(feature = "erased")] + fn erase<'a>(self) -> crate::adaptors::erased::ErasedRequest<'a, Self::Payload, Self::Err> + where + Self: Sized + 'a, + { + crate::adaptors::erased::ErasedRequest::erase(self) + } +} + +// FIXME: re-introduce `Either` impls once `Either: IntoFuture` (or make out own +// `Either`) (same for `Requester`) + +// impl Request for Either +// where +// L: Request, +// R: Request, +// { +// type Err = L::Err; + +// type Send = future::Either; + +// type SendRef = future::Either; + +// fn send(self) -> Self::Send { +// self.map_left(<_>::send) +// .map_right(<_>::send) +// .either(future::Either::Left, future::Either::Right) +// } + +// fn send_ref(&self) -> Self::SendRef { +// self.as_ref() +// .map_left(<_>::send_ref) +// .map_right(<_>::send_ref) +// .either(future::Either::Left, future::Either::Right) +// } +// } diff --git a/crates/teloxide-core/src/requests/requester.rs b/crates/teloxide-core/src/requests/requester.rs new file mode 100644 index 00000000..3fc819bb --- /dev/null +++ b/crates/teloxide-core/src/requests/requester.rs @@ -0,0 +1,1328 @@ +// We can't change Telegram API +#![allow(clippy::too_many_arguments)] + +use url::Url; + +use crate::{ + payloads::{GetMe, SendMessage, *}, + requests::Request, + types::*, +}; + +/// Telegram Bot API client. +/// +/// This trait is implemented by all bots & bot adaptors. +/// +/// ## Calling Telegram Bot API methods +/// +/// To call Telegram's methods you first need to get a [`Bot`] instance or any +/// other type which implement this trait. +/// +/// Then you can simply call the method you want and pass required parameters to +/// it. Optional parameters can be supplied by calling setters (like +/// `parse_mode` in the example below). Lastly, you need to `.await` the request +/// created in previous steps, to actually send it to telegram and wait for the +/// response. +/// +/// ``` +/// # async { +/// # let chat_id = ChatId(-1); +/// use teloxide_core::{ +/// prelude::*, +/// types::{ChatId, ParseMode}, +/// }; +/// +/// // Bot implements `Requester` +/// let bot = Bot::new("TOKEN"); +/// +/// // Required parameters are supplied to the `Requester` methods: +/// bot.send_message(chat_id, "Text") +/// // Optional parameters can be supplied by calling setters +/// .parse_mode(ParseMode::Html) +/// // To send request to telegram you need to `.await` the request +/// .await?; +/// # Ok::<_, teloxide_core::RequestError>(()) +/// # }; +/// ``` +/// +/// ## Adaptors +/// +/// Similarly to how [`Iterator`] has iterator adaptors ([`FlatMap`], +/// [`Filter`], etc) that wrap an [`Iterator`] and alter its behaviour, Teloxide +/// has a similar story with `Requester`. +/// +/// [`adaptors`] module provides a handful of `Requester` adaptors that can be +/// created via [`RequesterExt`] methods. For example using [`.parse_mode(...)`] +/// on a bot will wrap it in [`DefaultParseMode`] adaptor which sets the parse +/// mode to a default value: +/// +/// ```rust +/// # async { +/// # let chat_id = ChatId(-1); +/// use teloxide_core::{ +/// prelude::*, +/// types::{ChatId, ParseMode}, +/// }; +/// +/// let bot = Bot::new("TOKEN") +/// // Wrap the bot in an adaptor +/// .parse_mode(ParseMode::Html); +/// +/// // This will use `ParseMode::Html` +/// bot.send_message(chat_id, "Text").await?; +/// +/// // This will use `ParseMode::MarkdownV2` +/// bot.send_message(chat_id, "**Text**").parse_mode(ParseMode::MarkdownV2).await?; +/// # Ok::<_, teloxide_core::RequestError>(()) +/// # }; +/// ``` +/// +/// Note that just as with iterators, adaptors change type: +/// +/// ```compile_fail +/// # use teloxide_core::{prelude::*, types::{ChatId, ParseMode}}; +/// let bot: Bot = Bot::new("TOKEN").parse_mode(ParseMode::Html); +/// ``` +/// ```rust +/// # use teloxide_core::{prelude::*, types::{ChatId, ParseMode}, adaptors::DefaultParseMode}; +/// let bot: DefaultParseMode = Bot::new("TOKEN").parse_mode(ParseMode::Html); +/// ``` +/// +/// Because of this it's oftentimes more convinient to have a type alias: +/// +/// ```rust +/// # async { +/// # use teloxide_core::{adaptors::{DefaultParseMode, Throttle}, requests::RequesterExt, types::ParseMode}; +/// type Bot = DefaultParseMode>; +/// +/// let bot: Bot = teloxide_core::Bot::new("TOKEN") +/// .throttle(<_>::default()) +/// .parse_mode(ParseMode::Html); +/// # let _ = bot; +/// # }; +/// ``` +/// +/// Also note that most adaptors require specific cargo features to be enabled. +/// For example, to use [`Throttle`] you need to enable `throttle` feature in +/// your `Cargo.toml`: +/// +/// ```toml +/// teloxide_core = { version = "...", features = ["throttle"] } +/// ``` +/// +/// Refer to adaptor's documentation for information about what features it +/// requires. +/// +/// ## Using `Requester` in a generic context +/// +/// When writing helper function you may be indifferent to which exact type is +/// being used as a bot and instead only care that it implements `Requester` +/// trait. In this case you can use generic bounds to express this exact thing: +/// +/// ``` +/// use teloxide_core::{ +/// prelude::*, +/// types::{ChatId, Message}, +/// }; +/// +/// async fn send_hi(bot: R, chat: ChatId) -> Message +/// where +/// R: Requester, +/// { +/// bot.send_message(chat, "hi").await.expect("error") +/// } +/// +/// // `send_hi` can be called with `Bot`, `DefaultParseMode` and so on, and so forth +/// ``` +/// +/// [`Bot`]: crate::Bot +/// [`FlatMap`]: std::iter::FlatMap +/// [`Filter`]: std::iter::Filter +/// [`adaptors`]: crate::adaptors +/// [`DefaultParseMode`]: crate::adaptors::DefaultParseMode +/// [`Throttle`]: crate::adaptors::Throttle +/// [`RequesterExt`]: crate::requests::RequesterExt +/// [`.parse_mode(...)`]: crate::requests::RequesterExt::parse_mode +#[cfg_attr(all(any(docsrs, dep_docsrs), feature = "nightly"), doc(notable_trait))] +pub trait Requester { + /// Error type returned by all requests. + type Err: std::error::Error + Send; + + // START BLOCK requester_methods + // Generated by `codegen_requester_methods`, do not edit by hand. + + type GetUpdates: Request; + + /// For Telegram documentation see [`GetUpdates`]. + fn get_updates(&self) -> Self::GetUpdates; + + type SetWebhook: Request; + + /// For Telegram documentation see [`SetWebhook`]. + fn set_webhook(&self, url: Url) -> Self::SetWebhook; + + type DeleteWebhook: Request; + + /// For Telegram documentation see [`DeleteWebhook`]. + fn delete_webhook(&self) -> Self::DeleteWebhook; + + type GetWebhookInfo: Request; + + /// For Telegram documentation see [`GetWebhookInfo`]. + fn get_webhook_info(&self) -> Self::GetWebhookInfo; + + type GetMe: Request; + + /// For Telegram documentation see [`GetMe`]. + fn get_me(&self) -> Self::GetMe; + + type LogOut: Request; + + /// For Telegram documentation see [`LogOut`]. + fn log_out(&self) -> Self::LogOut; + + type Close: Request; + + /// For Telegram documentation see [`Close`]. + fn close(&self) -> Self::Close; + + type SendMessage: Request; + + /// For Telegram documentation see [`SendMessage`]. + fn send_message(&self, chat_id: C, text: T) -> Self::SendMessage + where + C: Into, + T: Into; + + type ForwardMessage: Request; + + /// For Telegram documentation see [`ForwardMessage`]. + fn forward_message( + &self, + chat_id: C, + from_chat_id: F, + message_id: MessageId, + ) -> Self::ForwardMessage + where + C: Into, + F: Into; + + type CopyMessage: Request; + + /// For Telegram documentation see [`CopyMessage`]. + fn copy_message( + &self, + chat_id: C, + from_chat_id: F, + message_id: MessageId, + ) -> Self::CopyMessage + where + C: Into, + F: Into; + + type SendPhoto: Request; + + /// For Telegram documentation see [`SendPhoto`]. + fn send_photo(&self, chat_id: C, photo: InputFile) -> Self::SendPhoto + where + C: Into; + + type SendAudio: Request; + + /// For Telegram documentation see [`SendAudio`]. + fn send_audio(&self, chat_id: C, audio: InputFile) -> Self::SendAudio + where + C: Into; + + type SendDocument: Request; + + /// For Telegram documentation see [`SendDocument`]. + fn send_document(&self, chat_id: C, document: InputFile) -> Self::SendDocument + where + C: Into; + + type SendVideo: Request; + + /// For Telegram documentation see [`SendVideo`]. + fn send_video(&self, chat_id: C, video: InputFile) -> Self::SendVideo + where + C: Into; + + type SendAnimation: Request; + + /// For Telegram documentation see [`SendAnimation`]. + fn send_animation(&self, chat_id: C, animation: InputFile) -> Self::SendAnimation + where + C: Into; + + type SendVoice: Request; + + /// For Telegram documentation see [`SendVoice`]. + fn send_voice(&self, chat_id: C, voice: InputFile) -> Self::SendVoice + where + C: Into; + + type SendVideoNote: Request; + + /// For Telegram documentation see [`SendVideoNote`]. + fn send_video_note(&self, chat_id: C, video_note: InputFile) -> Self::SendVideoNote + where + C: Into; + + type SendMediaGroup: Request; + + /// For Telegram documentation see [`SendMediaGroup`]. + fn send_media_group(&self, chat_id: C, media: M) -> Self::SendMediaGroup + where + C: Into, + M: IntoIterator; + + type SendLocation: Request; + + /// For Telegram documentation see [`SendLocation`]. + fn send_location(&self, chat_id: C, latitude: f64, longitude: f64) -> Self::SendLocation + where + C: Into; + + type EditMessageLiveLocation: Request; + + /// For Telegram documentation see [`EditMessageLiveLocation`]. + fn edit_message_live_location( + &self, + chat_id: C, + message_id: MessageId, + latitude: f64, + longitude: f64, + ) -> Self::EditMessageLiveLocation + where + C: Into; + + type EditMessageLiveLocationInline: Request< + Payload = EditMessageLiveLocationInline, + Err = Self::Err, + >; + + /// For Telegram documentation see [`EditMessageLiveLocationInline`]. + fn edit_message_live_location_inline( + &self, + inline_message_id: I, + latitude: f64, + longitude: f64, + ) -> Self::EditMessageLiveLocationInline + where + I: Into; + + type StopMessageLiveLocation: Request; + + /// For Telegram documentation see [`StopMessageLiveLocation`]. + fn stop_message_live_location( + &self, + chat_id: C, + message_id: MessageId, + latitude: f64, + longitude: f64, + ) -> Self::StopMessageLiveLocation + where + C: Into; + + type StopMessageLiveLocationInline: Request< + Payload = StopMessageLiveLocationInline, + Err = Self::Err, + >; + + /// For Telegram documentation see [`StopMessageLiveLocationInline`]. + fn stop_message_live_location_inline( + &self, + inline_message_id: I, + latitude: f64, + longitude: f64, + ) -> Self::StopMessageLiveLocationInline + where + I: Into; + + type SendVenue: Request; + + /// For Telegram documentation see [`SendVenue`]. + fn send_venue( + &self, + chat_id: C, + latitude: f64, + longitude: f64, + title: T, + address: A, + ) -> Self::SendVenue + where + C: Into, + T: Into, + A: Into; + + type SendContact: Request; + + /// For Telegram documentation see [`SendContact`]. + fn send_contact( + &self, + chat_id: C, + phone_number: P, + first_name: F, + ) -> Self::SendContact + where + C: Into, + P: Into, + F: Into; + + type SendPoll: Request; + + /// For Telegram documentation see [`SendPoll`]. + fn send_poll(&self, chat_id: C, question: Q, options: O) -> Self::SendPoll + where + C: Into, + Q: Into, + O: IntoIterator; + + type SendDice: Request; + + /// For Telegram documentation see [`SendDice`]. + fn send_dice(&self, chat_id: C) -> Self::SendDice + where + C: Into; + + type SendChatAction: Request; + + /// For Telegram documentation see [`SendChatAction`]. + fn send_chat_action(&self, chat_id: C, action: ChatAction) -> Self::SendChatAction + where + C: Into; + + type GetUserProfilePhotos: Request; + + /// For Telegram documentation see [`GetUserProfilePhotos`]. + fn get_user_profile_photos(&self, user_id: UserId) -> Self::GetUserProfilePhotos; + + type GetFile: Request; + + /// For Telegram documentation see [`GetFile`]. + fn get_file(&self, file_id: F) -> Self::GetFile + where + F: Into; + + type BanChatMember: Request; + + /// For Telegram documentation see [`BanChatMember`]. + fn ban_chat_member(&self, chat_id: C, user_id: UserId) -> Self::BanChatMember + where + C: Into; + + type KickChatMember: Request; + + /// For Telegram documentation see [`KickChatMember`]. + fn kick_chat_member(&self, chat_id: C, user_id: UserId) -> Self::KickChatMember + where + C: Into; + + type UnbanChatMember: Request; + + /// For Telegram documentation see [`UnbanChatMember`]. + fn unban_chat_member(&self, chat_id: C, user_id: UserId) -> Self::UnbanChatMember + where + C: Into; + + type RestrictChatMember: Request; + + /// For Telegram documentation see [`RestrictChatMember`]. + fn restrict_chat_member( + &self, + chat_id: C, + user_id: UserId, + permissions: ChatPermissions, + ) -> Self::RestrictChatMember + where + C: Into; + + type PromoteChatMember: Request; + + /// For Telegram documentation see [`PromoteChatMember`]. + fn promote_chat_member(&self, chat_id: C, user_id: UserId) -> Self::PromoteChatMember + where + C: Into; + + type SetChatAdministratorCustomTitle: Request< + Payload = SetChatAdministratorCustomTitle, + Err = Self::Err, + >; + + /// For Telegram documentation see [`SetChatAdministratorCustomTitle`]. + fn set_chat_administrator_custom_title( + &self, + chat_id: Ch, + user_id: UserId, + custom_title: C, + ) -> Self::SetChatAdministratorCustomTitle + where + Ch: Into, + C: Into; + + type BanChatSenderChat: Request; + + /// For Telegram documentation see [`BanChatSenderChat`]. + fn ban_chat_sender_chat(&self, chat_id: C, sender_chat_id: S) -> Self::BanChatSenderChat + where + C: Into, + S: Into; + + type UnbanChatSenderChat: Request; + + /// For Telegram documentation see [`UnbanChatSenderChat`]. + fn unban_chat_sender_chat( + &self, + chat_id: C, + sender_chat_id: S, + ) -> Self::UnbanChatSenderChat + where + C: Into, + S: Into; + + type SetChatPermissions: Request; + + /// For Telegram documentation see [`SetChatPermissions`]. + fn set_chat_permissions( + &self, + chat_id: C, + permissions: ChatPermissions, + ) -> Self::SetChatPermissions + where + C: Into; + + type ExportChatInviteLink: Request; + + /// For Telegram documentation see [`ExportChatInviteLink`]. + fn export_chat_invite_link(&self, chat_id: C) -> Self::ExportChatInviteLink + where + C: Into; + + type CreateChatInviteLink: Request; + + /// For Telegram documentation see [`CreateChatInviteLink`]. + fn create_chat_invite_link(&self, chat_id: C) -> Self::CreateChatInviteLink + where + C: Into; + + type EditChatInviteLink: Request; + + /// For Telegram documentation see [`EditChatInviteLink`]. + fn edit_chat_invite_link(&self, chat_id: C, invite_link: I) -> Self::EditChatInviteLink + where + C: Into, + I: Into; + + type RevokeChatInviteLink: Request; + + /// For Telegram documentation see [`RevokeChatInviteLink`]. + fn revoke_chat_invite_link( + &self, + chat_id: C, + invite_link: I, + ) -> Self::RevokeChatInviteLink + where + C: Into, + I: Into; + + type ApproveChatJoinRequest: Request; + + /// For Telegram documentation see [`ApproveChatJoinRequest`]. + fn approve_chat_join_request( + &self, + chat_id: C, + user_id: UserId, + ) -> Self::ApproveChatJoinRequest + where + C: Into; + + type DeclineChatJoinRequest: Request; + + /// For Telegram documentation see [`DeclineChatJoinRequest`]. + fn decline_chat_join_request( + &self, + chat_id: C, + user_id: UserId, + ) -> Self::DeclineChatJoinRequest + where + C: Into; + + type SetChatPhoto: Request; + + /// For Telegram documentation see [`SetChatPhoto`]. + fn set_chat_photo(&self, chat_id: C, photo: InputFile) -> Self::SetChatPhoto + where + C: Into; + + type DeleteChatPhoto: Request; + + /// For Telegram documentation see [`DeleteChatPhoto`]. + fn delete_chat_photo(&self, chat_id: C) -> Self::DeleteChatPhoto + where + C: Into; + + type SetChatTitle: Request; + + /// For Telegram documentation see [`SetChatTitle`]. + fn set_chat_title(&self, chat_id: C, title: T) -> Self::SetChatTitle + where + C: Into, + T: Into; + + type SetChatDescription: Request; + + /// For Telegram documentation see [`SetChatDescription`]. + fn set_chat_description(&self, chat_id: C) -> Self::SetChatDescription + where + C: Into; + + type PinChatMessage: Request; + + /// For Telegram documentation see [`PinChatMessage`]. + fn pin_chat_message(&self, chat_id: C, message_id: MessageId) -> Self::PinChatMessage + where + C: Into; + + type UnpinChatMessage: Request; + + /// For Telegram documentation see [`UnpinChatMessage`]. + fn unpin_chat_message(&self, chat_id: C) -> Self::UnpinChatMessage + where + C: Into; + + type UnpinAllChatMessages: Request; + + /// For Telegram documentation see [`UnpinAllChatMessages`]. + fn unpin_all_chat_messages(&self, chat_id: C) -> Self::UnpinAllChatMessages + where + C: Into; + + type LeaveChat: Request; + + /// For Telegram documentation see [`LeaveChat`]. + fn leave_chat(&self, chat_id: C) -> Self::LeaveChat + where + C: Into; + + type GetChat: Request; + + /// For Telegram documentation see [`GetChat`]. + fn get_chat(&self, chat_id: C) -> Self::GetChat + where + C: Into; + + type GetChatAdministrators: Request; + + /// For Telegram documentation see [`GetChatAdministrators`]. + fn get_chat_administrators(&self, chat_id: C) -> Self::GetChatAdministrators + where + C: Into; + + type GetChatMemberCount: Request; + + /// For Telegram documentation see [`GetChatMemberCount`]. + fn get_chat_member_count(&self, chat_id: C) -> Self::GetChatMemberCount + where + C: Into; + + type GetChatMembersCount: Request; + + /// For Telegram documentation see [`GetChatMembersCount`]. + fn get_chat_members_count(&self, chat_id: C) -> Self::GetChatMembersCount + where + C: Into; + + type GetChatMember: Request; + + /// For Telegram documentation see [`GetChatMember`]. + fn get_chat_member(&self, chat_id: C, user_id: UserId) -> Self::GetChatMember + where + C: Into; + + type SetChatStickerSet: Request; + + /// For Telegram documentation see [`SetChatStickerSet`]. + fn set_chat_sticker_set( + &self, + chat_id: C, + sticker_set_name: S, + ) -> Self::SetChatStickerSet + where + C: Into, + S: Into; + + type DeleteChatStickerSet: Request; + + /// For Telegram documentation see [`DeleteChatStickerSet`]. + fn delete_chat_sticker_set(&self, chat_id: C) -> Self::DeleteChatStickerSet + where + C: Into; + + type AnswerCallbackQuery: Request; + + /// For Telegram documentation see [`AnswerCallbackQuery`]. + fn answer_callback_query(&self, callback_query_id: C) -> Self::AnswerCallbackQuery + where + C: Into; + + type SetMyCommands: Request; + + /// For Telegram documentation see [`SetMyCommands`]. + fn set_my_commands(&self, commands: C) -> Self::SetMyCommands + where + C: IntoIterator; + + type GetMyCommands: Request; + + /// For Telegram documentation see [`GetMyCommands`]. + fn get_my_commands(&self) -> Self::GetMyCommands; + + type SetChatMenuButton: Request; + + /// For Telegram documentation see [`SetChatMenuButton`]. + fn set_chat_menu_button(&self) -> Self::SetChatMenuButton; + + type GetChatMenuButton: Request; + + /// For Telegram documentation see [`GetChatMenuButton`]. + fn get_chat_menu_button(&self) -> Self::GetChatMenuButton; + + type SetMyDefaultAdministratorRights: Request< + Payload = SetMyDefaultAdministratorRights, + Err = Self::Err, + >; + + /// For Telegram documentation see [`SetMyDefaultAdministratorRights`]. + fn set_my_default_administrator_rights(&self) -> Self::SetMyDefaultAdministratorRights; + + type GetMyDefaultAdministratorRights: Request< + Payload = GetMyDefaultAdministratorRights, + Err = Self::Err, + >; + + /// For Telegram documentation see [`GetMyDefaultAdministratorRights`]. + fn get_my_default_administrator_rights(&self) -> Self::GetMyDefaultAdministratorRights; + + type DeleteMyCommands: Request; + + /// For Telegram documentation see [`DeleteMyCommands`]. + fn delete_my_commands(&self) -> Self::DeleteMyCommands; + + type AnswerInlineQuery: Request; + + /// For Telegram documentation see [`AnswerInlineQuery`]. + fn answer_inline_query(&self, inline_query_id: I, results: R) -> Self::AnswerInlineQuery + where + I: Into, + R: IntoIterator; + + type AnswerWebAppQuery: Request; + + /// For Telegram documentation see [`AnswerWebAppQuery`]. + fn answer_web_app_query( + &self, + web_app_query_id: W, + result: InlineQueryResult, + ) -> Self::AnswerWebAppQuery + where + W: Into; + + type EditMessageText: Request; + + /// For Telegram documentation see [`EditMessageText`]. + fn edit_message_text( + &self, + chat_id: C, + message_id: MessageId, + text: T, + ) -> Self::EditMessageText + where + C: Into, + T: Into; + + type EditMessageTextInline: Request; + + /// For Telegram documentation see [`EditMessageTextInline`]. + fn edit_message_text_inline( + &self, + inline_message_id: I, + text: T, + ) -> Self::EditMessageTextInline + where + I: Into, + T: Into; + + type EditMessageCaption: Request; + + /// For Telegram documentation see [`EditMessageCaption`]. + fn edit_message_caption( + &self, + chat_id: C, + message_id: MessageId, + ) -> Self::EditMessageCaption + where + C: Into; + + type EditMessageCaptionInline: Request; + + /// For Telegram documentation see [`EditMessageCaptionInline`]. + fn edit_message_caption_inline( + &self, + inline_message_id: I, + ) -> Self::EditMessageCaptionInline + where + I: Into; + + type EditMessageMedia: Request; + + /// For Telegram documentation see [`EditMessageMedia`]. + fn edit_message_media( + &self, + chat_id: C, + message_id: MessageId, + media: InputMedia, + ) -> Self::EditMessageMedia + where + C: Into; + + type EditMessageMediaInline: Request; + + /// For Telegram documentation see [`EditMessageMediaInline`]. + fn edit_message_media_inline( + &self, + inline_message_id: I, + media: InputMedia, + ) -> Self::EditMessageMediaInline + where + I: Into; + + type EditMessageReplyMarkup: Request; + + /// For Telegram documentation see [`EditMessageReplyMarkup`]. + fn edit_message_reply_markup( + &self, + chat_id: C, + message_id: MessageId, + ) -> Self::EditMessageReplyMarkup + where + C: Into; + + type EditMessageReplyMarkupInline: Request< + Payload = EditMessageReplyMarkupInline, + Err = Self::Err, + >; + + /// For Telegram documentation see [`EditMessageReplyMarkupInline`]. + fn edit_message_reply_markup_inline( + &self, + inline_message_id: I, + ) -> Self::EditMessageReplyMarkupInline + where + I: Into; + + type StopPoll: Request; + + /// For Telegram documentation see [`StopPoll`]. + fn stop_poll(&self, chat_id: C, message_id: MessageId) -> Self::StopPoll + where + C: Into; + + type DeleteMessage: Request; + + /// For Telegram documentation see [`DeleteMessage`]. + fn delete_message(&self, chat_id: C, message_id: MessageId) -> Self::DeleteMessage + where + C: Into; + + type SendSticker: Request; + + /// For Telegram documentation see [`SendSticker`]. + fn send_sticker(&self, chat_id: C, sticker: InputFile) -> Self::SendSticker + where + C: Into; + + type GetStickerSet: Request; + + /// For Telegram documentation see [`GetStickerSet`]. + fn get_sticker_set(&self, name: N) -> Self::GetStickerSet + where + N: Into; + + type GetCustomEmojiStickers: Request; + + /// For Telegram documentation see [`GetCustomEmojiStickers`]. + fn get_custom_emoji_stickers(&self, custom_emoji_ids: C) -> Self::GetCustomEmojiStickers + where + C: IntoIterator; + + type UploadStickerFile: Request; + + /// For Telegram documentation see [`UploadStickerFile`]. + fn upload_sticker_file( + &self, + user_id: UserId, + png_sticker: InputFile, + ) -> Self::UploadStickerFile; + + type CreateNewStickerSet: Request; + + /// For Telegram documentation see [`CreateNewStickerSet`]. + fn create_new_sticker_set( + &self, + user_id: UserId, + name: N, + title: T, + sticker: InputSticker, + emojis: E, + ) -> Self::CreateNewStickerSet + where + N: Into, + T: Into, + E: Into; + + type AddStickerToSet: Request; + + /// For Telegram documentation see [`AddStickerToSet`]. + fn add_sticker_to_set( + &self, + user_id: UserId, + name: N, + sticker: InputSticker, + emojis: E, + ) -> Self::AddStickerToSet + where + N: Into, + E: Into; + + type SetStickerPositionInSet: Request; + + /// For Telegram documentation see [`SetStickerPositionInSet`]. + fn set_sticker_position_in_set( + &self, + sticker: S, + position: u32, + ) -> Self::SetStickerPositionInSet + where + S: Into; + + type DeleteStickerFromSet: Request; + + /// For Telegram documentation see [`DeleteStickerFromSet`]. + fn delete_sticker_from_set(&self, sticker: S) -> Self::DeleteStickerFromSet + where + S: Into; + + type SetStickerSetThumb: Request; + + /// For Telegram documentation see [`SetStickerSetThumb`]. + fn set_sticker_set_thumb(&self, name: N, user_id: UserId) -> Self::SetStickerSetThumb + where + N: Into; + + type SendInvoice: Request; + + /// For Telegram documentation see [`SendInvoice`]. + fn send_invoice( + &self, + chat_id: Ch, + title: T, + description: D, + payload: Pa, + provider_token: P, + currency: C, + prices: Pri, + ) -> Self::SendInvoice + where + Ch: Into, + T: Into, + D: Into, + Pa: Into, + P: Into, + C: Into, + Pri: IntoIterator; + + type CreateInvoiceLink: Request; + + /// For Telegram documentation see [`CreateInvoiceLink`]. + fn create_invoice_link( + &self, + title: T, + description: D, + payload: Pa, + provider_token: P, + currency: C, + prices: Pri, + ) -> Self::CreateInvoiceLink + where + T: Into, + D: Into, + Pa: Into, + P: Into, + C: Into, + Pri: IntoIterator; + + type AnswerShippingQuery: Request; + + /// For Telegram documentation see [`AnswerShippingQuery`]. + fn answer_shipping_query(&self, shipping_query_id: S, ok: bool) -> Self::AnswerShippingQuery + where + S: Into; + + type AnswerPreCheckoutQuery: Request; + + /// For Telegram documentation see [`AnswerPreCheckoutQuery`]. + fn answer_pre_checkout_query

( + &self, + pre_checkout_query_id: P, + ok: bool, + ) -> Self::AnswerPreCheckoutQuery + where + P: Into; + + type SetPassportDataErrors: Request; + + /// For Telegram documentation see [`SetPassportDataErrors`]. + fn set_passport_data_errors( + &self, + user_id: UserId, + errors: E, + ) -> Self::SetPassportDataErrors + where + E: IntoIterator; + + type SendGame: Request; + + /// For Telegram documentation see [`SendGame`]. + fn send_game(&self, chat_id: u32, game_short_name: G) -> Self::SendGame + where + G: Into; + + type SetGameScore: Request; + + /// For Telegram documentation see [`SetGameScore`]. + fn set_game_score( + &self, + user_id: UserId, + score: u64, + chat_id: u32, + message_id: MessageId, + ) -> Self::SetGameScore; + + type SetGameScoreInline: Request; + + /// For Telegram documentation see [`SetGameScoreInline`]. + fn set_game_score_inline( + &self, + user_id: UserId, + score: u64, + inline_message_id: I, + ) -> Self::SetGameScoreInline + where + I: Into; + + type GetGameHighScores: Request; + + /// For Telegram documentation see [`GetGameHighScores`]. + fn get_game_high_scores(&self, user_id: UserId, target: T) -> Self::GetGameHighScores + where + T: Into; + // END BLOCK requester_methods +} + +macro_rules! fty { + ($T:ident) => { + B::$T + }; +} + +macro_rules! fwd_deref { + ($m:ident $this:ident ($($arg:ident : $T:ty),*)) => { + core::ops::Deref::deref($this).$m($($arg),*) + }; +} + +macro_rules! forward_all { + ($body:ident, $ty:ident) => { + requester_forward! { + get_me, + log_out, + close, + get_updates, + set_webhook, + delete_webhook, + get_webhook_info, + forward_message, + copy_message, + send_message, + send_photo, + send_audio, + send_document, + send_video, + send_animation, + send_voice, + send_video_note, + send_media_group, + send_location, + edit_message_live_location, + edit_message_live_location_inline, + stop_message_live_location, + stop_message_live_location_inline, + send_venue, + send_contact, + send_poll, + send_dice, + send_chat_action, + get_user_profile_photos, + get_file, + kick_chat_member, + ban_chat_member, + unban_chat_member, + restrict_chat_member, + promote_chat_member, + set_chat_administrator_custom_title, + ban_chat_sender_chat, + unban_chat_sender_chat, + set_chat_permissions, + export_chat_invite_link, + create_chat_invite_link, + edit_chat_invite_link, + revoke_chat_invite_link, + set_chat_photo, + delete_chat_photo, + set_chat_title, + set_chat_description, + pin_chat_message, + unpin_chat_message, + unpin_all_chat_messages, + leave_chat, + get_chat, + get_chat_administrators, + get_chat_members_count, + get_chat_member_count, + get_chat_member, + set_chat_sticker_set, + delete_chat_sticker_set, + answer_callback_query, + set_my_commands, + get_my_commands, + set_chat_menu_button, + get_chat_menu_button, + set_my_default_administrator_rights, + get_my_default_administrator_rights, + delete_my_commands, + answer_inline_query, + answer_web_app_query, + edit_message_text, + edit_message_text_inline, + edit_message_caption, + edit_message_caption_inline, + edit_message_media, + edit_message_media_inline, + edit_message_reply_markup, + edit_message_reply_markup_inline, + stop_poll, + delete_message, + send_sticker, + get_sticker_set, + get_custom_emoji_stickers, + upload_sticker_file, + create_new_sticker_set, + add_sticker_to_set, + set_sticker_position_in_set, + delete_sticker_from_set, + set_sticker_set_thumb, + send_invoice, + create_invoice_link, + answer_shipping_query, + answer_pre_checkout_query, + set_passport_data_errors, + send_game, + set_game_score, + set_game_score_inline, + get_game_high_scores, + approve_chat_join_request, + decline_chat_join_request + => $body, $ty + } + }; + () => { + forward_all! { fwd_deref, fty } + }; +} + +impl Requester for &B +where + B: Requester, +{ + type Err = B::Err; + + forward_all! {} +} + +impl Requester for &mut B +where + B: Requester, +{ + type Err = B::Err; + + forward_all! {} +} + +impl Requester for Box +where + B: Requester, +{ + type Err = B::Err; + + forward_all! {} +} + +impl Requester for std::sync::Arc +where + B: Requester, +{ + type Err = B::Err; + + forward_all! {} +} + +impl Requester for std::rc::Rc +where + B: Requester, +{ + type Err = B::Err; + + forward_all! {} +} + +// macro_rules! fty_either { +// ($T:ident) => { +// either::Either +// }; +// } + +// macro_rules! fwd_either { +// ($m:ident $this:ident ($($arg:ident : $T:ty),*)) => { +// match ($this) { +// either::Either::Left(l) => either::Either::Left(l.$m($($arg),*)), +// either::Either::Right(r) => +// either::Either::Right(r.$m($($arg),*)), } +// }; +// } + +// impl Requester for either::Either +// where +// LR: Requester, +// RR: Requester, +// { +// type Err = LR::Err; + +// forward_all! { fwd_either, fty_either } +// } + +#[test] +fn codegen_requester_methods() { + use crate::codegen::{ + add_hidden_preamble, + convert::{convert_for, Convert}, + ensure_file_contents, min_prefix, project_root, reformat, replace_block, + schema::{self, Type}, + to_uppercase, + }; + use indexmap::IndexMap; + use itertools::Itertools; + + let schema = schema::get(); + + let methods = schema + .methods + .iter() + .map(|m| { + let mut convert_params = m + .params + .iter() + .filter(|p| !matches!(p.ty, Type::Option(_))) + .map(|p| (&p.name, convert_for(&p.ty))) + .filter(|(_, c)| !matches!(c, Convert::Id(_))) + .map(|(name, _)| &**name) + .collect::>(); + + convert_params.sort_unstable(); + + let prefixes: IndexMap<_, _> = convert_params + .iter() + .copied() + // Workaround to output the last type as the first letter + .chain(["\0"]) + .tuple_windows() + .map(|(l, r)| (l, min_prefix(l, r))) + .collect(); + + let args = m + .params + .iter() + .filter(|p| !matches!(p.ty, schema::Type::Option(_))) + .map(|p| match prefixes.get(&*p.name) { + Some(prefix) => format!("{}: {}", p.name, to_uppercase(prefix)), + None => format!("{}: {}", p.name, p.ty), + }) + .join(", "); + + let generics = m + .params + .iter() + .flat_map(|p| prefixes.get(&*p.name)) + .copied() + .map(to_uppercase) + .join(", "); + let where_clause = m + .params + .iter() + .filter(|p| !matches!(p.ty, Type::Option(_))) + .flat_map(|p| match convert_for(&p.ty) { + Convert::Id(_) => None, + Convert::Into(ty) => { + Some(format!("{}: Into<{}>", &to_uppercase(prefixes[&*p.name]), ty)) + } + Convert::Collect(ty) => Some(format!( + "{}: IntoIterator", + &to_uppercase(prefixes[&*p.name]), + ty + )), + }) + .join(",\n "); + + let generics = + if generics.is_empty() { String::from("") } else { format!("<{}>", generics) }; + + let where_clause = if where_clause.is_empty() { + String::from("") + } else { + format!(" where {}", where_clause) + }; + + format!( + " +type {Method}: Request; + +/// For Telegram documentation see [`{Method}`]. +fn {method} {generics} (&self, {args}) -> Self::{Method}{where_clause}; + ", + Method = m.names.1, + method = m.names.2, + ) + }) + .collect::(); + + let path = project_root().join("src/requests/requester.rs"); + + ensure_file_contents( + &path, + &reformat(replace_block( + &path, + "requester_methods", + &add_hidden_preamble("codegen_requester_methods", methods), + )), + ); +} diff --git a/crates/teloxide-core/src/requests/requester_ext.rs b/crates/teloxide-core/src/requests/requester_ext.rs new file mode 100644 index 00000000..35509409 --- /dev/null +++ b/crates/teloxide-core/src/requests/requester_ext.rs @@ -0,0 +1,117 @@ +use crate::{adaptors::DefaultParseMode, requests::Requester, types::ParseMode}; + +#[cfg(feature = "cache_me")] +use crate::adaptors::CacheMe; + +#[cfg(feature = "auto_send")] +#[allow(deprecated)] +use crate::adaptors::AutoSend; + +#[cfg(feature = "erased")] +use crate::adaptors::ErasedRequester; + +#[cfg(feature = "trace_adaptor")] +use crate::adaptors::trace::{Settings, Trace}; + +#[cfg(feature = "throttle")] +use crate::adaptors::throttle::{Limits, Throttle}; + +/// Extensions methods for [`Requester`]. +pub trait RequesterExt: Requester { + /// Add `get_me` caching ability, see [`CacheMe`] for more. + #[cfg(feature = "cache_me")] + fn cache_me(self) -> CacheMe + where + Self: Sized, + { + CacheMe::new(self) + } + + /// Send requests automatically, see [`AutoSend`] for more. + #[cfg(feature = "auto_send")] + #[deprecated( + since = "0.8.0", + note = "`AutoSend` is no longer required to `.await` requests and is now noop" + )] + #[allow(deprecated)] + fn auto_send(self) -> AutoSend + where + Self: Sized, + { + AutoSend::new(self) + } + + /// Erase requester type. + #[cfg(feature = "erased")] + fn erase<'a>(self) -> ErasedRequester<'a, Self::Err> + where + Self: 'a, + Self: Sized, + { + ErasedRequester::new(self) + } + + /// Trace requests, see [`Trace`] for more. + #[cfg(feature = "trace_adaptor")] + fn trace(self, settings: Settings) -> Trace + where + Self: Sized, + { + Trace::new(self, settings) + } + + /// Add throttling ability, see [`Throttle`] for more. + /// + /// Note: this spawns the worker, just as [`Throttle::new_spawn`]. + #[cfg(feature = "throttle")] + fn throttle(self, limits: Limits) -> Throttle + where + Self: Sized + Clone + Send + Sync + 'static, + Self::Err: crate::errors::AsResponseParameters, + Self::GetChat: Send, + { + Throttle::new_spawn(self, limits) + } + + /// Specifies default [`ParseMode`], which will be used during all calls to: + /// + /// - [`send_message`] + /// - [`send_photo`] + /// - [`send_video`] + /// - [`send_audio`] + /// - [`send_document`] + /// - [`send_animation`] + /// - [`send_voice`] + /// - [`send_poll`] + /// - [`edit_message_text`] (and [`edit_message_text_inline`]) + /// - [`edit_message_caption`] (and [`edit_message_caption_inline`]) + /// + /// [`send_message`]: crate::requests::Requester::send_message + /// [`send_photo`]: crate::requests::Requester::send_photo + /// [`send_video`]: crate::requests::Requester::send_video + /// [`send_audio`]: crate::requests::Requester::send_audio + /// [`send_document`]: crate::requests::Requester::send_document + /// [`send_animation`]: crate::requests::Requester::send_animation + /// [`send_voice`]: crate::requests::Requester::send_voice + /// [`send_poll`]: crate::requests::Requester::send_poll + /// [`edit_message_text`]: crate::requests::Requester::edit_message_text + /// [`edit_message_text_inline`]: + /// crate::requests::Requester::edit_message_text_inline + /// [`edit_message_caption`]: + /// crate::requests::Requester::edit_message_caption + /// [`edit_message_caption_inline`]: + /// crate::requests::Requester::edit_message_caption_inline + fn parse_mode(self, parse_mode: ParseMode) -> DefaultParseMode + where + Self: Sized, + { + DefaultParseMode::new(self, parse_mode) + } +} + +impl RequesterExt for T +where + T: Requester, +{ + /* use default impls */ +} diff --git a/crates/teloxide-core/src/requests/utils.rs b/crates/teloxide-core/src/requests/utils.rs new file mode 100644 index 00000000..36e7510e --- /dev/null +++ b/crates/teloxide-core/src/requests/utils.rs @@ -0,0 +1,16 @@ +use bytes::{Bytes, BytesMut}; +use tokio_util::codec::Decoder; + +struct FileDecoder; + +impl Decoder for FileDecoder { + type Item = Bytes; + type Error = std::io::Error; + + fn decode(&mut self, src: &mut BytesMut) -> Result, Self::Error> { + if src.is_empty() { + return Ok(None); + } + Ok(Some(src.split().freeze())) + } +} diff --git a/crates/teloxide-core/src/serde_multipart/error.rs b/crates/teloxide-core/src/serde_multipart/error.rs new file mode 100644 index 00000000..427f2da9 --- /dev/null +++ b/crates/teloxide-core/src/serde_multipart/error.rs @@ -0,0 +1,53 @@ +use std::fmt; + +use serde::ser; + +use crate::RequestError; + +#[derive(Debug, derive_more::From)] +pub(crate) enum Error { + Custom(String), + TopLevelNotStruct, + Fmt(std::fmt::Error), + Io(std::io::Error), + Json(serde_json::Error), +} + +impl ser::Error for Error { + fn custom(msg: T) -> Self + where + T: fmt::Display, + { + Self::Custom(msg.to_string()) + } +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Custom(s) => write!(f, "Custom serde error: {}", s), + Self::TopLevelNotStruct => write!(f, "Multipart supports only structs at top level"), + Self::Fmt(inner) => write!(f, "Formatting error: {}", inner), + Self::Io(inner) => write!(f, "Io error: {}", inner), + Self::Json(inner) => write!(f, "Json (de)serialization error: {}", inner), + } + } +} + +impl std::error::Error for Error {} + +impl From for RequestError { + fn from(err: Error) -> Self { + match err { + Error::Io(ioerr) => RequestError::Io(ioerr), + + // This should be ok since we (hopefuly) don't write request those may trigger errors + // and `Error` is internal. + e => unreachable!( + "we don't create requests those fail to serialize (if you see this, open an issue \ + :|): {}", + e + ), + } + } +} diff --git a/crates/teloxide-core/src/serde_multipart/mod.rs b/crates/teloxide-core/src/serde_multipart/mod.rs new file mode 100644 index 00000000..ffa7ad09 --- /dev/null +++ b/crates/teloxide-core/src/serde_multipart/mod.rs @@ -0,0 +1,187 @@ +//! Module for serializing into `multipart/form-data` +//! ([`reqwest::multipart::Form`]) +//! +//! [`reqwest::multipart::Form`]: reqwest::multipart::Form +//! +//! ## How it works +//! +//! You better not know... +//! +//! This whole module is an awful hack and we'll probably stop using it in next +//! versions (in favor of something less automatic, but more simple). + +mod error; +mod serializers; + +use std::future::Future; + +use reqwest::multipart::Form; +use serde::Serialize; + +use crate::requests::MultipartPayload; +use error::Error; +use serializers::MultipartSerializer; + +/// Serializes given value into [`Form`] **taking all input files out**. +/// +/// [`Form`]: reqwest::multipart::Form +pub(crate) fn to_form(val: &mut T) -> Result, Error> +where + T: Serialize + MultipartPayload, +{ + let mut form = val.serialize(MultipartSerializer::new())?; + + let mut vec = Vec::with_capacity(1); + val.move_files(&mut |f| vec.push(f)); + let iter = vec.into_iter(); + + let fut = async move { + for file in iter { + if file.needs_attach() { + let id = file.id().to_owned(); + if let Some(part) = file.into_part() { + form = form.part(id, part.await); + } + } + } + + form + }; + + Ok(fut) +} + +/// Serializes given value into [`Form`]. +/// +/// [`Form`]: reqwest::multipart::Form +pub(crate) fn to_form_ref(val: &T) -> Result, Error> +where + T: Serialize + MultipartPayload, +{ + let mut form = val.serialize(MultipartSerializer::new())?; + let mut vec = Vec::with_capacity(1); + val.copy_files(&mut |f| vec.push(f)); + + let iter = vec.into_iter(); + + let fut = async move { + for file in iter { + if file.needs_attach() { + let id = file.id().to_owned(); + if let Some(part) = file.into_part() { + form = form.part(id, part.await); + } + } + } + + form + }; + + Ok(fut) +} + +#[cfg(test)] +mod tests { + use tokio::fs::File; + + use super::to_form_ref; + use crate::{ + payloads::{self, setters::*}, + types::{ + ChatId, InputFile, InputMedia, InputMediaAnimation, InputMediaAudio, + InputMediaDocument, InputMediaPhoto, InputMediaVideo, InputSticker, MessageEntity, + MessageEntityKind, ParseMode, UserId, + }, + }; + + // https://github.com/teloxide/teloxide/issues/473 + #[tokio::test] + async fn issue_473() { + to_form_ref( + &payloads::SendPhoto::new(ChatId(0), InputFile::file_id("0")).caption_entities([ + MessageEntity { kind: MessageEntityKind::Url, offset: 0, length: 0 }, + ]), + ) + .unwrap() + .await; + } + + #[tokio::test] + async fn test_send_media_group() { + const CAPTION: &str = "caption"; + + to_form_ref(&payloads::SendMediaGroup::new( + ChatId(0), + [ + InputMedia::Photo( + InputMediaPhoto::new(InputFile::file("../../media/teloxide-core-logo.png")) + .caption(CAPTION) + .parse_mode(ParseMode::MarkdownV2) + .caption_entities(entities()), + ), + InputMedia::Video( + InputMediaVideo::new(InputFile::file_id("17")).supports_streaming(true), + ), + InputMedia::Animation( + InputMediaAnimation::new(InputFile::read( + File::open("../../media/example.gif").await.unwrap(), + )) + .thumb(InputFile::read( + File::open("../../media/teloxide-core-logo.png").await.unwrap(), + )) + .duration(17), + ), + InputMedia::Audio( + InputMediaAudio::new(InputFile::url("https://example.com".parse().unwrap())) + .performer("a"), + ), + InputMedia::Document(InputMediaDocument::new(InputFile::memory( + &b"Hello world!"[..], + ))), + ], + )) + .unwrap() + .await; + } + + #[tokio::test] + async fn test_add_sticker_to_set() { + to_form_ref(&payloads::AddStickerToSet::new( + UserId(0), + "name", + InputSticker::Png(InputFile::file("../../media/teloxide-core-logo.png")), + "✈️⚙️", + )) + .unwrap() + .await; + } + + #[tokio::test] + async fn test_send_animation() { + to_form_ref( + &payloads::SendAnimation::new( + ChatId(0), + InputFile::file("../../media/teloxide-core-logo.png"), + ) + .caption_entities(entities()) + .thumb(InputFile::read(File::open("../../media/teloxide-core-logo.png").await.unwrap())) + .allow_sending_without_reply(true), + ) + .unwrap() + .await; + } + + fn entities() -> impl Iterator { + <_>::into_iter([ + MessageEntity::new(MessageEntityKind::Url, 0, 0), + MessageEntity::new(MessageEntityKind::Pre { language: None }, 0, 0), + MessageEntity::new(MessageEntityKind::Pre { language: Some(String::new()) }, 0, 0), + MessageEntity::new(MessageEntityKind::Url, 0, 0), + MessageEntity::new( + MessageEntityKind::TextLink { url: "https://example.com".parse().unwrap() }, + 0, + 0, + ), + ]) + } +} diff --git a/crates/teloxide-core/src/serde_multipart/serializers.rs b/crates/teloxide-core/src/serde_multipart/serializers.rs new file mode 100644 index 00000000..0d0f33be --- /dev/null +++ b/crates/teloxide-core/src/serde_multipart/serializers.rs @@ -0,0 +1,534 @@ +use crate::serde_multipart::error::Error; + +use reqwest::multipart::{Form, Part}; +use serde::{ + ser::{Impossible, SerializeMap, SerializeSeq, SerializeStruct}, + Serialize, Serializer, +}; + +/// The main serializer that serializes top-level and structures +pub(super) struct MultipartSerializer(Form); + +/// Serializer for maps (support for `#[serde(flatten)]`) +pub(super) struct MultipartMapSerializer { + form: Form, + key: Option, +} + +/// Serializer for single "fields" that are serialized as multipart "part"s. +/// +/// - Integers serialized as their text decimal representation +/// - Strings and byte slices are serialized as-is, without any changes +/// - Structs are serialized with JSON +/// - C-like enums are serialized as their names +struct PartSerializer; + +/// Struct or Seq -> Json -> Part serializer +struct JsonPartSerializer { + buf: String, + state: PartSerializerStructState, +} + +/// State for `PartSerializerStruct` +/// +/// Json doesn't allow trailing commas, so we need to know if we already +/// serialized something and need to add a comma before next field +enum PartSerializerStructState { + Empty, + Rest, +} + +impl MultipartSerializer { + pub(super) fn new() -> Self { + Self(Form::new()) + } +} + +impl Serializer for MultipartSerializer { + type Ok = Form; + type Error = Error; + + // for `serde(flatten)` (e.g.: in CreateNewStickerSet) + type SerializeMap = MultipartMapSerializer; + + // The main serializer - struct + type SerializeStruct = Self; + + // Unimplemented + type SerializeSeq = Impossible; + type SerializeTuple = Impossible; + type SerializeTupleStruct = Impossible; + type SerializeTupleVariant = Impossible; + type SerializeStructVariant = Impossible; + + fn serialize_map(self, _: Option) -> Result { + Ok(MultipartMapSerializer { form: Form::new(), key: None }) + } + + fn serialize_struct( + self, + _: &'static str, + _: usize, + ) -> Result { + Ok(self) + } + + // Everything down below in this impl just returns + // `Err(Error::TopLevelNotStruct)` + + fn serialize_bool(self, _: bool) -> Result { + Err(Error::TopLevelNotStruct) + } + + fn serialize_i8(self, _: i8) -> Result { + Err(Error::TopLevelNotStruct) + } + + fn serialize_i16(self, _: i16) -> Result { + Err(Error::TopLevelNotStruct) + } + + fn serialize_i32(self, _: i32) -> Result { + Err(Error::TopLevelNotStruct) + } + + fn serialize_i64(self, _: i64) -> Result { + Err(Error::TopLevelNotStruct) + } + + fn serialize_u8(self, _: u8) -> Result { + Err(Error::TopLevelNotStruct) + } + + fn serialize_u16(self, _: u16) -> Result { + Err(Error::TopLevelNotStruct) + } + + fn serialize_u32(self, _: u32) -> Result { + Err(Error::TopLevelNotStruct) + } + + fn serialize_u64(self, _: u64) -> Result { + Err(Error::TopLevelNotStruct) + } + + fn serialize_f32(self, _: f32) -> Result { + Err(Error::TopLevelNotStruct) + } + + fn serialize_f64(self, _: f64) -> Result { + Err(Error::TopLevelNotStruct) + } + + fn serialize_char(self, _: char) -> Result { + Err(Error::TopLevelNotStruct) + } + + fn serialize_str(self, _: &str) -> Result { + Err(Error::TopLevelNotStruct) + } + + fn serialize_bytes(self, _: &[u8]) -> Result { + Err(Error::TopLevelNotStruct) + } + + fn serialize_none(self) -> Result { + Err(Error::TopLevelNotStruct) + } + + fn serialize_some(self, _: &T) -> Result + where + T: Serialize, + { + Err(Error::TopLevelNotStruct) + } + + fn serialize_unit(self) -> Result { + Err(Error::TopLevelNotStruct) + } + + fn serialize_unit_struct(self, _: &'static str) -> Result { + Err(Error::TopLevelNotStruct) + } + + fn serialize_unit_variant( + self, + _: &'static str, + _: u32, + _: &'static str, + ) -> Result { + Err(Error::TopLevelNotStruct) + } + + fn serialize_newtype_struct( + self, + _: &'static str, + _: &T, + ) -> Result + where + T: Serialize, + { + Err(Error::TopLevelNotStruct) + } + + fn serialize_newtype_variant( + self, + _: &'static str, + _: u32, + _: &'static str, + _: &T, + ) -> Result + where + T: Serialize, + { + Err(Error::TopLevelNotStruct) + } + + fn serialize_seq(self, _: Option) -> Result { + Err(Error::TopLevelNotStruct) + } + + fn serialize_tuple(self, _: usize) -> Result { + Err(Error::TopLevelNotStruct) + } + + fn serialize_tuple_struct( + self, + _: &'static str, + _: usize, + ) -> Result { + Err(Error::TopLevelNotStruct) + } + + fn serialize_tuple_variant( + self, + _: &'static str, + _: u32, + _: &'static str, + _: usize, + ) -> Result { + Err(Error::TopLevelNotStruct) + } + + fn serialize_struct_variant( + self, + _: &'static str, + _: u32, + _: &'static str, + _: usize, + ) -> Result { + Err(Error::TopLevelNotStruct) + } +} + +impl SerializeStruct for MultipartSerializer { + type Ok = Form; + type Error = Error; + + fn serialize_field( + &mut self, + key: &'static str, + value: &T, + ) -> Result<(), Self::Error> + where + T: Serialize, + { + let part = value.serialize(PartSerializer {})?; + take_mut::take(&mut self.0, |f| f.part(key, part)); + + Ok(()) + } + + fn end(self) -> Result { + Ok(self.0) + } +} + +impl SerializeMap for MultipartMapSerializer { + type Ok = Form; + type Error = Error; + + fn serialize_key(&mut self, key: &T) -> Result<(), Self::Error> + where + T: Serialize, + { + if let Ok(serde_json::Value::String(s)) = serde_json::to_value(key) { + self.key = Some(s); + } + + Ok(()) + } + + fn serialize_value(&mut self, value: &T) -> Result<(), Self::Error> + where + T: Serialize, + { + let key = self.key.take().expect("Value serialized before key or key is not string"); + + let part = value.serialize(PartSerializer {})?; + + take_mut::take(&mut self.form, |f| f.part(key, part)); + Ok(()) + } + + fn end(self) -> Result { + Ok(self.form) + } +} + +impl Serializer for PartSerializer { + type Ok = Part; + type Error = Error; + + type SerializeStruct = JsonPartSerializer; + type SerializeSeq = JsonPartSerializer; + + // Unimplemented + type SerializeTuple = Impossible; + type SerializeTupleStruct = Impossible; + type SerializeTupleVariant = Impossible; + type SerializeMap = Impossible; + type SerializeStructVariant = Impossible; + + fn serialize_bool(self, v: bool) -> Result { + Ok(Part::text(v.to_string())) + } + + fn serialize_i8(self, v: i8) -> Result { + Ok(Part::text(v.to_string())) + } + + fn serialize_i16(self, v: i16) -> Result { + Ok(Part::text(v.to_string())) + } + + fn serialize_i32(self, v: i32) -> Result { + Ok(Part::text(v.to_string())) + } + + fn serialize_i64(self, v: i64) -> Result { + Ok(Part::text(v.to_string())) + } + + fn serialize_u8(self, v: u8) -> Result { + Ok(Part::text(v.to_string())) + } + + fn serialize_u16(self, v: u16) -> Result { + Ok(Part::text(v.to_string())) + } + + fn serialize_u32(self, v: u32) -> Result { + Ok(Part::text(v.to_string())) + } + + fn serialize_u64(self, v: u64) -> Result { + Ok(Part::text(v.to_string())) + } + + fn serialize_f32(self, v: f32) -> Result { + Ok(Part::text(v.to_string())) + } + + fn serialize_f64(self, v: f64) -> Result { + Ok(Part::text(v.to_string())) + } + + fn serialize_char(self, v: char) -> Result { + Ok(Part::text(v.to_string())) + } + + fn serialize_str(self, v: &str) -> Result { + Ok(Part::text(v.to_owned())) + } + + fn serialize_bytes(self, v: &[u8]) -> Result { + Ok(Part::bytes(v.to_owned())) + } + + fn serialize_some(self, value: &T) -> Result + where + T: Serialize, + { + value.serialize(self) + } + + fn serialize_unit_variant( + self, + _: &'static str, + _: u32, + variant_name: &'static str, + ) -> Result { + Ok(Part::text(variant_name)) + } + + fn serialize_struct( + self, + _: &'static str, + _: usize, + ) -> Result { + Ok(JsonPartSerializer { buf: String::new(), state: PartSerializerStructState::Empty }) + } + + fn serialize_seq(self, _: Option) -> Result { + Ok(JsonPartSerializer { buf: String::new(), state: PartSerializerStructState::Empty }) + } + + // Unimplemented + + fn serialize_none(self) -> Result { + unimplemented!( + "We use `#[serde_with_macros::skip_serializing_none]` everywhere so `None`s are not \ + serialized" + ) + } + + fn serialize_unit(self) -> Result { + unimplemented!() + } + + fn serialize_unit_struct(self, _: &'static str) -> Result { + unimplemented!() + } + + fn serialize_newtype_struct( + self, + _: &'static str, + _: &T, + ) -> Result + where + T: Serialize, + { + unimplemented!() + } + + fn serialize_newtype_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _value: &T, + ) -> Result + where + T: Serialize, + { + unimplemented!() + } + + fn serialize_tuple(self, _: usize) -> Result { + unimplemented!() + } + + fn serialize_tuple_struct( + self, + _: &'static str, + _: usize, + ) -> Result { + unimplemented!() + } + + fn serialize_tuple_variant( + self, + _: &'static str, + _: u32, + _: &'static str, + _: usize, + ) -> Result { + unimplemented!() + } + + fn serialize_map(self, _: Option) -> Result { + unimplemented!() + } + + fn serialize_struct_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _len: usize, + ) -> Result { + unimplemented!() + } +} + +impl SerializeStruct for JsonPartSerializer { + type Ok = Part; + type Error = Error; + + fn serialize_field( + &mut self, + key: &'static str, + value: &T, + ) -> Result<(), Self::Error> + where + T: Serialize, + { + use std::fmt::Write; + use PartSerializerStructState::*; + + let value = serde_json::to_string(value)?; + match self.state { + Empty => { + self.state = Rest; + + write!(&mut self.buf, "{{\"{}\":{}", key, value)? + } + Rest => write!(&mut self.buf, ",\"{}\":{}", key, value)?, + } + + Ok(()) + } + + fn end(mut self) -> Result { + use PartSerializerStructState::*; + + match self.state { + Empty => Ok(Part::text("{{}}")), + Rest => { + self.buf += "}"; + + Ok(Part::text(self.buf)) + } + } + } +} + +impl SerializeSeq for JsonPartSerializer { + type Ok = Part; + + type Error = Error; + + fn serialize_element(&mut self, value: &T) -> Result<(), Self::Error> + where + T: Serialize, + { + use std::fmt::Write; + use PartSerializerStructState::*; + + let value = serde_json::to_string(value)?; + match self.state { + Empty => { + self.state = Rest; + + write!(&mut self.buf, "[{}", value)? + } + Rest => write!(&mut self.buf, ",{}", value)?, + } + + Ok(()) + } + + fn end(mut self) -> Result { + use PartSerializerStructState::*; + + match self.state { + Empty => Ok(Part::text("[]")), + Rest => { + self.buf += "]"; + + Ok(Part::text(self.buf)) + } + } + } +} diff --git a/crates/teloxide-core/src/serde_multipart/unserializers.rs b/crates/teloxide-core/src/serde_multipart/unserializers.rs new file mode 100644 index 00000000..d6b020d3 --- /dev/null +++ b/crates/teloxide-core/src/serde_multipart/unserializers.rs @@ -0,0 +1,113 @@ +mod bytes; +mod input_file; +mod string; + +pub(crate) use input_file::InputFileUnserializer; +pub(crate) use string::StringUnserializer; + +use std::fmt::{self, Display}; + +use serde::ser; + +#[derive(Debug, PartialEq, Eq)] +pub enum UnserializerError { + Custom(String), + UnsupportedType { + ty: &'static str, + supported: &'static str, + }, + UnexpectedField { + name: &'static str, + expected: &'static [&'static str], + }, + UnexpectedVariant { + name: &'static str, + expected: &'static [&'static str], + }, + WrongLen { + len: usize, + expected: usize, + }, +} + +impl ser::Error for UnserializerError { + fn custom(msg: T) -> Self + where + T: Display, + { + Self::Custom(msg.to_string()) + } +} + +impl Display for UnserializerError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::UnexpectedField { name, expected } => write!( + f, + "Unexpected field: `{}`, expected field(s): `{}`", + name, + expected.join(", ") + ), + Self::Custom(s) => write!(f, "Custom serde error: {}", s), + Self::UnsupportedType { ty, supported } => { + write!( + f, + "Unsupported type: `{}`, supported type(s): `{}`", + ty, supported + ) + } + Self::UnexpectedVariant { name, expected } => write!( + f, + "Unexpected variant: `{}`, expected variants(s): `{}`", + name, + expected.join(", ") + ), + Self::WrongLen { len, expected } => { + write!(f, "Wrong len: `{}`, expected `{}`", len, expected) + } + } + } +} + +impl std::error::Error for UnserializerError {} + +#[test] +fn test() { + use crate::{ + serde_multipart::unserializers::{ + input_file::InputFileUnserializer, string::StringUnserializer, + }, + types::InputFile, + }; + + use serde::Serialize; + + use std::{borrow::Cow, path::Path}; + + let value = String::from("test"); + assert!(matches!(value.serialize(StringUnserializer), Ok(v) if v == value)); + + let url = reqwest::Url::parse("http://example.com").unwrap(); + let value = InputFile::Url(url.clone()); + assert!( + matches!(value.serialize(InputFileUnserializer::NotMem), Ok(InputFile::Url(v)) if v == url) + ); + + let value = InputFile::FileId(String::from("file_id")); + assert!( + matches!(value.serialize(InputFileUnserializer::NotMem), Ok(InputFile::FileId(v)) if v == "file_id") + ); + + let value = InputFile::Memory { + file_name: String::from("name"), + data: Cow::Owned(vec![1, 2, 3]), + }; + assert!( + matches!(value.serialize(InputFileUnserializer::memory()), Ok(InputFile::Memory { file_name, data }) if file_name == "name" && *data == [1, 2, 3]) + ); + + let value = InputFile::File("a/b/c".into()); + assert!( + matches!(value.serialize(InputFileUnserializer::NotMem), Ok(InputFile::File(v)) if v == Path::new("a/b/c")) + ); +} diff --git a/crates/teloxide-core/src/serde_multipart/unserializers/input_file.rs b/crates/teloxide-core/src/serde_multipart/unserializers/input_file.rs new file mode 100644 index 00000000..efc6c194 --- /dev/null +++ b/crates/teloxide-core/src/serde_multipart/unserializers/input_file.rs @@ -0,0 +1,184 @@ +use std::borrow::Cow; + +use serde::{ + ser::{Impossible, SerializeStructVariant}, + Serialize, Serializer, +}; + +use crate::{ + serde_multipart::unserializers::{ + bytes::BytesUnserializer, string::StringUnserializer, UnserializerError, + }, + types::InputFile, +}; + +pub(crate) enum InputFileUnserializer { + Memory { + file_name: String, + data: Cow<'static, [u8]>, + }, + NotMem, +} + +impl InputFileUnserializer { + pub(crate) fn memory() -> Self { + Self::Memory { + file_name: String::new(), + data: Cow::Borrowed(&[]), + } + } +} + +impl Serializer for InputFileUnserializer { + type Ok = InputFile; + type Error = UnserializerError; + + type SerializeSeq = Impossible; + type SerializeTuple = Impossible; + type SerializeTupleStruct = Impossible; + type SerializeTupleVariant = Impossible; + type SerializeMap = Impossible; + type SerializeStruct = Impossible; + + type SerializeStructVariant = Self; + + fn serialize_newtype_variant( + self, + name: &'static str, + _: u32, + variant: &'static str, + value: &T, + ) -> Result + where + T: Serialize, + { + if name != "InputFile" { + return Err(UnserializerError::UnsupportedType { + ty: name, + supported: "InputFile", // TODO + }); + } + + // TODO + match variant { + "File" => Ok(InputFile::File(value.serialize(StringUnserializer)?.into())), + "Url" => Ok(InputFile::Url( + reqwest::Url::parse(&value.serialize(StringUnserializer)?).unwrap(), + )), + "FileId" => Ok(InputFile::FileId(value.serialize(StringUnserializer)?)), + name => Err(UnserializerError::UnexpectedVariant { + name, + expected: &["File", "Url", "FileId"], // TODO + }), + } + } + + fn serialize_struct_variant( + self, + name: &'static str, + _variant_index: u32, + variant: &'static str, + len: usize, + ) -> Result { + if name != "InputFile" { + return Err(UnserializerError::UnsupportedType { + ty: name, + supported: "InputFile", + }); + } + + if variant != "Memory" { + return Err(UnserializerError::UnexpectedVariant { + name: variant, + expected: &["Memory"], + }); + } + + if len != 2 { + return Err(UnserializerError::WrongLen { len, expected: 2 }); + } + + Ok(self) + } + + forward_to_unsuported_ty! { + supported: "Newtype variant, struct variant"; + simple { + serialize_bool bool + serialize_i8 i8 + serialize_i16 i16 + serialize_i32 i32 + serialize_i64 i64 + serialize_u8 u8 + serialize_u16 u16 + serialize_u32 u32 + serialize_u64 u64 + serialize_f32 f32 + serialize_f64 f64 + serialize_bytes &[u8] + serialize_char char + serialize_str &str + } + unit { + serialize_none "None" + serialize_unit "unit" + } + compound { + serialize_some(_: &T) -> Self::Ok => "Some(_)" + serialize_unit_struct(_: &'static str) -> Self::Ok => "unit struct" + serialize_unit_variant(_: &'static str, _: u32, _: &'static str) -> Self::Ok => "unit variant" + serialize_newtype_struct(_: &'static str, _: &T) -> Self::Ok => "newtype struct" + serialize_seq(_: Option) -> Self::SerializeSeq => "sequence" + serialize_tuple(_: usize) -> Self::SerializeTuple => "tuple" + serialize_tuple_struct(_: &'static str, _: usize) -> Self::SerializeTupleStruct => "tuple struct" + serialize_tuple_variant(_: &'static str, _: u32, _: &'static str, _: usize) -> Self::SerializeTupleVariant => "tuple variant" + serialize_map(_: Option) -> Self::SerializeMap => "map" + serialize_struct(_: &'static str, _: usize) -> Self::SerializeStruct => "struct" + } + } +} + +impl SerializeStructVariant for InputFileUnserializer { + type Ok = InputFile; + type Error = UnserializerError; + + fn serialize_field( + &mut self, + key: &'static str, + value: &T, + ) -> Result<(), Self::Error> + where + T: Serialize, + { + let (file_name, data) = match self { + Self::Memory { file_name, data } => (file_name, data), + Self::NotMem => { + *self = Self::memory(); + match self { + Self::Memory { file_name, data } => (file_name, data), + Self::NotMem => unreachable!(), + } + } + }; + + match key { + "file_name" => *file_name = value.serialize(StringUnserializer)?, + "data" => *data = Cow::Owned(value.serialize(BytesUnserializer::default())?), + name => { + return Err(UnserializerError::UnexpectedField { + name, + expected: &["file_name", "data"], // TODO + }); + } + } + + Ok(()) + } + + fn end(self) -> Result { + match self { + Self::Memory { file_name, data } => Ok(InputFile::Memory { file_name, data }), + Self::NotMem => unreachable!("struct without fields?"), + } + } +} diff --git a/crates/teloxide-core/src/types.rs b/crates/teloxide-core/src/types.rs new file mode 100644 index 00000000..0b09f7c0 --- /dev/null +++ b/crates/teloxide-core/src/types.rs @@ -0,0 +1,422 @@ +//! Telegram API types. + +pub use allowed_update::*; +pub use animation::*; +pub use audio::*; +pub use bot_command::*; +pub use bot_command_scope::*; +pub use callback_game::*; +pub use callback_query::*; +pub use chat::*; +pub use chat_action::*; +pub use chat_administrator_rights::*; +pub use chat_invite_link::*; +pub use chat_join_request::*; +pub use chat_location::*; +pub use chat_member::*; +pub use chat_member_updated::*; +pub use chat_permissions::*; +pub use chat_photo::*; +pub use chat_type::*; +pub use chosen_inline_result::*; +pub use contact::*; +pub use dice::*; +pub use dice_emoji::*; +pub use document::*; +pub use encrypted_credentials::*; +pub use encrypted_passport_element::*; +pub use file::*; +pub use force_reply::*; +pub use game::*; +pub use game_high_score::*; +pub use inline_keyboard_button::*; +pub use inline_keyboard_markup::*; +pub use inline_query::*; +pub use inline_query_result::*; +pub use inline_query_result_article::*; +pub use inline_query_result_audio::*; +pub use inline_query_result_cached_audio::*; +pub use inline_query_result_cached_document::*; +pub use inline_query_result_cached_gif::*; +pub use inline_query_result_cached_mpeg4_gif::*; +pub use inline_query_result_cached_photo::*; +pub use inline_query_result_cached_sticker::*; +pub use inline_query_result_cached_video::*; +pub use inline_query_result_cached_voice::*; +pub use inline_query_result_contact::*; +pub use inline_query_result_document::*; +pub use inline_query_result_game::*; +pub use inline_query_result_gif::*; +pub use inline_query_result_location::*; +pub use inline_query_result_mpeg4_gif::*; +pub use inline_query_result_photo::*; +pub use inline_query_result_venue::*; +pub use inline_query_result_video::*; +pub use inline_query_result_voice::*; +pub use input_file::*; +pub use input_media::*; +pub use input_message_content::*; +pub use input_sticker::*; +pub use invoice::*; +pub use keyboard_button::*; +pub use keyboard_button_poll_type::*; +pub use label_price::*; +pub use location::*; +pub use login_url::*; +pub use mask_position::*; +pub use me::*; +pub use menu_button::*; +pub use message::*; +pub use message_auto_delete_timer_changed::*; +pub use message_entity::*; +pub use message_id::*; +pub use order_info::*; +pub use parse_mode::*; +pub use passport_data::*; +pub use passport_element_error::*; +pub use passport_file::*; +pub use photo_size::*; +pub use poll::*; +pub use poll_answer::*; +pub use poll_type::*; +pub use pre_checkout_query::*; +pub use proximity_alert_triggered::*; +pub use reply_keyboard_markup::*; +pub use reply_keyboard_remove::*; +pub use reply_markup::*; +pub use response_parameters::*; +pub use sent_web_app_message::*; +use serde::Serialize; +pub use shipping_address::*; +pub use shipping_option::*; +pub use shipping_query::*; +pub use sticker::*; +pub use sticker_set::*; +pub use successful_payment::*; +pub use target_message::*; +pub use unit_false::*; +pub use unit_true::*; +pub use update::*; +pub use user::*; +pub use user_profile_photos::*; +pub use venue::*; +pub use video::*; +pub use video_chat_ended::*; +pub use video_chat_participants_invited::*; +pub use video_chat_scheduled::*; +pub use video_chat_started::*; +pub use video_note::*; +pub use voice::*; +pub use web_app_data::*; +pub use web_app_info::*; +pub use webhook_info::*; + +mod allowed_update; +mod animation; +mod audio; +mod bot_command; +mod bot_command_scope; +mod callback_game; +mod callback_query; +mod chat; +mod chat_action; +mod chat_administrator_rights; +mod chat_invite_link; +mod chat_join_request; +mod chat_location; +mod chat_member; +mod chat_member_updated; +mod chat_permissions; +mod chat_photo; +mod chat_type; +mod chosen_inline_result; +mod contact; +mod dice; +mod dice_emoji; +mod document; +mod file; +mod force_reply; +mod game; +mod game_high_score; +mod inline_keyboard_button; +mod inline_keyboard_markup; +mod input_file; +mod input_media; +mod input_message_content; +mod input_sticker; +mod invoice; +mod keyboard_button; +mod keyboard_button_poll_type; +mod label_price; +mod location; +mod login_url; +mod mask_position; +mod me; +mod menu_button; +mod message; +mod message_auto_delete_timer_changed; +mod message_entity; +mod message_id; +mod order_info; +mod parse_mode; +mod photo_size; +mod poll; +mod poll_answer; +mod poll_type; +mod pre_checkout_query; +mod proximity_alert_triggered; +mod reply_keyboard_markup; +mod reply_keyboard_remove; +mod reply_markup; +mod response_parameters; +mod sent_web_app_message; +mod shipping_address; +mod shipping_option; +mod shipping_query; +mod sticker; +mod sticker_set; +mod successful_payment; +mod target_message; +mod unit_false; +mod unit_true; +mod update; +mod user; +mod user_profile_photos; +mod venue; +mod video; +mod video_chat_ended; +mod video_chat_participants_invited; +mod video_chat_scheduled; +mod video_chat_started; +mod video_note; +mod voice; +mod web_app_data; +mod web_app_info; +mod webhook_info; + +mod inline_query; +mod inline_query_result; +mod inline_query_result_article; +mod inline_query_result_audio; +mod inline_query_result_cached_audio; +mod inline_query_result_cached_document; +mod inline_query_result_cached_gif; +mod inline_query_result_cached_mpeg4_gif; +mod inline_query_result_cached_photo; +mod inline_query_result_cached_sticker; +mod inline_query_result_cached_video; +mod inline_query_result_cached_voice; +mod inline_query_result_contact; +mod inline_query_result_document; +mod inline_query_result_game; +mod inline_query_result_gif; +mod inline_query_result_location; +mod inline_query_result_mpeg4_gif; +mod inline_query_result_photo; +mod inline_query_result_venue; +mod inline_query_result_video; +mod inline_query_result_voice; + +mod encrypted_credentials; +mod encrypted_passport_element; +mod passport_data; +mod passport_element_error; +mod passport_file; + +pub use non_telegram_types::{country_code::*, currency::*, until_date::*}; +mod non_telegram_types { + pub(super) mod country_code; + pub(super) mod currency; + pub(crate) mod mime; + pub(super) mod until_date; +} + +mod chat_id; +mod recipient; +mod user_id; + +pub use chat_id::*; +pub use recipient::*; +pub use user_id::*; + +/// Converts an `i64` timestump to a `choro::DateTime`, producing serde error +/// for invalid timestumps +pub(crate) fn serde_timestamp( + timestamp: i64, +) -> Result, E> { + use chrono::{DateTime, NaiveDateTime, Utc}; + + NaiveDateTime::from_timestamp_opt(timestamp, 0) + .ok_or_else(|| E::custom("invalid timestump")) + .map(|naive| DateTime::from_utc(naive, Utc)) +} + +pub(crate) mod serde_opt_date_from_unix_timestamp { + use chrono::{DateTime, Utc}; + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + + use crate::types::serde_timestamp; + + pub(crate) fn serialize( + this: &Option>, + serializer: S, + ) -> Result + where + S: Serializer, + { + this.map(|dt| dt.timestamp()).serialize(serializer) + } + + pub(crate) fn deserialize<'de, D>(deserializer: D) -> Result>, D::Error> + where + D: Deserializer<'de>, + { + Option::::deserialize(deserializer)?.map(serde_timestamp).transpose() + } + + #[test] + fn test() { + #[derive(Serialize, Deserialize)] + struct Struct { + #[serde(default, with = "crate::types::serde_opt_date_from_unix_timestamp")] + date: Option>, + } + + { + let json = r#"{"date":1}"#; + let expected = + DateTime::from_utc(chrono::NaiveDateTime::from_timestamp_opt(1, 0).unwrap(), Utc); + + let Struct { date } = serde_json::from_str(json).unwrap(); + assert_eq!(date, Some(expected)); + } + + { + let json = r#"{}"#; + + let Struct { date } = serde_json::from_str(json).unwrap(); + assert_eq!(date, None); + } + } +} + +pub(crate) mod serde_date_from_unix_timestamp { + use chrono::{DateTime, Utc}; + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + + use crate::types::serde_timestamp; + + pub(crate) fn serialize(this: &DateTime, serializer: S) -> Result + where + S: Serializer, + { + this.timestamp().serialize(serializer) + } + + pub(crate) fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + { + serde_timestamp(i64::deserialize(deserializer)?) + } +} + +pub(crate) mod option_url_from_string { + use reqwest::Url; + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + + pub(crate) fn serialize(this: &Option, serializer: S) -> Result + where + S: Serializer, + { + match this { + Some(url) => url.serialize(serializer), + None => "".serialize(serializer), + } + } + + pub(crate) fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + { + Ok(reqwest::Url::deserialize(deserializer).ok()) + } + + #[test] + fn test() { + use std::str::FromStr; + #[derive(Serialize, Deserialize)] + struct Struct { + #[serde(with = "crate::types::option_url_from_string")] + url: Option, + } + + { + let json = r#"{"url":""}"#; + let url: Struct = serde_json::from_str(json).unwrap(); + assert_eq!(url.url, None); + assert_eq!(serde_json::to_string(&url).unwrap(), json.to_owned()); + + let json = r#"{"url":"https://github.com/token"}"#; + let url: Struct = serde_json::from_str(json).unwrap(); + assert_eq!(url.url, Some(Url::from_str("https://github.com/token").unwrap())); + assert_eq!(serde_json::to_string(&url).unwrap(), json.to_owned()); + } + } +} + +pub(crate) mod duration_secs { + use std::time::Duration; + + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + + pub(crate) fn serialize(this: &Duration, serializer: S) -> Result + where + S: Serializer, + { + this.as_secs().serialize(serializer) + } + + pub(crate) fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + u64::deserialize(deserializer).map(Duration::from_secs) + } + + #[test] + fn test() { + #[derive(Serialize, Deserialize)] + struct Struct { + #[serde(with = "crate::types::duration_secs")] + duration: Duration, + } + + { + let json = r#"{"duration":0}"#; + let duration: Struct = serde_json::from_str(json).unwrap(); + assert_eq!(duration.duration, Duration::from_secs(0)); + assert_eq!(serde_json::to_string(&duration).unwrap(), json.to_owned()); + + let json = r#"{"duration":12}"#; + let duration: Struct = serde_json::from_str(json).unwrap(); + assert_eq!(duration.duration, Duration::from_secs(12)); + assert_eq!(serde_json::to_string(&duration).unwrap(), json.to_owned()); + + let json = r#"{"duration":1234}"#; + let duration: Struct = serde_json::from_str(json).unwrap(); + assert_eq!(duration.duration, Duration::from_secs(1234)); + assert_eq!(serde_json::to_string(&duration).unwrap(), json.to_owned()); + } + } +} + +pub(crate) fn serialize_reply_to_message_id( + this: &Option, + serializer: S, +) -> Result +where + S: serde::Serializer, +{ + this.map(|MessageId(id)| id).serialize(serializer) +} diff --git a/crates/teloxide-core/src/types/allowed_update.rs b/crates/teloxide-core/src/types/allowed_update.rs new file mode 100644 index 00000000..7820ad8f --- /dev/null +++ b/crates/teloxide-core/src/types/allowed_update.rs @@ -0,0 +1,20 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum AllowedUpdate { + Message, + EditedMessage, + ChannelPost, + EditedChannelPost, + InlineQuery, + ChosenInlineResult, + CallbackQuery, + ShippingQuery, + PreCheckoutQuery, + Poll, + PollAnswer, + MyChatMember, + ChatMember, + ChatJoinRequest, +} diff --git a/crates/teloxide-core/src/types/animation.rs b/crates/teloxide-core/src/types/animation.rs new file mode 100644 index 00000000..b583cce9 --- /dev/null +++ b/crates/teloxide-core/src/types/animation.rs @@ -0,0 +1,77 @@ +use mime::Mime; +use serde::{Deserialize, Serialize}; + +use crate::types::{FileMeta, PhotoSize}; + +/// This object represents an animation file (GIF or H.264/MPEG-4 AVC video +/// without sound). +/// +/// [The official docs](https://core.telegram.org/bots/api#animation). +#[serde_with_macros::skip_serializing_none] +#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)] +pub struct Animation { + /// Metadata of the animation file. + #[serde(flatten)] + pub file: FileMeta, + + /// A video width as defined by a sender. + pub width: u32, + + /// A video height as defined by a sender. + pub height: u32, + + /// A duration of the video in seconds as defined by a sender. + pub duration: u32, + + /// An animation thumbnail as defined by a sender. + pub thumb: Option, + + /// An original animation filename as defined by a sender. + pub file_name: Option, + + /// A MIME type of the file as defined by a sender. + #[serde(with = "crate::types::non_telegram_types::mime::opt_deser")] + pub mime_type: Option, +} + +#[cfg(test)] +mod tests { + use crate::types::FileMeta; + + use super::*; + + #[test] + fn deserialize() { + let json = r#"{ + "file_id":"id", + "file_unique_id":"", + "width":320, + "height":320, + "duration":59, + "thumb":{ + "file_id":"id", + "file_unique_id":"", + "width":320, + "height":320, + "file_size":3452 + }, + "file_name":"some", + "mime_type":"video/gif", + "file_size":6500}"#; + let expected = Animation { + file: FileMeta { id: "id".to_string(), unique_id: "".to_string(), size: 6500 }, + width: 320, + height: 320, + duration: 59, + thumb: Some(PhotoSize { + file: FileMeta { id: "id".to_owned(), unique_id: "".to_owned(), size: 3452 }, + width: 320, + height: 320, + }), + file_name: Some("some".to_string()), + mime_type: Some("video/gif".parse().unwrap()), + }; + let actual = serde_json::from_str::(json).unwrap(); + assert_eq!(actual, expected) + } +} diff --git a/crates/teloxide-core/src/types/audio.rs b/crates/teloxide-core/src/types/audio.rs new file mode 100644 index 00000000..87b0505e --- /dev/null +++ b/crates/teloxide-core/src/types/audio.rs @@ -0,0 +1,77 @@ +use mime::Mime; +use serde::{Deserialize, Serialize}; + +use crate::types::{FileMeta, PhotoSize}; + +/// This object represents an audio file to be treated as music by the Telegram +/// clients. +/// +/// [The official docs](https://core.telegram.org/bots/api#audio). +#[serde_with_macros::skip_serializing_none] +#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)] +pub struct Audio { + /// Metadata of the audio file. + #[serde(flatten)] + pub file: FileMeta, + + /// A duration of the audio in seconds as defined by a sender. + pub duration: u32, + + /// A performer of the audio as defined by a sender or by audio tags. + pub performer: Option, + + /// A title of the audio as defined by sender or by audio tags. + pub title: Option, + + /// Original filename as defined by sender + pub file_name: Option, + + /// A MIME type of the file as defined by a sender. + #[serde(with = "crate::types::non_telegram_types::mime::opt_deser")] + pub mime_type: Option, + + /// A thumbnail of the album cover to which the music file belongs. + pub thumb: Option, +} + +#[cfg(test)] +mod tests { + use crate::types::FileMeta; + + use super::*; + + #[test] + fn deserialize() { + let json = r#"{ + "file_id":"id", + "file_unique_id":"", + "duration":60, + "performer":"Performer", + "title":"Title", + "mime_type":"application/zip", + "file_size":123456, + "thumb":{ + "file_id":"id", + "file_unique_id":"", + "width":320, + "height":320, + "file_size":3452 + } + }"#; + let expected = Audio { + file: FileMeta { id: "id".to_string(), unique_id: "".to_string(), size: 123_456 }, + duration: 60, + performer: Some("Performer".to_string()), + title: Some("Title".to_string()), + mime_type: Some("application/zip".parse().unwrap()), + thumb: Some(PhotoSize { + file: FileMeta { id: "id".to_owned(), unique_id: "".to_owned(), size: 3452 }, + width: 320, + height: 320, + }), + file_name: None, + }; + let actual = serde_json::from_str::

+/// +///
+/// +/// [Telegram Login Widget]: https://core.telegram.org/widgets/login +#[serde_with_macros::skip_serializing_none] +#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)] +pub struct LoginUrl { + /// An HTTPS URL to be opened with user authorization data added to the + /// query string when the button is pressed. If the user refuses to + /// provide authorization data, the original URL without information + /// about the user will be opened. The data added is the same as + /// described in [Receiving authorization data]. + /// + /// [Receiving authorization data]: https://core.telegram.org/widgets/login#receiving-authorization-data + /// + /// NOTE: You must always check the hash of the received data to verify the + /// authentication and the integrity of the data as described in [Checking + /// authorization]. + /// + /// [Checking authorization]: https://core.telegram.org/widgets/login#checking-authorization + pub url: reqwest::Url, + /// New text of the button in forwarded messages. + pub forward_text: Option, + /// Username of a bot, which will be used for user authorization. See + /// [Setting up a bot] for more details. If not specified, the current bot's + /// username will be assumed. The url's domain must be the same as the + /// domain linked with the bot. See [Linking your domain to the bot] for + /// more details. + /// + /// [Setting up a bot]: https://core.telegram.org/widgets/login#setting-up-a-bot + /// [Linking your domain to the bot]: https://core.telegram.org/widgets/login#linking-your-domain-to-the-bot + pub bot_username: Option, + /// Pass `true` to request the permission for your bot to send messages to + /// the user. + pub request_write_access: Option, +} + +impl LoginUrl { + #[must_use] + pub fn url(mut self, val: reqwest::Url) -> Self { + self.url = val; + self + } + + pub fn forward_text(mut self, val: S) -> Self + where + S: Into, + { + self.forward_text = Some(val.into()); + self + } + + pub fn bot_username(mut self, val: S) -> Self + where + S: Into, + { + self.bot_username = Some(val.into()); + self + } + + #[must_use] + pub fn request_write_access(mut self, val: bool) -> Self { + self.request_write_access = Some(val); + self + } +} diff --git a/crates/teloxide-core/src/types/mask_position.rs b/crates/teloxide-core/src/types/mask_position.rs new file mode 100644 index 00000000..147654e1 --- /dev/null +++ b/crates/teloxide-core/src/types/mask_position.rs @@ -0,0 +1,64 @@ +use serde::{Deserialize, Serialize}; + +/// This object describes the position on faces where a mask should be placed by +/// default. +/// +/// [The official docs](https://core.telegram.org/bots/api#maskposition). +#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct MaskPosition { + /// The part of the face relative to which the mask should be placed. One + /// of `forehead`, `eyes`, `mouth`, or `chin`. + pub point: MaskPoint, + + /// Shift by X-axis measured in widths of the mask scaled to the face size, + /// from left to right. For example, choosing `-1.0` will place mask just + /// to the left of the default mask position. + pub x_shift: f64, + + /// Shift by Y-axis measured in heights of the mask scaled to the face + /// size, from top to bottom. For example, `1.0` will place the mask just + /// below the default mask position. + pub y_shift: f64, + + /// Mask scaling coefficient. For example, `2.0` means double size. + pub scale: f64, +} + +/// The part of the face relative to which the mask should be placed. +#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum MaskPoint { + Forehead, + Eyes, + Mouth, + Chin, +} + +impl MaskPosition { + pub const fn new(point: MaskPoint, x_shift: f64, y_shift: f64, scale: f64) -> Self { + Self { point, x_shift, y_shift, scale } + } + + pub const fn point(mut self, val: MaskPoint) -> Self { + self.point = val; + self + } + + #[must_use] + pub const fn x_shift(mut self, val: f64) -> Self { + self.x_shift = val; + self + } + + #[must_use] + pub const fn y_shift(mut self, val: f64) -> Self { + self.y_shift = val; + self + } + + #[must_use] + pub const fn scale(mut self, val: f64) -> Self { + self.scale = val; + self + } +} diff --git a/crates/teloxide-core/src/types/me.rs b/crates/teloxide-core/src/types/me.rs new file mode 100644 index 00000000..bea15ca3 --- /dev/null +++ b/crates/teloxide-core/src/types/me.rs @@ -0,0 +1,81 @@ +use std::ops::Deref; + +use serde::{Deserialize, Serialize}; + +use crate::types::User; + +/// Returned only in [`GetMe`]. +/// +/// [`GetMe`]: crate::payloads::GetMe +#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)] +pub struct Me { + #[serde(flatten)] + pub user: User, + + /// `true`, if the bot can be invited to groups. + pub can_join_groups: bool, + + /// `true`, if [privacy mode] is disabled for the bot. + /// + /// [privacy mode]: https://core.telegram.org/bots#privacy-mode + pub can_read_all_group_messages: bool, + + /// `true`, if the bot supports inline queries. + pub supports_inline_queries: bool, +} + +impl Me { + /// Returns the username of the bot. + #[must_use] + pub fn username(&self) -> &str { + self.user.username.as_deref().expect("Bots must have usernames") + } + + /// Returns a username mention of this bot. + #[must_use] + pub fn mention(&self) -> String { + format!("@{}", self.username()) + } + + /// Returns an URL that links to this bot in the form of `t.me/<...>`. + #[must_use] + pub fn tme_url(&self) -> reqwest::Url { + format!("https://t.me/{}", self.username()).parse().unwrap() + } +} + +impl Deref for Me { + type Target = User; + + fn deref(&self) -> &Self::Target { + &self.user + } +} + +#[cfg(test)] +mod tests { + use crate::types::{Me, User, UserId}; + + #[test] + fn convenience_methods_work() { + let me = Me { + user: User { + id: UserId(42), + is_bot: true, + first_name: "First".to_owned(), + last_name: None, + username: Some("SomethingSomethingBot".to_owned()), + language_code: None, + is_premium: false, + added_to_attachment_menu: false, + }, + can_join_groups: false, + can_read_all_group_messages: false, + supports_inline_queries: false, + }; + + assert_eq!(me.username(), "SomethingSomethingBot"); + assert_eq!(me.mention(), "@SomethingSomethingBot"); + assert_eq!(me.tme_url(), "https://t.me/SomethingSomethingBot".parse().unwrap()); + } +} diff --git a/crates/teloxide-core/src/types/menu_button.rs b/crates/teloxide-core/src/types/menu_button.rs new file mode 100644 index 00000000..6e056932 --- /dev/null +++ b/crates/teloxide-core/src/types/menu_button.rs @@ -0,0 +1,35 @@ +use serde::{Deserialize, Serialize}; + +use crate::types::WebAppInfo; + +/// This object describes the bot's menu button in a private chat. +/// +/// If a menu button other than `MenuButton::Default` is set for a private chat, +/// then it is applied in the chat. Otherwise the default menu button is +/// applied. By default, the menu button opens the list of bot commands. +#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +#[serde(tag = "type")] +pub enum MenuButton { + /// Represents a menu button, which opens the bot's list of commands. + Commands, + + /// Represents a menu button, which launches a [Web App]. + /// + /// [Web App]: https://core.telegram.org/bots/webapps + WebApp { + /// Text on the button. + text: String, + + /// Description of the Web App that will be launched when the user + /// presses the button. The Web App will be able to send an arbitrary + /// message on behalf of the user using the method + /// [`AnswerWebAppQuery`]. + /// + /// [`AnswerWebAppQuery`]: crate::payloads::AnswerWebAppQuery + web_app: WebAppInfo, + }, + + /// Describes that no specific value for the menu button was set. + Default, +} diff --git a/crates/teloxide-core/src/types/message.rs b/crates/teloxide-core/src/types/message.rs new file mode 100644 index 00000000..d0ff1510 --- /dev/null +++ b/crates/teloxide-core/src/types/message.rs @@ -0,0 +1,1728 @@ +#![allow(clippy::large_enum_variant)] + +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use url::Url; + +use crate::types::{ + Animation, Audio, BareChatId, Chat, ChatId, Contact, Dice, Document, Game, + InlineKeyboardMarkup, Invoice, Location, MessageAutoDeleteTimerChanged, MessageEntity, + MessageEntityRef, MessageId, PassportData, PhotoSize, Poll, ProximityAlertTriggered, Sticker, + SuccessfulPayment, True, User, Venue, Video, VideoChatEnded, VideoChatParticipantsInvited, + VideoChatScheduled, VideoChatStarted, VideoNote, Voice, WebAppData, +}; + +/// This object represents a message. +/// +/// [The official docs](https://core.telegram.org/bots/api#message). +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct Message { + /// Unique message identifier inside this chat. + #[serde(flatten)] + pub id: MessageId, + + /// Date the message was sent in Unix time. + #[serde(with = "crate::types::serde_date_from_unix_timestamp")] + pub date: DateTime, + + /// Conversation the message belongs to. + pub chat: Chat, + + /// Bot through which the message was sent. + pub via_bot: Option, + + #[serde(flatten)] + pub kind: MessageKind, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(untagged)] +pub enum MessageKind { + Common(MessageCommon), + NewChatMembers(MessageNewChatMembers), + LeftChatMember(MessageLeftChatMember), + NewChatTitle(MessageNewChatTitle), + NewChatPhoto(MessageNewChatPhoto), + DeleteChatPhoto(MessageDeleteChatPhoto), + GroupChatCreated(MessageGroupChatCreated), + SupergroupChatCreated(MessageSupergroupChatCreated), + ChannelChatCreated(MessageChannelChatCreated), + MessageAutoDeleteTimerChanged(MessageMessageAutoDeleteTimerChanged), + Pinned(MessagePinned), + Invoice(MessageInvoice), + SuccessfulPayment(MessageSuccessfulPayment), + ConnectedWebsite(MessageConnectedWebsite), + PassportData(MessagePassportData), + Dice(MessageDice), + ProximityAlertTriggered(MessageProximityAlertTriggered), + VideoChatScheduled(MessageVideoChatScheduled), + VideoChatStarted(MessageVideoChatStarted), + VideoChatEnded(MessageVideoChatEnded), + VideoChatParticipantsInvited(MessageVideoChatParticipantsInvited), + WebAppData(MessageWebAppData), +} + +#[serde_with_macros::skip_serializing_none] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct MessageCommon { + /// Sender, empty for messages sent to channels. + pub from: Option, + + /// Sender of the message, sent on behalf of a chat. The channel itself for + /// channel messages. The supergroup itself for messages from anonymous + /// group administrators. The linked channel for messages automatically + /// forwarded to the discussion group + pub sender_chat: Option, + + /// Signature of the post author for messages in channels, or the custom + /// title of an anonymous group administrator. + pub author_signature: Option, + + /// For forwarded messages, information about the forward + #[serde(flatten)] + pub forward: Option, + + /// For replies, the original message. Note that the Message object in this + /// field will not contain further `reply_to_message` fields even if it + /// itself is a reply. + pub reply_to_message: Option>, + + /// Date the message was last edited in Unix time. + #[serde(default, with = "crate::types::serde_opt_date_from_unix_timestamp")] + pub edit_date: Option>, + + #[serde(flatten)] + pub media_kind: MediaKind, + + /// Inline keyboard attached to the message. `login_url` buttons are + /// represented as ordinary `url` buttons. + pub reply_markup: Option, + + /// `true`, if the message is a channel post that was automatically + /// forwarded to the connected discussion group. + #[serde(default)] + pub is_automatic_forward: bool, + + /// `true`, if the message can't be forwarded. + #[serde(default)] + pub has_protected_content: bool, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct MessageNewChatMembers { + /// New members that were added to the group or supergroup and + /// information about them (the bot itself may be one of these + /// members). + pub new_chat_members: Vec, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct MessageLeftChatMember { + /// A member was removed from the group, information about them (this + /// member may be the bot itself). + pub left_chat_member: User, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct MessageNewChatTitle { + /// A chat title was changed to this value. + pub new_chat_title: String, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct MessageNewChatPhoto { + /// A chat photo was change to this value. + pub new_chat_photo: Vec, +} + +#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] +pub struct MessageDeleteChatPhoto { + /// Service message: the chat photo was deleted. + pub delete_chat_photo: True, +} + +#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] +pub struct MessageGroupChatCreated { + /// Service message: the group has been created. + pub group_chat_created: True, +} + +#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] +pub struct MessageSupergroupChatCreated { + /// Service message: the supergroup has been created. This field can‘t + /// be received in a message coming through updates, because bot can’t + /// be a member of a supergroup when it is created. It can only be + /// found in `reply_to_message` if someone replies to a very first + /// message in a directly created supergroup. + pub supergroup_chat_created: True, +} + +#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] +pub struct MessageChannelChatCreated { + /// Service message: the channel has been created. This field can‘t be + /// received in a message coming through updates, because bot can’t be + /// a member of a channel when it is created. It can only be found in + /// `reply_to_message` if someone replies to a very first message in a + /// channel. + pub channel_chat_created: True, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct MessageMessageAutoDeleteTimerChanged { + /// Service message: auto-delete timer settings changed in the chat. + pub message_auto_delete_timer_changed: MessageAutoDeleteTimerChanged, +} + +/// Represents group migration to a supergroup or a supergroup migration from a +/// group. +/// +/// Note that bot receives **both** updates. For example: a group with id `0` +/// migrates to a supergroup with id `1` bots in that group will receive 2 +/// updates: +/// - `message.chat.id = 0`, `message.chat_migration() = ChatMigration::To { +/// chat_id: 1 }` +/// - `message.chat.id = 1`, `message.chat_migration() = ChatMigration::From { +/// chat_id: 0 }` +#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(untagged)] +pub enum ChatMigration { + /// The group has been migrated to a supergroup with the specified + /// identifier `chat_id`. + To { + #[serde(rename = "migrate_to_chat_id")] + chat_id: ChatId, + }, + + /// The supergroup has been migrated from a group with the specified + /// identifier `chat_id`. + From { + #[serde(rename = "migrate_from_chat_id")] + chat_id: ChatId, + }, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct MessagePinned { + /// Specified message was pinned. Note that the Message object in this + /// field will not contain further `reply_to_message` fields even if it + /// is itself a reply. + #[serde(rename = "pinned_message")] + pub pinned: Box, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct MessageInvoice { + /// Message is an invoice for a [payment], information about the + /// invoice. [More about payments »]. + /// + /// [payment]: https://core.telegram.org/bots/api#payments + /// [More about payments »]: https://core.telegram.org/bots/api#payments + pub invoice: Invoice, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct MessageSuccessfulPayment { + /// Message is a service message about a successful payment, + /// information about the payment. [More about payments »]. + /// + /// [More about payments »]: https://core.telegram.org/bots/api#payments + pub successful_payment: SuccessfulPayment, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct MessageConnectedWebsite { + /// The domain name of the website on which the user has logged in. + /// [More about Telegram Login »]. + /// + /// [More about Telegram Login »]: https://core.telegram.org/widgets/login + pub connected_website: String, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct MessagePassportData { + /// Telegram Passport data. + pub passport_data: PassportData, +} + +/// Information about forwarded message. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct Forward { + /// Date the original message was sent in Unix time. + #[serde(rename = "forward_date")] + #[serde(with = "crate::types::serde_date_from_unix_timestamp")] + pub date: DateTime, + + /// The entity that sent the original message. + #[serde(flatten)] + pub from: ForwardedFrom, + + /// For messages forwarded from channels, signature of the post author if + /// present. For messages forwarded from anonymous admins, authors title, if + /// present. + #[serde(rename = "forward_signature")] + pub signature: Option, + + /// For messages forwarded from channels, identifier of the original message + /// in the channel + #[serde(rename = "forward_from_message_id")] + pub message_id: Option, +} + +/// The entity that sent the original message that later was forwarded. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub enum ForwardedFrom { + /// The message was sent by a user. + #[serde(rename = "forward_from")] + User(User), + /// The message was sent by an anonymous user on behalf of a group or + /// channel. + #[serde(rename = "forward_from_chat")] + Chat(Chat), + /// The message was sent by a user who disallow adding a link to their + /// account in forwarded messages. + #[serde(rename = "forward_sender_name")] + SenderName(String), +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(untagged)] +pub enum MediaKind { + // Note: + // - `Venue` must be in front of `Location` + // - `Animation` must be in front of `Document` + // + // This is needed so serde doesn't parse `Venue` as `Location` or `Animation` as `Document` + // (for backward compatability telegram duplicates some fields) + // + // See + Animation(MediaAnimation), + Audio(MediaAudio), + Contact(MediaContact), + Document(MediaDocument), + Game(MediaGame), + Venue(MediaVenue), + Location(MediaLocation), + Photo(MediaPhoto), + Poll(MediaPoll), + Sticker(MediaSticker), + Text(MediaText), + Video(MediaVideo), + VideoNote(MediaVideoNote), + Voice(MediaVoice), + Migration(ChatMigration), +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct MediaAnimation { + /// Message is an animation, information about the animation. For + /// backward compatibility, when this field is set, the document field + /// will also be set. + pub animation: Animation, + + /// Caption for the animation, 0-1024 characters. + pub caption: Option, + + /// For messages with a caption, special entities like usernames, URLs, + /// bot commands, etc. that appear in the caption. + #[serde(default = "Vec::new")] + pub caption_entities: Vec, + // Note: for backward compatibility telegram also sends `document` field, but we ignore it +} + +#[serde_with_macros::skip_serializing_none] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct MediaAudio { + /// Message is an audio file, information about the file. + pub audio: Audio, + + /// Caption for the audio, 0-1024 characters. + pub caption: Option, + + /// For messages with a caption, special entities like usernames, URLs, + /// bot commands, etc. that appear in the caption. + #[serde(default = "Vec::new")] + pub caption_entities: Vec, + + /// The unique identifier of a media message group this message belongs + /// to. + pub media_group_id: Option, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct MediaContact { + /// Message is a shared contact, information about the contact. + pub contact: Contact, +} + +#[serde_with_macros::skip_serializing_none] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct MediaDocument { + /// Message is a general file, information about the file. + pub document: Document, + + /// Caption for the document, 0-1024 characters. + pub caption: Option, + + /// For messages with a caption, special entities like usernames, URLs, + /// bot commands, etc. that appear in the caption. + #[serde(default)] + pub caption_entities: Vec, + + /// The unique identifier of a media message group this message belongs + /// to. + pub media_group_id: Option, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct MediaGame { + /// Message is a game, information about the game. [More + /// about games »]. + /// + /// [More about games »]: https://core.telegram.org/bots/api#games + pub game: Game, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct MediaLocation { + /// Message is a shared location, information about the location. + pub location: Location, +} + +#[serde_with_macros::skip_serializing_none] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct MediaPhoto { + /// Message is a photo, available sizes of the photo. + pub photo: Vec, + + /// Caption for the photo, 0-1024 characters. + pub caption: Option, + + /// For messages with a caption, special entities like usernames, URLs, + /// bot commands, etc. that appear in the caption. + #[serde(default = "Vec::new")] + pub caption_entities: Vec, + + /// The unique identifier of a media message group this message belongs + /// to. + pub media_group_id: Option, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct MediaPoll { + /// Message is a native poll, information about the poll. + pub poll: Poll, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct MediaSticker { + /// Message is a sticker, information about the sticker. + pub sticker: Sticker, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct MediaText { + /// For text messages, the actual UTF-8 text of the message, 0-4096 + /// characters. + pub text: String, + + /// For text messages, special entities like usernames, URLs, bot + /// commands, etc. that appear in the text. + #[serde(default = "Vec::new")] + pub entities: Vec, +} + +#[serde_with_macros::skip_serializing_none] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct MediaVideo { + /// Message is a video, information about the video. + pub video: Video, + + /// Caption for the video, 0-1024 characters. + pub caption: Option, + + /// For messages with a caption, special entities like usernames, URLs, + /// bot commands, etc. that appear in the caption. + #[serde(default = "Vec::new")] + pub caption_entities: Vec, + + /// The unique identifier of a media message group this message belongs + /// to. + pub media_group_id: Option, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct MediaVideoNote { + /// Message is a [video note], information about the video message. + /// + /// [video note]: https://telegram.org/blog/video-messages-and-telescope + pub video_note: VideoNote, +} + +#[serde_with_macros::skip_serializing_none] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct MediaVoice { + /// Message is a voice message, information about the file. + pub voice: Voice, + + /// Caption for the voice, 0-1024 characters. + pub caption: Option, + + /// For messages with a caption, special entities like usernames, URLs, + /// bot commands, etc. that appear in the caption. + #[serde(default = "Vec::new")] + pub caption_entities: Vec, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct MediaVenue { + /// Message is a venue, information about the venue. + pub venue: Venue, + // Note: for backward compatibility telegram also sends `location` field, but we ignore it +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct MessageDice { + /// Message is a dice with random value from 1 to 6. + pub dice: Dice, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct MessageProximityAlertTriggered { + /// Service message. A user in the chat triggered another user's proximity + /// alert while sharing Live Location. + pub proximity_alert_triggered: ProximityAlertTriggered, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct MessageVideoChatScheduled { + /// Service message: video chat scheduled + pub video_chat_scheduled: VideoChatScheduled, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct MessageVideoChatStarted { + /// Service message: video chat started. + pub video_chat_started: VideoChatStarted, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct MessageVideoChatEnded { + /// Service message: video chat ended. + pub video_chat_ended: VideoChatEnded, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct MessageVideoChatParticipantsInvited { + /// Service message: new participants invited to a video chat. + pub video_chat_participants_invited: VideoChatParticipantsInvited, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct MessageWebAppData { + /// Service message: data sent by a Web App. + pub web_app_data: WebAppData, +} + +mod getters { + use chrono::{DateTime, Utc}; + use std::ops::Deref; + + use crate::types::{ + self, message::MessageKind::*, Chat, ChatId, ChatMigration, Forward, ForwardedFrom, + MediaAnimation, MediaAudio, MediaContact, MediaDocument, MediaGame, MediaKind, + MediaLocation, MediaPhoto, MediaPoll, MediaSticker, MediaText, MediaVenue, MediaVideo, + MediaVideoNote, MediaVoice, Message, MessageChannelChatCreated, MessageCommon, + MessageConnectedWebsite, MessageDeleteChatPhoto, MessageDice, MessageEntity, + MessageGroupChatCreated, MessageInvoice, MessageLeftChatMember, MessageNewChatMembers, + MessageNewChatPhoto, MessageNewChatTitle, MessagePassportData, MessagePinned, + MessageProximityAlertTriggered, MessageSuccessfulPayment, MessageSupergroupChatCreated, + PhotoSize, True, User, + }; + + /// Getters for [Message] fields from [telegram docs]. + /// + /// [Message]: crate::types::Message + /// [telegram docs]: https://core.telegram.org/bots/api#message + impl Message { + #[must_use] + pub fn from(&self) -> Option<&User> { + match &self.kind { + Common(MessageCommon { from, .. }) => from.as_ref(), + _ => None, + } + } + + #[must_use] + pub fn author_signature(&self) -> Option<&str> { + match &self.kind { + Common(MessageCommon { author_signature, .. }) => author_signature.as_deref(), + _ => None, + } + } + + #[must_use] + pub fn sender_chat(&self) -> Option<&Chat> { + match &self.kind { + Common(MessageCommon { sender_chat, .. }) => sender_chat.as_ref(), + _ => None, + } + } + + #[deprecated(since = "0.4.2", note = "use `.chat.id` field instead")] + #[must_use] + pub fn chat_id(&self) -> ChatId { + self.chat.id + } + + #[must_use] + pub fn forward(&self) -> Option<&Forward> { + self.common().and_then(|m| m.forward.as_ref()) + } + + #[must_use] + pub fn forward_date(&self) -> Option> { + self.forward().map(|f| f.date) + } + + #[must_use] + pub fn forward_from(&self) -> Option<&ForwardedFrom> { + self.forward().map(|f| &f.from) + } + + #[must_use] + pub fn forward_from_user(&self) -> Option<&User> { + self.forward_from().and_then(|from| match from { + ForwardedFrom::User(user) => Some(user), + _ => None, + }) + } + + #[must_use] + pub fn forward_from_chat(&self) -> Option<&Chat> { + self.forward_from().and_then(|from| match from { + ForwardedFrom::Chat(chat) => Some(chat), + _ => None, + }) + } + + #[must_use] + pub fn forward_from_sender_name(&self) -> Option<&str> { + self.forward_from().and_then(|from| match from { + ForwardedFrom::SenderName(sender_name) => Some(&**sender_name), + _ => None, + }) + } + + #[must_use] + pub fn forward_from_message_id(&self) -> Option { + self.forward().and_then(|f| f.message_id) + } + + #[must_use] + pub fn forward_signature(&self) -> Option<&str> { + self.forward().and_then(|f| f.signature.as_deref()) + } + + #[must_use] + pub fn reply_to_message(&self) -> Option<&Message> { + self.common().and_then(|m| m.reply_to_message.as_deref()) + } + + #[must_use] + pub fn edit_date(&self) -> Option<&DateTime> { + match &self.kind { + Common(MessageCommon { edit_date, .. }) => edit_date.as_ref(), + _ => None, + } + } + + #[must_use] + pub fn media_group_id(&self) -> Option<&str> { + match &self.kind { + Common(MessageCommon { + media_kind: MediaKind::Video(MediaVideo { media_group_id, .. }), + .. + }) + | Common(MessageCommon { + media_kind: MediaKind::Photo(MediaPhoto { media_group_id, .. }), + .. + }) + | Common(MessageCommon { + media_kind: MediaKind::Document(MediaDocument { media_group_id, .. }), + .. + }) + | Common(MessageCommon { + media_kind: MediaKind::Audio(MediaAudio { media_group_id, .. }), + .. + }) => media_group_id.as_ref().map(Deref::deref), + _ => None, + } + } + + #[must_use] + pub fn text(&self) -> Option<&str> { + match &self.kind { + Common(MessageCommon { + media_kind: MediaKind::Text(MediaText { text, .. }), + .. + }) => Some(text), + _ => None, + } + } + + /// Returns message entities that represent text formatting. + /// + /// **Note:** you probably want to use [`parse_entities`] instead. + /// + /// This function returns `Some(entities)` for **text messages** and + /// `None` for all other kinds of messages (including photos with + /// captions). + /// + /// See also: [`caption_entities`]. + /// + /// [`parse_entities`]: Message::parse_entities + /// [`caption_entities`]: Message::caption_entities + #[must_use] + pub fn entities(&self) -> Option<&[MessageEntity]> { + match &self.kind { + Common(MessageCommon { + media_kind: MediaKind::Text(MediaText { entities, .. }), + .. + }) => Some(entities), + _ => None, + } + } + + /// Returns message entities that represent text formatting. + /// + /// **Note:** you probably want to use [`parse_caption_entities`] + /// instead. + /// + /// This function returns `Some(entities)` for **media messages** and + /// `None` for all other kinds of messages (including text messages). + /// + /// See also: [`entities`]. + /// + /// [`parse_caption_entities`]: Message::parse_caption_entities + /// [`entities`]: Message::entities + #[must_use] + pub fn caption_entities(&self) -> Option<&[MessageEntity]> { + match &self.kind { + Common(MessageCommon { + media_kind: MediaKind::Animation(MediaAnimation { caption_entities, .. }), + .. + }) + | Common(MessageCommon { + media_kind: MediaKind::Audio(MediaAudio { caption_entities, .. }), + .. + }) + | Common(MessageCommon { + media_kind: MediaKind::Document(MediaDocument { caption_entities, .. }), + .. + }) + | Common(MessageCommon { + media_kind: MediaKind::Photo(MediaPhoto { caption_entities, .. }), + .. + }) + | Common(MessageCommon { + media_kind: MediaKind::Video(MediaVideo { caption_entities, .. }), + .. + }) + | Common(MessageCommon { + media_kind: MediaKind::Voice(MediaVoice { caption_entities, .. }), + .. + }) => Some(caption_entities), + _ => None, + } + } + + #[must_use] + pub fn audio(&self) -> Option<&types::Audio> { + match &self.kind { + Common(MessageCommon { + media_kind: MediaKind::Audio(MediaAudio { audio, .. }), + .. + }) => Some(audio), + _ => None, + } + } + + #[must_use] + pub fn document(&self) -> Option<&types::Document> { + match &self.kind { + Common(MessageCommon { + media_kind: MediaKind::Document(MediaDocument { document, .. }), + .. + }) => Some(document), + _ => None, + } + } + + #[must_use] + pub fn animation(&self) -> Option<&types::Animation> { + match &self.kind { + Common(MessageCommon { + media_kind: MediaKind::Animation(MediaAnimation { animation, .. }), + .. + }) => Some(animation), + _ => None, + } + } + + #[must_use] + pub fn game(&self) -> Option<&types::Game> { + match &self.kind { + Common(MessageCommon { + media_kind: MediaKind::Game(MediaGame { game, .. }), + .. + }) => Some(game), + _ => None, + } + } + + #[must_use] + pub fn photo(&self) -> Option<&[PhotoSize]> { + match &self.kind { + Common(MessageCommon { + media_kind: MediaKind::Photo(MediaPhoto { photo, .. }), + .. + }) => Some(photo), + _ => None, + } + } + + #[must_use] + pub fn sticker(&self) -> Option<&types::Sticker> { + match &self.kind { + Common(MessageCommon { + media_kind: MediaKind::Sticker(MediaSticker { sticker, .. }), + .. + }) => Some(sticker), + _ => None, + } + } + + #[must_use] + pub fn video(&self) -> Option<&types::Video> { + match &self.kind { + Common(MessageCommon { + media_kind: MediaKind::Video(MediaVideo { video, .. }), + .. + }) => Some(video), + _ => None, + } + } + + #[must_use] + pub fn voice(&self) -> Option<&types::Voice> { + match &self.kind { + Common(MessageCommon { + media_kind: MediaKind::Voice(MediaVoice { voice, .. }), + .. + }) => Some(voice), + _ => None, + } + } + + #[must_use] + pub fn video_note(&self) -> Option<&types::VideoNote> { + match &self.kind { + Common(MessageCommon { + media_kind: MediaKind::VideoNote(MediaVideoNote { video_note, .. }), + .. + }) => Some(video_note), + _ => None, + } + } + + #[must_use] + pub fn caption(&self) -> Option<&str> { + match &self.kind { + Common(MessageCommon { media_kind, .. }) => match media_kind { + MediaKind::Animation(MediaAnimation { caption, .. }) + | MediaKind::Audio(MediaAudio { caption, .. }) + | MediaKind::Document(MediaDocument { caption, .. }) + | MediaKind::Photo(MediaPhoto { caption, .. }) + | MediaKind::Video(MediaVideo { caption, .. }) + | MediaKind::Voice(MediaVoice { caption, .. }) => { + caption.as_ref().map(Deref::deref) + } + _ => None, + }, + _ => None, + } + } + + #[must_use] + pub fn contact(&self) -> Option<&types::Contact> { + match &self.kind { + Common(MessageCommon { + media_kind: MediaKind::Contact(MediaContact { contact, .. }), + .. + }) => Some(contact), + _ => None, + } + } + + #[must_use] + pub fn location(&self) -> Option<&types::Location> { + match &self.kind { + Common(MessageCommon { + media_kind: MediaKind::Location(MediaLocation { location, .. }), + .. + }) => Some(location), + _ => None, + } + } + + #[must_use] + pub fn venue(&self) -> Option<&types::Venue> { + match &self.kind { + Common(MessageCommon { + media_kind: MediaKind::Venue(MediaVenue { venue, .. }), + .. + }) => Some(venue), + _ => None, + } + } + + #[must_use] + pub fn poll(&self) -> Option<&types::Poll> { + match &self.kind { + Common(MessageCommon { + media_kind: MediaKind::Poll(MediaPoll { poll, .. }), + .. + }) => Some(poll), + _ => None, + } + } + + #[must_use] + pub fn new_chat_members(&self) -> Option<&[User]> { + match &self.kind { + NewChatMembers(MessageNewChatMembers { new_chat_members }) => { + Some(new_chat_members.as_ref()) + } + _ => None, + } + } + + #[must_use] + pub fn left_chat_member(&self) -> Option<&User> { + match &self.kind { + LeftChatMember(MessageLeftChatMember { left_chat_member }) => { + Some(left_chat_member) + } + _ => None, + } + } + + #[must_use] + pub fn new_chat_title(&self) -> Option<&str> { + match &self.kind { + NewChatTitle(MessageNewChatTitle { new_chat_title }) => Some(new_chat_title), + _ => None, + } + } + + #[must_use] + pub fn new_chat_photo(&self) -> Option<&[PhotoSize]> { + match &self.kind { + NewChatPhoto(MessageNewChatPhoto { new_chat_photo }) => Some(new_chat_photo), + _ => None, + } + } + + // TODO: OK, `Option` is weird, can we do something with it? + // mb smt like `is_delete_chat_photo(&self) -> bool`? + #[must_use] + pub fn delete_chat_photo(&self) -> Option { + match &self.kind { + DeleteChatPhoto(MessageDeleteChatPhoto { delete_chat_photo }) => { + Some(*delete_chat_photo) + } + _ => None, + } + } + + #[must_use] + pub fn group_chat_created(&self) -> Option { + match &self.kind { + GroupChatCreated(MessageGroupChatCreated { group_chat_created }) => { + Some(*group_chat_created) + } + _ => None, + } + } + + #[must_use] + pub fn super_group_chat_created(&self) -> Option { + match &self.kind { + SupergroupChatCreated(MessageSupergroupChatCreated { supergroup_chat_created }) => { + Some(*supergroup_chat_created) + } + _ => None, + } + } + + #[must_use] + pub fn channel_chat_created(&self) -> Option { + match &self.kind { + ChannelChatCreated(MessageChannelChatCreated { channel_chat_created }) => { + Some(*channel_chat_created) + } + _ => None, + } + } + + #[must_use] + pub fn chat_migration(&self) -> Option { + match &self.kind { + Common(MessageCommon { + media_kind: MediaKind::Migration(chat_migration), .. + }) => Some(*chat_migration), + _ => None, + } + } + + #[must_use] + pub fn migrate_to_chat_id(&self) -> Option { + match &self.kind { + Common(MessageCommon { + media_kind: MediaKind::Migration(ChatMigration::To { chat_id }), + .. + }) => Some(*chat_id), + _ => None, + } + } + + #[must_use] + pub fn migrate_from_chat_id(&self) -> Option { + match &self.kind { + Common(MessageCommon { + media_kind: MediaKind::Migration(ChatMigration::From { chat_id }), + .. + }) => Some(*chat_id), + _ => None, + } + } + + #[must_use] + pub fn pinned_message(&self) -> Option<&Message> { + match &self.kind { + Pinned(MessagePinned { pinned }) => Some(pinned), + _ => None, + } + } + + #[must_use] + pub fn invoice(&self) -> Option<&types::Invoice> { + match &self.kind { + Invoice(MessageInvoice { invoice }) => Some(invoice), + _ => None, + } + } + + #[must_use] + pub fn successful_payment(&self) -> Option<&types::SuccessfulPayment> { + match &self.kind { + SuccessfulPayment(MessageSuccessfulPayment { successful_payment }) => { + Some(successful_payment) + } + _ => None, + } + } + + #[must_use] + pub fn connected_website(&self) -> Option<&str> { + match &self.kind { + ConnectedWebsite(MessageConnectedWebsite { connected_website }) => { + Some(connected_website) + } + _ => None, + } + } + + #[must_use] + pub fn passport_data(&self) -> Option<&types::PassportData> { + match &self.kind { + PassportData(MessagePassportData { passport_data }) => Some(passport_data), + _ => None, + } + } + + #[must_use] + pub fn dice(&self) -> Option<&types::Dice> { + match &self.kind { + Dice(MessageDice { dice }) => Some(dice), + _ => None, + } + } + + #[must_use] + pub fn proximity_alert_triggered(&self) -> Option<&types::ProximityAlertTriggered> { + match &self.kind { + ProximityAlertTriggered(MessageProximityAlertTriggered { + proximity_alert_triggered, + }) => Some(proximity_alert_triggered), + _ => None, + } + } + + #[must_use] + pub fn reply_markup(&self) -> Option<&types::InlineKeyboardMarkup> { + match &self.kind { + Common(MessageCommon { reply_markup, .. }) => reply_markup.as_ref(), + _ => None, + } + } + + #[must_use] + pub fn is_automatic_forward(&self) -> bool { + match &self.kind { + Common(MessageCommon { is_automatic_forward, .. }) => *is_automatic_forward, + _ => false, + } + } + + #[must_use] + pub fn has_protected_content(&self) -> bool { + match &self.kind { + Common(MessageCommon { has_protected_content, .. }) => *has_protected_content, + _ => false, + } + } + + /// Common message (text, image, etc) + fn common(&self) -> Option<&MessageCommon> { + match &self.kind { + Common(message) => Some(message), + _ => None, + } + } + } +} + +impl Message { + /// Produces a direct link to this message. + /// + /// Note that for private groups the link will only be accessible for group + /// members. + /// + /// Returns `None` for private chats (i.e.: DMs) and private groups (not + /// supergroups). + #[must_use] + pub fn url(&self) -> Option { + Self::url_of(self.chat.id, self.chat.username(), self.id) + } + + /// Produces a direct link to a message in a chat. + /// + /// If you have a `Message` object, use [`url`] instead. + /// This function should only be used if you have limited information about + /// the message (chat id, username of the chat, if any and its id). + /// + /// Note that for private groups the link will only be accessible for group + /// members. + /// + /// Returns `None` for private chats (i.e.: DMs) and private groups (not + /// supergroups). + /// + /// [`url`]: Message::url + #[track_caller] + #[must_use] + pub fn url_of( + chat_id: ChatId, + chat_username: Option<&str>, + message_id: MessageId, + ) -> Option { + use BareChatId::*; + + // Note: `t.me` links use bare chat ids + let chat_id = match chat_id.to_bare() { + // For private chats (i.e.: DMs) we can't produce "normal" t.me link. + // + // There are "tg://openmessage?user_id={0}&message_id={1}" links, which are + // supposed to open any chat, including private messages, but they + // are only supported by some telegram clients (e.g. Plus Messenger, + // Telegram for Android 4.9+). + User(_) => return None, + // Similarly to user chats, there is no way to create a link to a message in a normal, + // private group. + // + // (public groups are always supergroup which are in turn channels). + Group(_) => return None, + Channel(id) => id, + }; + + let url = match chat_username { + // If it's public group (i.e. not DM, not private group), we can produce + // "normal" t.me link (accessible to everyone). + Some(username) => format!("https://t.me/{0}/{1}", username, message_id.0), + // For private supergroups and channels we produce "private" t.me/c links. These are + // only accessible to the group members. + None => format!("https://t.me/c/{0}/{1}", chat_id, message_id.0), + }; + + // UNWRAP: + // + // The `url` produced by formatting is correct since username is + // /[a-zA-Z0-9_]{5,32}/ and chat/message ids are integers. + Some(reqwest::Url::parse(&url).unwrap()) + } + + /// Produces a direct link to a comment on this post. + /// + /// Note that for private channels the link will only be accessible for + /// channel members. + /// + /// Returns `None` for private chats (i.e.: DMs) and private groups (not + /// supergroups). + #[must_use] + pub fn comment_url(&self, comment_id: MessageId) -> Option { + Self::comment_url_of(self.chat.id, self.chat.username(), self.id, comment_id) + } + + /// Produces a direct link to a comment on a post. + /// + /// If you have a `Message` object of the channel post, use [`comment_url`] + /// instead. This function should only be used if you have limited + /// information about the message (channel id, username of the channel, + /// if any, post id and comment id). + /// + /// Note that for private channels the link will only be accessible for + /// channel members. + /// + /// Returns `None` for private chats (i.e.: DMs) and private groups (not + /// supergroups). + /// + /// [`comment_url`]: Message::comment_url + #[must_use] + pub fn comment_url_of( + channel_id: ChatId, + channel_username: Option<&str>, + post_id: MessageId, + comment_id: MessageId, + ) -> Option { + Self::url_of(channel_id, channel_username, post_id).map(|mut url| { + url.set_query(Some(&format!("comment={}", comment_id.0))); + url + }) + } + + /// Produces a direct link to this message in a given thread. + /// + /// "Thread" is a group of messages that reply to each other in a tree-like + /// structure. `thread_starter_msg_id` is the id of the first message in + /// the thread, the root of the tree. + /// + /// Note that for private groups the link will only be accessible for group + /// members. + /// + /// Returns `None` for private chats (i.e.: DMs) and private groups (not + /// supergroups). + #[must_use] + pub fn url_in_thread(&self, thread_starter_msg_id: MessageId) -> Option { + Self::url_in_thread_of(self.chat.id, self.chat.username(), thread_starter_msg_id, self.id) + } + + /// Produces a direct link to a message in a given thread. + /// + /// If you have a `Message` object of the channel post, use + /// [`url_in_thread`] instead. This function should only be used if you + /// have limited information about the message (chat id, username of the + /// chat, if any, thread starter id and message id). + /// + /// "Thread" is a group of messages that reply to each other in a tree-like + /// structure. `thread_starter_msg_id` is the id of the first message in + /// the thread, the root of the tree. + /// + /// Note that for private groups the link will only be accessible for group + /// members. + /// + /// Returns `None` for private chats (i.e.: DMs) and private groups (not + /// supergroups). + /// + /// [`url_in_thread`]: Message::url_in_thread + #[must_use] + pub fn url_in_thread_of( + chat_id: ChatId, + chat_username: Option<&str>, + thread_starter_msg_id: MessageId, + message_id: MessageId, + ) -> Option { + Self::url_of(chat_id, chat_username, message_id).map(|mut url| { + url.set_query(Some(&format!("thread={}", thread_starter_msg_id.0))); + url + }) + } + + /// Returns message entities that represent text formatting. + /// + /// This function returns `Some(entities)` for **text messages** and + /// `None` for all other kinds of messages (including photos with + /// captions). + /// + /// See also: [`parse_caption_entities`]. + /// + /// [`parse_caption_entities`]: Message::parse_caption_entities + #[must_use] + pub fn parse_entities(&self) -> Option>> { + self.text().zip(self.entities()).map(|(t, e)| MessageEntityRef::parse(t, e)) + } + + /// Returns message entities that represent text formatting. + /// + /// This function returns `Some(entities)` for **media messages** and + /// `None` for all other kinds of messages (including text messages). + /// + /// See also: [`parse_entities`]. + /// + /// [`parse_entities`]: Message::parse_entities + #[must_use] + pub fn parse_caption_entities(&self) -> Option>> { + self.caption().zip(self.caption_entities()).map(|(t, e)| MessageEntityRef::parse(t, e)) + } +} + +#[cfg(test)] +mod tests { + use serde_json::from_str; + + use crate::types::*; + + #[test] + fn de_media_forwarded() { + let json = r#"{ + "message_id": 198283, + "from": { + "id": 250918540, + "is_bot": false, + "first_name": "Андрей", + "last_name": "Власов", + "username": "aka_dude", + "language_code": "en" + }, + "chat": { + "id": 250918540, + "first_name": "Андрей", + "last_name": "Власов", + "username": "aka_dude", + "type": "private" + }, + "date": 1567927221, + "video": { + "duration": 13, + "width": 512, + "height": 640, + "mime_type": "video/mp4", + "thumb": { + "file_id": "AAQCAAOmBAACBf2oS53pByA-I4CWWCObDwAEAQAHbQADMWcAAhYE", + "file_unique_id":"", + "file_size": 10339, + "width": 256, + "height": 320 + }, + "file_id": "BAADAgADpgQAAgX9qEud6QcgPiOAlhYE", + "file_unique_id":"", + "file_size": 1381334 + } + }"#; + let message = from_str::(json); + assert!(message.is_ok()); + } + + #[test] + fn de_media_group_forwarded() { + let json = r#"{ + "message_id": 198283, + "from": { + "id": 250918540, + "is_bot": false, + "first_name": "Андрей", + "last_name": "Власов", + "username": "aka_dude", + "language_code": "en" + }, + "chat": { + "id": 250918540, + "first_name": "Андрей", + "last_name": "Власов", + "username": "aka_dude", + "type": "private" + }, + "date": 1567927221, + "media_group_id": "12543417770506682", + "video": { + "duration": 13, + "width": 512, + "height": 640, + "mime_type": "video/mp4", + "thumb": { + "file_id": "AAQCAAOmBAACBf2oS53pByA-I4CWWCObDwAEAQAHbQADMWcAAhYE", + "file_unique_id":"", + "file_size": 10339, + "width": 256, + "height": 320 + }, + "file_id": "BAADAgADpgQAAgX9qEud6QcgPiOAlhYE", + "file_unique_id":"", + "file_size": 1381334 + } + }"#; + let message = from_str::(json); + assert!(message.is_ok()); + } + + #[test] + fn de_text() { + let json = r#"{ + "message_id": 199785, + "from": { + "id": 250918540, + "is_bot": false, + "first_name": "Андрей", + "last_name": "Власов", + "username": "aka_dude", + "language_code": "en" + }, + "chat": { + "id": 250918540, + "first_name": "Андрей", + "last_name": "Власов", + "username": "aka_dude", + "type": "private" + }, + "date": 1568289890, + "text": "Лол кек 😂" + }"#; + let message = from_str::(json); + assert!(message.is_ok()); + } + + #[test] + fn de_sticker() { + let json = r#"{ + "message_id": 199787, + "from": { + "id": 250918540, + "is_bot": false, + "first_name": "Андрей", + "last_name": "Власов", + "username": "aka_dude", + "language_code": "en" + }, + "chat": { + "id": 250918540, + "first_name": "Андрей", + "last_name": "Власов", + "username": "aka_dude", + "type": "private" + }, + "date": 1568290188, + "sticker": { + "width": 512, + "height": 512, + "emoji": "😡", + "set_name": "AdvenTimeAnim", + "is_animated": true, + "is_video": false, + "type": "regular", + "thumb": { + "file_id": "AAMCAgADGQEAARIt0GMwiZ6n4nRbxdpM3pL8vPX6PVAhAAIjAAOw0PgMaabKAcaXKCABAAdtAAMpBA", + "file_unique_id": "AQADIwADsND4DHI", + "file_size": 4118, + "width": 128, + "height": 128 + }, + "file_id": "CAACAgIAAxkBAAESLdBjMImep-J0W8XaTN6S_Lz1-j1QIQACIwADsND4DGmmygHGlyggKQQ", + "file_unique_id": "AgADIwADsND4DA", + "file_size": 16639 + } + }"#; + from_str::(json).unwrap(); + } + + #[test] + fn de_image() { + let json = r#"{ + "message_id": 199791, + "from": { + "id": 250918540, + "is_bot": false, + "first_name": "Андрей", + "last_name": "Власов", + "username": "aka_dude", + "language_code": "en" + }, + "chat": { + "id": 250918540, + "first_name": "Андрей", + "last_name": "Власов", + "username": "aka_dude", + "type": "private" + }, + "date": 1568290622, + "photo": [ + { + "file_id": "AgADAgAD36sxG-PX0UvQSXIn9rccdw-ACA4ABAEAAwIAA20AAybcBAABFgQ", + "file_unique_id":"", + "file_size": 18188, + "width": 320, + "height": 239 + }, + { + "file_id": "AgADAgAD36sxG-PX0UvQSXIn9rccdw-ACA4ABAEAAwIAA3gAAyfcBAABFgQ", + "file_unique_id":"", + "file_size": 62123, + "width": 800, + "height": 598 + }, + { + "file_id": "AgADAgAD36sxG-PX0UvQSXIn9rccdw-ACA4ABAEAAwIAA3kAAyTcBAABFgQ", + "file_unique_id":"", + "file_size": 75245, + "width": 962, + "height": 719 + } + ] + }"#; + let message = from_str::(json); + assert!(message.is_ok()); + } + + /// Regression test for + #[test] + fn issue_419() { + let json = r#"{ + "message_id": 1, + "from": { + "id": 1087968824, + "is_bot": true, + "first_name": "Group", + "username": "GroupAnonymousBot" + }, + "author_signature": "TITLE2", + "sender_chat": { + "id": -1001160242915, + "title": "a", + "type": "supergroup" + }, + "chat": { + "id": -1001160242915, + "title": "a", + "type": "supergroup" + }, + "date": 1640359576, + "forward_from_chat": { + "id": -1001160242915, + "title": "a", + "type": "supergroup" + }, + "forward_signature": "TITLE", + "forward_date": 1640359544, + "text": "text" + }"#; + + // Anonymous admin with title "TITLE2" forwards a message from anonymous + // admin with title "TITLE" with text "a", everything is happening in + // the same group. + let message: Message = serde_json::from_str(json).unwrap(); + + let group = Chat { + id: ChatId(-1001160242915), + kind: ChatKind::Public(ChatPublic { + title: Some("a".to_owned()), + kind: PublicChatKind::Supergroup(PublicChatSupergroup { + username: None, + sticker_set_name: None, + can_set_sticker_set: None, + permissions: None, + slow_mode_delay: None, + linked_chat_id: None, + location: None, + join_by_request: None, + join_to_send_messages: None, + }), + description: None, + invite_link: None, + has_protected_content: None, + }), + message_auto_delete_time: None, + photo: None, + pinned_message: None, + }; + + assert!(message.from().unwrap().is_anonymous()); + assert_eq!(message.author_signature().unwrap(), "TITLE2"); + assert_eq!(message.sender_chat().unwrap(), &group); + assert_eq!(&message.chat, &group); + assert_eq!(message.forward_from_chat().unwrap(), &group); + assert_eq!(message.forward_signature().unwrap(), "TITLE"); + assert!(message.forward_date().is_some()); + assert_eq!(message.text().unwrap(), "text"); + } + + /// Regression test for + #[test] + fn issue_427() { + let old = ChatId(-599075523); + let new = ChatId(-1001555296434); + + // Migration to a supergroup + let json = r#"{"chat":{"all_members_are_administrators":false,"id":-599075523,"title":"test","type":"group"},"date":1629404938,"from":{"first_name":"nullptr","id":729497414,"is_bot":false,"language_code":"en","username":"hex0x0000"},"message_id":16,"migrate_to_chat_id":-1001555296434}"#; + let message: Message = from_str(json).unwrap(); + + assert_eq!(message.chat.id, old); + assert_eq!(message.chat_migration(), Some(ChatMigration::To { chat_id: new })); + assert_eq!(message.migrate_to_chat_id(), Some(new)); + + // The user who initialized the migration + assert!(message.from().is_some()); + + // Migration from a common group + let json = r#"{"chat":{"id":-1001555296434,"title":"test","type":"supergroup"},"date":1629404938,"from":{"first_name":"Group","id":1087968824,"is_bot":true,"username":"GroupAnonymousBot"},"message_id":1,"migrate_from_chat_id":-599075523,"sender_chat":{"id":-1001555296434,"title":"test","type":"supergroup"}}"#; + let message: Message = from_str(json).unwrap(); + + assert_eq!(message.chat.id, new); + assert_eq!(message.chat_migration(), Some(ChatMigration::From { chat_id: old })); + assert_eq!(message.migrate_from_chat_id(), Some(old)); + + // Anonymous bot + assert!(message.from().is_some()); + + // The chat to which the group migrated + assert!(message.sender_chat().is_some()); + } + + /// Regression test for + #[test] + fn issue_481() { + let json = r#" +{ + "message_id": 0, + "date": 0, + "location": { + "latitude": 0.0, + "longitude": 0.0 + }, + "chat": { + "id": 0, + "first_name": "f", + "type": "private" + }, + "venue": { + "location": { + "latitude": 0.0, + "longitude": 0.0 + }, + "title": "Title", + "address": "Address", + "foursquare_id": "some_foursquare_id" + } + } +"#; + let message: Message = from_str(json).unwrap(); + assert_eq!( + message.venue().unwrap(), + &Venue { + location: Location { + longitude: 0.0, + latitude: 0.0, + horizontal_accuracy: None, + live_period: None, + heading: None, + proximity_alert_radius: None + }, + title: "Title".to_owned(), + address: "Address".to_owned(), + foursquare_id: Some("some_foursquare_id".to_owned()), + foursquare_type: None, + google_place_id: None, + google_place_type: None, + } + ) + } + + /// Regression test for + #[test] + fn issue_475() { + let json = r#"{"message_id":198295,"from":{"id":1087968824,"is_bot":true,"first_name":"Group","username":"GroupAnonymousBot"},"sender_chat":{"id":-1001331354980,"title":"C++ Together 2.0","username":"cpptogether","type":"supergroup"},"chat":{"id":-1001331354980,"title":"C++ Together 2.0","username":"cpptogether","type":"supergroup"},"date":1638236631,"video_chat_started":{}}"#; + + let message: Message = serde_json::from_str(json).unwrap(); + + assert!(matches!(message.kind, MessageKind::VideoChatStarted { .. })); + + // FIXME(waffle): it seems like we are losing `sender_chat` in some + // cases inclusing this + // assert!(message.sender_chat().is_some()); + } + + #[test] + fn parse_caption_entities() { + let json = r#" + { + "message_id": 3460, + "from": { + "id": 27433968, + "is_bot": false, + "first_name": "Crax | rats addict", + "username": "tacocrasco", + "language_code": "en" + }, + "chat": { + "id": 27433968, + "first_name": "Crax | rats addict", + "username": "tacocrasco", + "type": "private" + }, + "date": 1655671349, + "photo": [ + { + "file_id": "AgACAgQAAxkBAAINhGKvijUVSn2i3980bQIIc1fqWGNCAAJpvDEbEmaBUfuA43fR-BnlAQADAgADcwADJAQ", + "file_unique_id": "AQADabwxGxJmgVF4", + "file_size": 2077, + "width": 90, + "height": 90 + }, + { + "file_id": "AgACAgQAAxkBAAINhGKvijUVSn2i3980bQIIc1fqWGNCAAJpvDEbEmaBUfuA43fR-BnlAQADAgADbQADJAQ", + "file_unique_id": "AQADabwxGxJmgVFy", + "file_size": 27640, + "width": 320, + "height": 320 + }, + { + "file_id": "AgACAgQAAxkBAAINhGKvijUVSn2i3980bQIIc1fqWGNCAAJpvDEbEmaBUfuA43fR-BnlAQADAgADeAADJAQ", + "file_unique_id": "AQADabwxGxJmgVF9", + "file_size": 99248, + "width": 800, + "height": 800 + }, + { + "file_id": "AgACAgQAAxkBAAINhGKvijUVSn2i3980bQIIc1fqWGNCAAJpvDEbEmaBUfuA43fR-BnlAQADAgADeQADJAQ", + "file_unique_id": "AQADabwxGxJmgVF-", + "file_size": 162061, + "width": 1280, + "height": 1280 + } + ], + "caption": "www.example.com", + "caption_entities": [ + { + "offset": 0, + "length": 15, + "type": "url" + } + ] + }"#; + + let message: Message = serde_json::from_str(json).unwrap(); + let entities = message.parse_caption_entities(); + assert!(entities.is_some()); + + let entities = entities.unwrap(); + assert!(!entities.is_empty()); + assert_eq!(entities[0].kind().clone(), MessageEntityKind::Url); + } +} diff --git a/crates/teloxide-core/src/types/message_auto_delete_timer_changed.rs b/crates/teloxide-core/src/types/message_auto_delete_timer_changed.rs new file mode 100644 index 00000000..0e24be5a --- /dev/null +++ b/crates/teloxide-core/src/types/message_auto_delete_timer_changed.rs @@ -0,0 +1,9 @@ +use serde::{Deserialize, Serialize}; + +/// This object represents a service message about a change in auto-delete timer +/// settings. +#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)] +pub struct MessageAutoDeleteTimerChanged { + /// New auto-delete time for messages in the chat + pub message_auto_delete_time: u32, +} diff --git a/crates/teloxide-core/src/types/message_entity.rs b/crates/teloxide-core/src/types/message_entity.rs new file mode 100644 index 00000000..98f90615 --- /dev/null +++ b/crates/teloxide-core/src/types/message_entity.rs @@ -0,0 +1,405 @@ +use std::{cmp, ops::Range}; + +use serde::{Deserialize, Serialize}; + +use crate::types::{User, UserId}; + +/// This object represents one special entity in a text message. +/// +/// For example, hashtags, usernames, URLs, etc. +/// +/// [The official docs](https://core.telegram.org/bots/api#messageentity). +#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)] +pub struct MessageEntity { + #[serde(flatten)] + pub kind: MessageEntityKind, + + /// Offset in UTF-16 code units to the start of the entity. + pub offset: usize, + + /// Length of the entity in UTF-16 code units. + pub length: usize, +} + +/// A "parsed" [`MessageEntity`]. +/// +/// [`MessageEntity`] has offsets in UTF-**16** code units, but in Rust we +/// mostly work with UTF-**8**. In order to use an entity we need to convert +/// UTF-16 offsets to UTF-8 ones. This type represents a message entity with +/// converted offsets and a reference to the text. +/// +/// You can get [`MessageEntityRef`]s by calling [`parse_entities`] and +/// [`parse_caption_entities`] methods of [`Message`] or by calling +/// [`MessageEntityRef::parse`]. +/// +/// [`parse_entities`]: crate::types::Message::parse_entities +/// [`parse_caption_entities`]: crate::types::Message::parse_caption_entities +/// [`Message`]: crate::types::Message +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +pub struct MessageEntityRef<'a> { + message: &'a str, + range: Range, + kind: &'a MessageEntityKind, +} + +impl MessageEntity { + #[must_use] + pub const fn new(kind: MessageEntityKind, offset: usize, length: usize) -> Self { + Self { kind, offset, length } + } + + /// Create a message entity representing a bold text. + #[must_use] + pub const fn bold(offset: usize, length: usize) -> Self { + Self { kind: MessageEntityKind::Bold, offset, length } + } + + /// Create a message entity representing an italic text. + #[must_use] + pub const fn italic(offset: usize, length: usize) -> Self { + Self { kind: MessageEntityKind::Italic, offset, length } + } + + /// Create a message entity representing an underline text. + #[must_use] + pub const fn underline(offset: usize, length: usize) -> Self { + Self { kind: MessageEntityKind::Underline, offset, length } + } + + /// Create a message entity representing a strikethrough text. + #[must_use] + pub const fn strikethrough(offset: usize, length: usize) -> Self { + Self { kind: MessageEntityKind::Strikethrough, offset, length } + } + + /// Create a message entity representing a spoiler text. + #[must_use] + pub const fn spoiler(offset: usize, length: usize) -> Self { + Self { kind: MessageEntityKind::Spoiler, offset, length } + } + + /// Create a message entity representing a monowidth text. + #[must_use] + pub const fn code(offset: usize, length: usize) -> Self { + Self { kind: MessageEntityKind::Code, offset, length } + } + + /// Create a message entity representing a monowidth block. + #[must_use] + pub const fn pre(language: Option, offset: usize, length: usize) -> Self { + Self { kind: MessageEntityKind::Pre { language }, offset, length } + } + + /// Create a message entity representing a clickable text URL. + #[must_use] + pub const fn text_link(url: reqwest::Url, offset: usize, length: usize) -> Self { + Self { kind: MessageEntityKind::TextLink { url }, offset, length } + } + + /// Create a message entity representing a text mention. + /// + /// # Note + /// + /// If you don't have a complete [`User`] value, please use + /// [`MessageEntity::text_mention_id`] instead. + #[must_use] + pub const fn text_mention(user: User, offset: usize, length: usize) -> Self { + Self { kind: MessageEntityKind::TextMention { user }, offset, length } + } + + /// Create a message entity representing a text link in the form of + /// `tg://user/?id=...` that mentions user with `user_id`. + #[must_use] + pub fn text_mention_id(user_id: UserId, offset: usize, length: usize) -> Self { + Self { kind: MessageEntityKind::TextLink { url: user_id.url() }, offset, length } + } + + /// Create a message entity representing a custom emoji. + #[must_use] + pub const fn custom_emoji(custom_emoji_id: String, offset: usize, length: usize) -> Self { + Self { kind: MessageEntityKind::CustomEmoji { custom_emoji_id }, offset, length } + } + + #[must_use] + pub fn kind(mut self, val: MessageEntityKind) -> Self { + self.kind = val; + self + } + + #[must_use] + pub const fn offset(mut self, val: usize) -> Self { + self.offset = val; + self + } + + #[must_use] + pub const fn length(mut self, val: usize) -> Self { + self.length = val; + self + } +} + +impl<'a> MessageEntityRef<'a> { + /// Returns kind of this entity. + #[must_use] + pub fn kind(&self) -> &'a MessageEntityKind { + self.kind + } + + /// Returns the text that this entity is related to. + #[must_use] + pub fn text(&self) -> &'a str { + &self.message[self.range.clone()] + } + + /// Returns range that this entity is related to. + /// + /// The range is in bytes for UTF-8 encoding i.e. you can use it with common + /// Rust strings. + #[must_use] + pub fn range(&self) -> Range { + self.range.clone() + } + + /// Returns the offset (in bytes, for UTF-8) to the start of this entity in + /// the original message. + #[must_use] + pub fn start(&self) -> usize { + self.range.start + } + + /// Returns the offset (in bytes, for UTF-8) to the end of this entity in + /// the original message. + #[must_use] + pub fn end(&self) -> usize { + self.range.end + } + + /// Returns the length of this entity in bytes for UTF-8 encoding. + #[allow(clippy::len_without_is_empty)] + #[must_use] + pub fn len(&self) -> usize { + self.range.len() + } + + /// Returns the full text of the original message. + #[must_use] + pub fn message_text(&self) -> &'a str { + self.message + } + + /// Parses telegram [`MessageEntity`]s converting offsets to UTF-8. + #[must_use] + pub fn parse(text: &'a str, entities: &'a [MessageEntity]) -> Vec { + // This creates entities with **wrong** offsets (UTF-16) that we later patch. + let mut entities: Vec<_> = entities + .iter() + .map(|e| Self { message: text, range: e.offset..e.offset + e.length, kind: &e.kind }) + .collect(); + + // Convert offsets + + // References to all offsets that need patching + let mut offsets: Vec<&mut usize> = entities + .iter_mut() + .flat_map(|Self { range: Range { start, end }, .. }| [start, end]) + .collect(); + + // Sort in decreasing order, so the smallest elements are at the end and can be + // removed more easily + offsets.sort_unstable_by_key(|&&mut offset| cmp::Reverse(offset)); + + let _ = text + .chars() + .chain(['\0']) // this is needed to process offset pointing at the end of the string + .try_fold((0, 0), |(len_utf8, len_utf16), c| { + // Stop if there are no more offsets to patch + if offsets.is_empty() { + return None; + } + + // Patch all offsets that can be patched + while offsets.last().map(|&&mut offset| offset <= len_utf16).unwrap_or(false) { + let offset = offsets.pop().unwrap(); + assert_eq!(*offset, len_utf16, "Invalid utf-16 offset"); + + // Patch the offset to be UTF-8 + *offset = len_utf8; + } + + // Update "running" length + Some((len_utf8 + c.len_utf8(), len_utf16 + c.len_utf16())) + }); + + entities + } +} + +#[serde_with_macros::skip_serializing_none] +#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +#[serde(tag = "type")] +pub enum MessageEntityKind { + Mention, + Hashtag, + Cashtag, + BotCommand, + Url, + Email, + PhoneNumber, + Bold, + Italic, + Underline, + Strikethrough, + Spoiler, + Code, + Pre { language: Option }, + TextLink { url: reqwest::Url }, + TextMention { user: User }, + CustomEmoji { custom_emoji_id: String }, // FIXME(waffle): newtype this +} + +#[cfg(test)] +mod tests { + use super::*; + use cool_asserts::assert_matches; + use MessageEntity; + use MessageEntityKind::*; + + #[test] + fn recursive_kind() { + use serde_json::from_str; + + assert_eq!( + MessageEntity { + kind: MessageEntityKind::TextLink { + url: reqwest::Url::parse("https://example.com").unwrap(), + }, + offset: 1, + length: 2, + }, + from_str::( + r#"{"type":"text_link","url":"https://example.com","offset":1,"length":2}"# + ) + .unwrap() + ); + } + + #[test] + fn pre() { + use serde_json::from_str; + + assert_eq!( + MessageEntity { + kind: MessageEntityKind::Pre { language: Some("rust".to_string()) }, + offset: 1, + length: 2, + }, + from_str::(r#"{"type":"pre","offset":1,"length":2,"language":"rust"}"#) + .unwrap() + ); + } + + // https://github.com/teloxide/teloxide-core/pull/145 + #[test] + fn pre_with_none_language() { + use serde_json::to_string; + + assert_eq!( + to_string(&MessageEntity { + kind: MessageEntityKind::Pre { language: None }, + offset: 1, + length: 2, + }) + .unwrap() + .find("language"), + None + ); + } + + #[test] + fn parse_быба() { + let parsed = MessageEntityRef::parse( + "быба", + &[ + MessageEntity { kind: Strikethrough, offset: 0, length: 1 }, + MessageEntity { kind: Bold, offset: 1, length: 1 }, + MessageEntity { kind: Italic, offset: 2, length: 1 }, + MessageEntity { kind: Code, offset: 3, length: 1 }, + ], + ); + + assert_matches!( + parsed, + [ + entity if entity.text() == "б" && entity.kind() == &Strikethrough, + entity if entity.text() == "ы" && entity.kind() == &Bold, + entity if entity.text() == "б" && entity.kind() == &Italic, + entity if entity.text() == "а" && entity.kind() == &Code, + + ] + ); + } + + #[test] + fn parse_symbol_24bit() { + let parsed = MessageEntityRef::parse( + "xx আ #tt", + &[MessageEntity { kind: Hashtag, offset: 5, length: 3 }], + ); + + assert_matches!( + parsed, + [entity if entity.text() == "#tt" && entity.kind() == &Hashtag] + ); + } + + #[test] + fn parse_enclosed() { + let parsed = MessageEntityRef::parse( + "b i b", + // For some reason this is how telegram encodes b i b + &[ + MessageEntity { kind: Bold, offset: 0, length: 2 }, + MessageEntity { kind: Bold, offset: 2, length: 3 }, + MessageEntity { kind: Italic, offset: 2, length: 1 }, + ], + ); + + assert_matches!( + parsed, + [ + entity if entity.text() == "b " && entity.kind() == &Bold, + entity if entity.text() == "i b" && entity.kind() == &Bold, + entity if entity.text() == "i" && entity.kind() == &Italic, + ] + ); + } + + #[test] + fn parse_nothing() { + let parsed = MessageEntityRef::parse("a", &[]); + assert_eq!(parsed, []); + } + + #[test] + fn parse_empty() { + // It should be impossible for this to be returned from telegram, but just to be + // sure + let parsed = MessageEntityRef::parse( + "", + &[ + MessageEntity { kind: Bold, offset: 0, length: 0 }, + MessageEntity { kind: Italic, offset: 0, length: 0 }, + ], + ); + + assert_matches!( + parsed, + [ + entity if entity.text() == "" && entity.kind() == &Bold, + entity if entity.text() == "" && entity.kind() == &Italic, + ] + ); + } +} diff --git a/crates/teloxide-core/src/types/message_id.rs b/crates/teloxide-core/src/types/message_id.rs new file mode 100644 index 00000000..c9c405ae --- /dev/null +++ b/crates/teloxide-core/src/types/message_id.rs @@ -0,0 +1,42 @@ +use serde::{Deserialize, Serialize}; + +/// A unique message identifier. +#[derive(Clone, Copy, Debug, derive_more::Display, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[serde(from = "MessageIdRaw", into = "MessageIdRaw")] +pub struct MessageId(pub i32); + +#[derive(Serialize, Deserialize)] +struct MessageIdRaw { + message_id: i32, +} + +impl From for MessageId { + fn from(MessageIdRaw { message_id }: MessageIdRaw) -> Self { + MessageId(message_id) + } +} + +impl From for MessageIdRaw { + fn from(MessageId(message_id): MessageId) -> Self { + MessageIdRaw { message_id } + } +} + +#[cfg(test)] +mod tests { + use crate::types::MessageId; + + #[test] + fn smoke_deser() { + let json = r#"{"message_id":123}"#; + let mid: MessageId = serde_json::from_str(json).unwrap(); + assert_eq!(mid, MessageId(123)); + } + + #[test] + fn smoke_ser() { + let mid: MessageId = MessageId(123); + let json = serde_json::to_string(&mid).unwrap(); + assert_eq!(json, r#"{"message_id":123}"#); + } +} diff --git a/crates/teloxide-core/src/types/non_telegram_types/country_code.rs b/crates/teloxide-core/src/types/non_telegram_types/country_code.rs new file mode 100644 index 00000000..459e7f35 --- /dev/null +++ b/crates/teloxide-core/src/types/non_telegram_types/country_code.rs @@ -0,0 +1,505 @@ +use serde::{Deserialize, Serialize}; + +/// ISO 3166-1 alpha-2 language code. +#[allow(clippy::upper_case_acronyms)] +#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)] +pub enum CountryCode { + /// Andorra + AD, + /// United Arab Emirates + AE, + /// Afghanistan + AF, + /// Antigua and Barbuda + AG, + /// Anguilla + AI, + /// Albania + AL, + /// Armenia + AM, + /// Angola + AO, + /// Antarctica + AQ, + /// Argentina + AR, + /// American Samoa + AS, + /// Austria + AT, + /// Australia + AU, + /// Aruba + AW, + /// Åland Islands + AX, + /// Azerbaijan + AZ, + /// Bosnia and Herzegovina + BA, + /// Barbados + BB, + /// Bangladesh + BD, + /// Belgium + BE, + /// Burkina Faso + BF, + /// Bulgaria + BG, + /// Bahrain + BH, + /// Burundi + BI, + /// Benin + BJ, + /// Saint Barthélemy + BL, + /// Bermuda + BM, + /// Brunei Darussalam + BN, + /// Bolivia (Plurinational State of) + BO, + /// Bonaire, Sint Eustatius and Saba + BQ, + /// Brazil + BR, + /// Bahamas + BS, + /// Bhutan + BT, + /// Bouvet Island + BV, + /// Botswana + BW, + /// Belarus + BY, + /// Belize + BZ, + /// Canada + CA, + /// Cocos (Keeling) Islands + CC, + /// Congo, Democratic Republic of the + CD, + /// Central African Republic + CF, + /// Congo + CG, + /// Switzerland + CH, + /// Côte d'Ivoire + CI, + /// Cook Islands + CK, + /// Chile + CL, + /// Cameroon + CM, + /// China + CN, + /// Colombia + CO, + /// Costa Rica + CR, + /// Cuba + CU, + /// Cabo Verde + CV, + /// Curaçao + CW, + /// Christmas Island + CX, + /// Cyprus + CY, + /// Czechia + CZ, + /// Germany + DE, + /// Djibouti + DJ, + /// Denmark + DK, + /// Dominica + DM, + /// Dominican Republic + DO, + /// Algeria + DZ, + /// Ecuador + EC, + /// Estonia + EE, + /// Egypt + EG, + /// Western Sahara + EH, + /// Eritrea + ER, + /// Spain + ES, + /// Ethiopia + ET, + /// Finland + FI, + /// Fiji + FJ, + /// Falkland Islands (Malvinas) + FK, + /// Micronesia (Federated States of) + FM, + /// Faroe Islands + FO, + /// France + FR, + /// Gabon + GA, + /// United Kingdom of Great Britain and Northern Ireland + GB, + /// Grenada + GD, + /// Georgia + GE, + /// French Guiana + GF, + /// Guernsey + GG, + /// Ghana + GH, + /// Gibraltar + GI, + /// Greenland + GL, + /// Gambia + GM, + /// Guinea + GN, + /// Guadeloupe + GP, + /// Equatorial Guinea + GQ, + /// Greece + GR, + /// South Georgia and the South Sandwich Islands + GS, + /// Guatemala + GT, + /// Guam + GU, + /// Guinea-Bissau + GW, + /// Guyana + GY, + /// Hong Kong + HK, + /// Heard Island and McDonald Islands + HM, + /// Honduras + HN, + /// Croatia + HR, + /// Haiti + HT, + /// Hungary + HU, + /// Indonesia + ID, + /// Ireland + IE, + /// Israel + IL, + /// Isle of Man + IM, + /// India + IN, + /// British Indian Ocean Territory + IO, + /// Iraq + IQ, + /// Iran (Islamic Republic of) + IR, + /// Iceland + IS, + /// Italy + IT, + /// Jersey + JE, + /// Jamaica + JM, + /// Jordan + JO, + /// Japan + JP, + /// Kenya + KE, + /// Kyrgyzstan + KG, + /// Cambodia + KH, + /// Kiribati + KI, + /// Comoros + KM, + /// Saint Kitts and Nevis + KN, + /// Korea (Democratic People's Republic of) + KP, + /// Korea, Republic of + KR, + /// Kuwait + KW, + /// Cayman Islands + KY, + /// Kazakhstan + KZ, + /// Lao People's Democratic Republic + LA, + /// Lebanon + LB, + /// Saint Lucia + LC, + /// Liechtenstein + LI, + /// Sri Lanka + LK, + /// Liberia + LR, + /// Lesotho + LS, + /// Lithuania + LT, + /// Luxembourg + LU, + /// Latvia + LV, + /// Libya + LY, + /// Morocco + MA, + /// Monaco + MC, + /// Moldova, Republic of + MD, + /// Montenegro + ME, + /// Saint Martin (French part) + MF, + /// Madagascar + MG, + /// Marshall Islands + MH, + /// North Macedonia + MK, + /// Mali + ML, + /// Myanmar + MM, + /// Mongolia + MN, + /// Macao + MO, + /// Northern Mariana Islands + MP, + /// Martinique + MQ, + /// Mauritania + MR, + /// Montserrat + MS, + /// Malta + MT, + /// Mauritius + MU, + /// Maldives + MV, + /// Malawi + MW, + /// Mexico + MX, + /// Malaysia + MY, + /// Mozambique + MZ, + /// Namibia + NA, + /// New Caledonia + NC, + /// Niger + NE, + /// Norfolk Island + NF, + /// Nigeria + NG, + /// Nicaragua + NI, + /// Netherlands + NL, + /// Norway + NO, + /// Nepal + NP, + /// Nauru + NR, + /// Niue + NU, + /// New Zealand + NZ, + /// Oman + OM, + /// Panama + PA, + /// Peru + PE, + /// French Polynesia + PF, + /// Papua New Guinea + PG, + /// Philippines + PH, + /// Pakistan + PK, + /// Poland + PL, + /// Saint Pierre and Miquelon + PM, + /// Pitcairn + PN, + /// Puerto Rico + PR, + /// Palestine, State of + PS, + /// Portugal + PT, + /// Palau + PW, + /// Paraguay + PY, + /// Qatar + QA, + /// Réunion + RE, + /// Romania + RO, + /// Serbia + RS, + /// Russian Federation + RU, + /// Rwanda + RW, + /// Saudi Arabia + SA, + /// Solomon Islands + SB, + /// Seychelles + SC, + /// Sudan + SD, + /// Sweden + SE, + /// Singapore + SG, + /// Saint Helena, Ascension and Tristan da Cunha + SH, + /// Slovenia + SI, + /// Svalbard and Jan Mayen + SJ, + /// Slovakia + SK, + /// Sierra Leone + SL, + /// San Marino + SM, + /// Senegal + SN, + /// Somalia + SO, + /// Suriname + SR, + /// South Sudan + SS, + /// Sao Tome and Principe + ST, + /// El Salvador + SV, + /// Sint Maarten (Dutch part) + SX, + /// Syrian Arab Republic + SY, + /// Eswatini + SZ, + /// Turks and Caicos Islands + TC, + /// Chad + TD, + /// French Southern Territories + TF, + /// Togo + TG, + /// Thailand + TH, + /// Tajikistan + TJ, + /// Tokelau + TK, + /// Timor-Leste + TL, + /// Turkmenistan + TM, + /// Tunisia + TN, + /// Tonga + TO, + /// Turkey + TR, + /// Trinidad and Tobago + TT, + /// Tuvalu + TV, + /// Taiwan, Province of China + TW, + /// Tanzania, United Republic of + TZ, + /// Ukraine + UA, + /// Uganda + UG, + /// United States Minor Outlying Islands + UM, + /// United States of America + US, + /// Uruguay + UY, + /// Uzbekistan + UZ, + /// Holy See + VA, + /// Saint Vincent and the Grenadines + VC, + /// Venezuela (Bolivarian Republic of) + VE, + /// Virgin Islands (British) + VG, + /// Virgin Islands (U.S.) + VI, + /// Viet Nam + VN, + /// Vanuatu + VU, + /// Wallis and Futuna + WF, + /// Samoa + WS, + /// Yemen + YE, + /// Mayotte + YT, + /// South Africa + ZA, + /// Zambia + ZM, + /// Zimbabwe + ZW, +} diff --git a/crates/teloxide-core/src/types/non_telegram_types/currency.rs b/crates/teloxide-core/src/types/non_telegram_types/currency.rs new file mode 100644 index 00000000..a252b48b --- /dev/null +++ b/crates/teloxide-core/src/types/non_telegram_types/currency.rs @@ -0,0 +1,365 @@ +use serde::{Deserialize, Serialize}; + +/// ISO 4217 currency. +#[allow(clippy::upper_case_acronyms)] +#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)] +pub enum Currency { + /// United Arab Emirates dirham + AED, + /// Afghan afghani + AFN, + /// Albanian lek + ALL, + /// Armenian dram + AMD, + /// Netherlands Antillean guilder + ANG, + /// Angolan kwanza + AOA, + /// Argentine peso + ARS, + /// Australian dollar + AUD, + /// Aruban florin + AWG, + /// Azerbaijani manat + AZN, + /// Bosnia and Herzegovina convertible mark + BAM, + /// Barbados dollar + BBD, + /// Bangladeshi taka + BDT, + /// Bulgarian lev + BGN, + /// Bahraini dinar + BHD, + /// Burundian franc + BIF, + /// Bermudian dollar + BMD, + /// Brunei dollar + BND, + /// Boliviano + BOB, + /// Bolivian Mvdol (funds code) + BOV, + /// Brazilian real + BRL, + /// Bahamian dollar + BSD, + /// Bhutanese ngultrum + BTN, + /// Botswana pula + BWP, + /// Belarusian ruble + BYN, + /// Belize dollar + BZD, + /// Canadian dollar + CAD, + /// Congolese franc + CDF, + /// WIR euro (complementary currency) + CHE, + /// Swiss franc + CHF, + /// WIR franc (complementary currency) + CHW, + /// Unidad de Fomento (funds code) + CLF, + /// Chilean peso + CLP, + /// Chinese yuan + CNY, + /// Colombian peso + COP, + /// Unidad de Valor Real (UVR) (funds code) + COU, + /// Costa Rican colon + CRC, + /// Cuban convertible peso + CUC, + /// Cuban peso + CUP, + /// Cape Verdean escudo + CVE, + /// Czech koruna + CZK, + /// Djiboutian franc + DJF, + /// Danish krone + DKK, + /// Dominican peso + DOP, + /// Algerian dinar + DZD, + /// Egyptian pound + EGP, + /// Eritrean nakfa + ERN, + /// Ethiopian birr + ETB, + /// Euro + EUR, + /// Fiji dollar + FJD, + /// Falkland Islands pound + FKP, + /// Pound sterling + GBP, + /// Georgian lari + GEL, + /// Ghanaian cedi + GHS, + /// Gibraltar pound + GIP, + /// Gambian dalasi + GMD, + /// Guinean franc + GNF, + /// Guatemalan quetzal + GTQ, + /// Guyanese dollar + GYD, + /// Hong Kong dollar + HKD, + /// Honduran lempira + HNL, + /// Croatian kuna + HRK, + /// Haitian gourde + HTG, + /// Hungarian forint + HUF, + /// Indonesian rupiah + IDR, + /// Israeli new shekel + ILS, + /// Indian rupee + INR, + /// Iraqi dinar + IQD, + /// Iranian rial + IRR, + /// Icelandic króna + ISK, + /// Jamaican dollar + JMD, + /// Jordanian dinar + JOD, + /// Japanese yen + JPY, + /// Kenyan shilling + KES, + /// Kyrgyzstani som + KGS, + /// Cambodian riel + KHR, + /// Comoro franc + KMF, + /// North Korean won + KPW, + /// South Korean won + KRW, + /// Kuwaiti dinar + KWD, + /// Cayman Islands dollar + KYD, + /// Kazakhstani tenge + KZT, + /// Lao kip + LAK, + /// Lebanese pound + LBP, + /// Sri Lankan rupee + LKR, + /// Liberian dollar + LRD, + /// Lesotho loti + LSL, + /// Libyan dinar + LYD, + /// Moroccan dirham + MAD, + /// Moldovan leu + MDL, + /// Malagasy ariary + MGA, + /// Macedonian denar + MKD, + /// Myanmar kyat + MMK, + /// Mongolian tögrög + MNT, + /// Macanese pataca + MOP, + /// Mauritanian ouguiya + MRU, + /// Mauritian rupee + MUR, + /// Maldivian rufiyaa + MVR, + /// Malawian kwacha + MWK, + /// Mexican peso + MXN, + /// Mexican Unidad de Inversion (UDI) (funds code) + MXV, + /// Malaysian ringgit + MYR, + /// Mozambican metical + MZN, + /// Namibian dollar + NAD, + /// Nigerian naira + NGN, + /// Nicaraguan córdoba + NIO, + /// Norwegian krone + NOK, + /// Nepalese rupee + NPR, + /// New Zealand dollar + NZD, + /// Omani rial + OMR, + /// Panamanian balboa + PAB, + /// Peruvian sol + PEN, + /// Papua New Guinean kina + PGK, + /// Philippine peso + PHP, + /// Pakistani rupee + PKR, + /// Polish złoty + PLN, + /// Paraguayan guaraní + PYG, + /// Qatari riyal + QAR, + /// Romanian leu + RON, + /// Serbian dinar + RSD, + /// Russian ruble + RUB, + /// Rwandan franc + RWF, + /// Saudi riyal + SAR, + /// Solomon Islands dollar + SBD, + /// Seychelles rupee + SCR, + /// Sudanese pound + SDG, + /// Swedish krona/kronor + SEK, + /// Singapore dollar + SGD, + /// Saint Helena pound + SHP, + /// Sierra Leonean leone + SLL, + /// Somali shilling + SOS, + /// Surinamese dollar + SRD, + /// South Sudanese pound + SSP, + /// São Tomé and Príncipe dobra + STN, + /// Salvadoran colón + SVC, + /// Syrian pound + SYP, + /// Swazi lilangeni + SZL, + /// Thai baht + THB, + /// Tajikistani somoni + TJS, + /// Turkmenistan manat + TMT, + /// Tunisian dinar + TND, + /// Tongan paʻanga + TOP, + /// Turkish lira + TRY, + /// Trinidad and Tobago dollar + TTD, + /// New Taiwan dollar + TWD, + /// Tanzanian shilling + TZS, + /// Ukrainian hryvnia + UAH, + /// Ugandan shilling + UGX, + /// United States dollar + USD, + /// United States dollar (next day) (funds code) + USN, + /// Uruguay Peso en Unidades Indexadas (URUIURUI) (funds code) + UYI, + /// Uruguayan peso + UYU, + /// Unidad previsional + UYW, + /// Uzbekistan som + UZS, + /// Venezuelan bolívar soberano + VES, + /// Vietnamese đồng + VND, + /// Vanuatu vatu + VUV, + /// Samoan tala + WST, + /// CFA franc BEAC + XAF, + /// Silver (one troy ounce) + XAG, + /// Gold (one troy ounce) + XAU, + /// European Composite Unit (EURCO) (bond market unit) + XBA, + /// European Monetary Unit (E.M.U.-6) (bond market unit) + XBB, + /// European Unit of Account 9 (E.U.A.-9) (bond market unit) + XBC, + /// European Unit of Account 17 (E.U.A.-17) (bond market unit) + XBD, + /// East Caribbean dollar + XCD, + /// Special drawing rights + XDR, + /// CFA franc BCEAO + XOF, + /// Palladium (one troy ounce) + XPD, + /// CFP franc (franc Pacifique) + XPF, + /// Platinum (one troy ounce) + XPT, + /// SUCRE + XSU, + /// Code reserved for testing + XTS, + /// ADB Unit of Account + XUA, + /// No currency + XXX, + /// Yemeni rial + YER, + /// South African rand + ZAR, + /// Zambian kwacha + ZMW, + /// Zimbabwean dollar + ZWL, +} diff --git a/crates/teloxide-core/src/types/non_telegram_types/mime.rs b/crates/teloxide-core/src/types/non_telegram_types/mime.rs new file mode 100644 index 00000000..34422e34 --- /dev/null +++ b/crates/teloxide-core/src/types/non_telegram_types/mime.rs @@ -0,0 +1,97 @@ +use std::fmt; + +use mime::Mime; +use serde::{de::Visitor, Deserialize, Deserializer, Serialize, Serializer}; + +pub(crate) mod deser { + use mime::Mime; + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + + use super::{MimeDe, MimeSer}; + + pub(crate) fn serialize( + this: &Mime, + serializer: S, + ) -> Result<::Ok, ::Error> + where + S: Serializer, + { + MimeSer(this).serialize(serializer) + } + + pub(crate) fn deserialize<'de, D>( + deserializer: D, + ) -> Result>::Error> + where + D: Deserializer<'de>, + { + MimeDe::deserialize(deserializer).map(|MimeDe(m)| m) + } +} + +pub(crate) mod opt_deser { + use mime::Mime; + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + + use super::{MimeDe, MimeSer}; + + pub(crate) fn serialize( + this: &Option, + serializer: S, + ) -> Result<::Ok, ::Error> + where + S: Serializer, + { + this.as_ref().map(MimeSer).serialize(serializer) + } + + pub(crate) fn deserialize<'de, D>( + deserializer: D, + ) -> Result, >::Error> + where + D: Deserializer<'de>, + { + Option::::deserialize(deserializer).map(|opt| opt.map(|MimeDe(m)| m)) + } +} + +struct MimeSer<'a>(&'a Mime); + +impl Serialize for MimeSer<'_> { + fn serialize(&self, serializer: S) -> Result<::Ok, ::Error> + where + S: Serializer, + { + serializer.serialize_str(self.0.as_ref()) + } +} + +struct MimeVisitor; +impl<'a> Visitor<'a> for MimeVisitor { + type Value = MimeDe; + + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + formatter.write_str("mime type") + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + match v.parse::() { + Ok(mime_type) => Ok(MimeDe(mime_type)), + Err(e) => Err(E::custom(e)), + } + } +} + +struct MimeDe(Mime); + +impl<'de> Deserialize<'de> for MimeDe { + fn deserialize(deserializer: D) -> Result>::Error> + where + D: Deserializer<'de>, + { + deserializer.deserialize_str(MimeVisitor) + } +} diff --git a/crates/teloxide-core/src/types/non_telegram_types/until_date.rs b/crates/teloxide-core/src/types/non_telegram_types/until_date.rs new file mode 100644 index 00000000..d0c09e4b --- /dev/null +++ b/crates/teloxide-core/src/types/non_telegram_types/until_date.rs @@ -0,0 +1,62 @@ +use chrono::{DateTime, Utc}; +use serde::{de::Visitor, Deserialize, Serialize}; + +use crate::types::serde_timestamp; + +/// A range of time, before some date (for example a time before a restrictions +/// will be lifted from a member of a chat). +#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)] +pub enum UntilDate { + /// The range is bound by a given date and time. + Date(DateTime), + /// There is no end date, the range is unbounded. + Forever, +} + +impl<'de> Deserialize<'de> for UntilDate { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct UntilDateVisitor; + + impl<'v> Visitor<'v> for UntilDateVisitor { + type Value = UntilDate; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("an integer representing a UNIX timestamp or a 0") + } + + fn visit_i64(self, v: i64) -> Result + where + E: serde::de::Error, + { + match v { + 0 => Ok(UntilDate::Forever), + timestamp => serde_timestamp(timestamp).map(UntilDate::Date), + } + } + + fn visit_u64(self, v: u64) -> Result + where + E: serde::de::Error, + { + self.visit_i64(v as _) + } + } + + deserializer.deserialize_i64(UntilDateVisitor) + } +} + +impl Serialize for UntilDate { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_i64(match self { + UntilDate::Date(dt) => dt.timestamp(), + UntilDate::Forever => 0, + }) + } +} diff --git a/crates/teloxide-core/src/types/order_info.rs b/crates/teloxide-core/src/types/order_info.rs new file mode 100644 index 00000000..a512b7ac --- /dev/null +++ b/crates/teloxide-core/src/types/order_info.rs @@ -0,0 +1,21 @@ +use serde::{Deserialize, Serialize}; + +use crate::types::ShippingAddress; + +/// This object represents information about an order. +/// +/// [The official docs](https://core.telegram.org/bots/api#orderinfo). +#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize, Default)] +pub struct OrderInfo { + /// User's name. + pub name: Option, + + /// User's phone number. + pub phone_number: Option, + + /// User's email. + pub email: Option, + + /// User's shipping address. + pub shipping_address: Option, +} diff --git a/crates/teloxide-core/src/types/parse_mode.rs b/crates/teloxide-core/src/types/parse_mode.rs new file mode 100644 index 00000000..edd652fe --- /dev/null +++ b/crates/teloxide-core/src/types/parse_mode.rs @@ -0,0 +1,204 @@ +// see https://github.com/rust-lang/rust/issues/38832 +// (for built ins there no warnings, but for (De)Serialize, there are) +#![allow(deprecated)] + +use std::{ + convert::{TryFrom, TryInto}, + str::FromStr, +}; + +use serde::{Deserialize, Serialize}; + +/// Formatting options. +/// +/// The Bot API supports basic formatting for messages. You can use bold, +/// italic, underlined, strikethrough, and spoiler text, as well as inline links +/// and pre-formatted code in your bots' messages. Telegram clients will render +/// them accordingly. You can use either markdown-style or HTML-style +/// formatting. +/// +/// Note that Telegram clients will display an **alert** to the user before +/// opening an inline link ('Open this link?' together with the full URL). +/// +/// Message entities can be nested, providing following restrictions are met: +/// - If two entities have common characters then one of them is fully contained +/// inside another. +/// - bold, italic, underline, strikethrough, and spoiler entities can contain +/// and can be part of any other entities, except pre and code. +/// - All other entities can't contain each other. +/// +/// Links `tg://user?id=` can be used to mention a user by their ID +/// without using a username. Please note: +/// +/// - These links will work **only** if they are used inside an inline link. For +/// example, they will not work, when used in an inline keyboard button or in +/// a message text. +/// - These mentions are only guaranteed to work if the user has contacted the +/// bot in the past, has sent a callback query to the bot via inline button or +/// is a member in the group where he was mentioned. +/// +/// ## MarkdownV2 style +/// +/// To use this mode, pass [`MarkdownV2`] in the `parse_mode` field. +/// Use the following syntax in your message: +/// ````text +/// *bold \*text* +/// _italic \*text_ +/// __underline__ +/// ~strikethrough~ +/// ||spoiler|| +/// *bold _italic bold ~italic bold strikethrough ||italic bold strikethrough spoiler||~ __underline italic bold___ bold* +/// [inline URL](http://www.example.com/) +/// [inline mention of a user](tg://user?id=123456789) +/// `inline fixed-width code` +/// ``` +/// pre-formatted fixed-width code block +/// ``` +/// ```rust +#[doc = "pre-formatted fixed-width code block written in the Rust programming language"] +/// ``` +/// ```` +/// +/// Please note: +/// - Any character between 1 and 126 inclusively can be escaped anywhere with a +/// preceding '\' character, in which case it is treated as an ordinary +/// character and not a part of the markup. +/// - Inside `pre` and `code` entities, all '`‘ and ’\‘ characters must be +/// escaped with a preceding ’\' character. +/// - Inside `(...)` part of inline link definition, all ')‘ and ’\‘ must be +/// escaped with a preceding ’\' character. +/// - In all other places characters ’_‘, ’*‘, ’[‘, ’]‘, ’(‘, ’)‘, ’~‘, ’`‘, +/// ’>‘, ’#‘, ’+‘, ’+‘, ’-‘, ’|‘, ’{‘, ’}‘, ’.‘, ’!‘ must be escaped with the +/// preceding character ’\'. +/// - In case of ambiguity between `italic` and `underline` entities ‘__’ is +/// always greadily treated from left to right as beginning or end of +/// `underline` entity, so instead of `___italic underline___` use `___italic +/// underline_\r__`, where `\r` is a character with code `13`, which will be +/// ignored. +/// +/// ## HTML style +/// +/// To use this mode, pass [`Html`] in the `parse_mode` field. +/// The following tags are currently supported: +/// ````text +/// bold, bold +/// italic, italic +/// underline, underline +/// strikethrough, strikethrough, strikethrough +/// spoiler, spoiler +/// bold italic bold italic bold strikethrough italic bold strikethrough spoiler underline italic bold bold +/// inline URL +/// inline mention of a user +/// inline fixed-width code +///
pre-formatted fixed-width code block
+#[doc = "
pre-formatted fixed-width code block written in the \
+         Rust programming language
"] +/// ```` +/// +/// Please note: +/// +/// - Only the tags mentioned above are currently supported. +/// - All `<`, `>` and `&` symbols that are not a part of a tag or an HTML +/// entity must be replaced with the corresponding HTML entities (`<` with +/// `<`, `>` with `>` and `&` with `&`). +/// - All numerical HTML entities are supported. +/// - The API currently supports only the following named HTML entities: `<`, +/// `>`, `&` and `"`. +/// - Use nested `pre` and `code` tags, to define programming language for `pre` +/// entity. +/// - Programming language can't be specified for standalone `code` tags. +/// +/// ## Markdown style +/// +/// This is a legacy mode, retained for backward compatibility. To use this +/// mode, pass [`Markdown`] in the `parse_mode` field. +/// Use the following syntax in your message: +/// ````text +/// *bold text* +/// _italic text_ +/// [inline URL](http://www.example.com/) +/// [inline mention of a user](tg://user?id=123456789) +/// `inline fixed-width code` +/// ``` +/// pre-formatted fixed-width code block +/// ``` +/// ```rust +/// pre-formatted fixed-width code block written in the Rust programming language +/// ``` +/// ```` +/// +/// Please note: +/// - Entities must not be nested, use parse mode [`MarkdownV2`] instead. +/// - There is no way to specify underline and strikethrough entities, use parse +/// mode [`MarkdownV2`] instead. +/// - To escape characters ’_‘, ’*‘, ’`‘, ’[‘ outside of an entity, prepend the +/// characters ’\' before them. +/// - Escaping inside entities is not allowed, so entity must be closed first +/// and reopened again: use `_snake_\__case_` for italic `snake_case` and +/// `*2*\**2=4*` for bold `2*2=4`. +/// +/// [`MarkdownV2`]: ParseMode::MarkdownV2 +/// [`Html`]: ParseMode::Html +/// [`Markdown`]: ParseMode::Markdown +#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)] +pub enum ParseMode { + MarkdownV2, + #[serde(rename = "HTML")] + Html, + #[deprecated = "This is a legacy mode, retained for backward compatibility. Use `MarkdownV2` \ + instead."] + Markdown, +} + +impl TryFrom<&str> for ParseMode { + type Error = (); + + fn try_from(value: &str) -> Result { + let normalized = value.to_lowercase(); + match normalized.as_ref() { + "html" => Ok(ParseMode::Html), + "markdown" => Ok(ParseMode::Markdown), + "markdownv2" => Ok(ParseMode::MarkdownV2), + _ => Err(()), + } + } +} + +impl TryFrom for ParseMode { + type Error = (); + + fn try_from(value: String) -> Result { + value.as_str().try_into() + } +} + +impl FromStr for ParseMode { + type Err = (); + + fn from_str(s: &str) -> Result { + s.try_into() + } +} + +#[cfg(test)] +mod tests { + #![allow(deprecated)] + + use super::*; + + #[test] + fn html_serialization() { + let expected_json = String::from(r#""HTML""#); + let actual_json = serde_json::to_string(&ParseMode::Html).unwrap(); + + assert_eq!(expected_json, actual_json) + } + + #[test] + fn markdown_serialization() { + let expected_json = String::from(r#""Markdown""#); + let actual_json = serde_json::to_string(&ParseMode::Markdown).unwrap(); + + assert_eq!(expected_json, actual_json) + } +} diff --git a/crates/teloxide-core/src/types/passport_data.rs b/crates/teloxide-core/src/types/passport_data.rs new file mode 100644 index 00000000..5e548f57 --- /dev/null +++ b/crates/teloxide-core/src/types/passport_data.rs @@ -0,0 +1,17 @@ +use serde::{Deserialize, Serialize}; + +use super::{EncryptedCredentials, EncryptedPassportElement}; + +/// Contains information about Telegram Passport data shared with the bot by the +/// user. +/// +/// [The official docs](https://core.telegram.org/bots/api#passportdata). +#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)] +pub struct PassportData { + /// Array with information about documents and other Telegram Passport + /// elements that was shared with the bot. + pub data: Vec, + + /// Encrypted credentials required to decrypt the data. + pub credentials: EncryptedCredentials, +} diff --git a/crates/teloxide-core/src/types/passport_element_error.rs b/crates/teloxide-core/src/types/passport_element_error.rs new file mode 100644 index 00000000..6769bcf2 --- /dev/null +++ b/crates/teloxide-core/src/types/passport_element_error.rs @@ -0,0 +1,548 @@ +use serde::{Deserialize, Serialize}; + +/// This object represents an error in the Telegram Passport element which was +/// submitted that should be resolved by the user. +/// +/// [The official docs](https://core.telegram.org/bots/api#passportelementerror). +#[derive(Clone, Debug, Hash, Eq, PartialEq, Serialize, Deserialize)] +pub struct PassportElementError { + /// Error message. + pub message: String, + + #[serde(flatten)] + pub kind: PassportElementErrorKind, +} + +impl PassportElementError { + pub fn new(message: S, kind: PassportElementErrorKind) -> Self + where + S: Into, + { + Self { message: message.into(), kind } + } + + pub fn message(mut self, val: S) -> Self + where + S: Into, + { + self.message = val.into(); + self + } + + #[must_use] + pub fn kind(mut self, val: PassportElementErrorKind) -> Self { + self.kind = val; + self + } +} + +#[derive(Clone, Debug, Hash, Eq, PartialEq, Serialize, Deserialize)] +#[serde(tag = "source")] +pub enum PassportElementErrorKind { + #[serde(rename = "data")] + DataField(PassportElementErrorDataField), + + #[serde(rename = "snake_case")] + FrontSide(PassportElementErrorFrontSide), + + #[serde(rename = "snake_case")] + ReverseSide(PassportElementErrorReverseSide), + + #[serde(rename = "snake_case")] + Selfie(PassportElementErrorSelfie), + + #[serde(rename = "snake_case")] + File(PassportElementErrorFile), + + #[serde(rename = "snake_case")] + Files(PassportElementErrorFiles), + + #[serde(rename = "snake_case")] + TranslationFile(PassportElementErrorTranslationFile), + + #[serde(rename = "snake_case")] + TranslationFiles(PassportElementErrorTranslationFiles), + + #[serde(rename = "snake_case")] + Unspecified(PassportElementErrorUnspecified), +} + +/// Represents an issue in one of the data fields that was provided by the +/// user. +/// +/// The error is considered resolved when the field's value changes. +/// +/// [The official docs](https://core.telegram.org/bots/api#passportelementerrordatafield). +#[derive(Clone, Debug, Hash, Eq, PartialEq, Serialize, Deserialize)] +pub struct PassportElementErrorDataField { + /// The section of the user's Telegram Passport which has the error. + pub r#type: PassportElementErrorDataFieldType, + + /// Name of the data field which has the error. + pub field_name: String, + + /// Base64-encoded data hash. + pub data_hash: String, +} + +impl PassportElementErrorDataField { + pub fn new( + r#type: PassportElementErrorDataFieldType, + field_name: S1, + data_hash: S2, + ) -> Self + where + S1: Into, + S2: Into, + { + Self { r#type, field_name: field_name.into(), data_hash: data_hash.into() } + } + + #[must_use] + pub fn r#type(mut self, val: PassportElementErrorDataFieldType) -> Self { + self.r#type = val; + self + } + + pub fn field_name(mut self, val: S) -> Self + where + S: Into, + { + self.field_name = val.into(); + self + } + + pub fn data_hash(mut self, val: S) -> Self + where + S: Into, + { + self.data_hash = val.into(); + self + } +} + +/// Represents an issue with the front side of a document. +/// +/// The error is considered resolved when the file with the front side of the +/// document changes. +/// +/// [The official docs](https://core.telegram.org/bots/api#passportelementerrorfrontside). +#[derive(Clone, Debug, Hash, Eq, PartialEq, Serialize, Deserialize)] +pub struct PassportElementErrorFrontSide { + /// The section of the user's Telegram Passport which has the issue. + pub r#type: PassportElementErrorFrontSideType, + + /// Base64-encoded hash of the file with the front side of the + /// document. + pub file_hash: String, +} + +impl PassportElementErrorFrontSide { + pub fn new(r#type: PassportElementErrorFrontSideType, file_hash: S) -> Self + where + S: Into, + { + Self { r#type, file_hash: file_hash.into() } + } + + #[must_use] + pub fn r#type(mut self, val: PassportElementErrorFrontSideType) -> Self { + self.r#type = val; + self + } + + pub fn file_hash(mut self, val: S) -> Self + where + S: Into, + { + self.file_hash = val.into(); + self + } +} + +/// Represents an issue with the reverse side of a document. +/// +/// The error is considered resolved when the file with reverse side of the +/// document changes. +/// +/// [The official docs](https://core.telegram.org/bots/api#passportelementerrorreverseside). +#[derive(Clone, Debug, Hash, Eq, PartialEq, Serialize, Deserialize)] +pub struct PassportElementErrorReverseSide { + /// The section of the user's Telegram Passport which has the issue. + pub r#type: PassportElementErrorReverseSideType, + + //// Base64-encoded hash of the file with the reverse side of the + //// document. + pub file_hash: String, +} + +impl PassportElementErrorReverseSide { + pub fn new(r#type: PassportElementErrorReverseSideType, file_hash: S) -> Self + where + S: Into, + { + Self { r#type, file_hash: file_hash.into() } + } + + #[must_use] + pub fn r#type(mut self, val: PassportElementErrorReverseSideType) -> Self { + self.r#type = val; + self + } + + pub fn file_hash(mut self, val: S) -> Self + where + S: Into, + { + self.file_hash = val.into(); + self + } +} + +//// Represents an issue with the selfie with a document. +// +/// The error is considered resolved when the file with the selfie changes. +/// +/// [The official docs](https://core.telegram.org/bots/api#passportelementerrorselfie). +#[derive(Clone, Debug, Hash, Eq, PartialEq, Serialize, Deserialize)] +pub struct PassportElementErrorSelfie { + /// The section of the user's Telegram Passport which has the issue. + pub r#type: PassportElementErrorSelfieType, + + /// Base64-encoded hash of the file with the selfie. + pub file_hash: String, +} + +impl PassportElementErrorSelfie { + pub fn new(r#type: PassportElementErrorSelfieType, file_hash: S) -> Self + where + S: Into, + { + Self { r#type, file_hash: file_hash.into() } + } + + #[must_use] + pub fn r#type(mut self, val: PassportElementErrorSelfieType) -> Self { + self.r#type = val; + self + } + + pub fn file_hash(mut self, val: S) -> Self + where + S: Into, + { + self.file_hash = val.into(); + self + } +} + +/// Represents an issue with a document scan. +/// +/// The error is considered resolved when the file with the document scan +/// changes. +/// +/// [The official docs](https://core.telegram.org/bots/api#passportelementerrorfile). +#[derive(Clone, Debug, Hash, Eq, PartialEq, Serialize, Deserialize)] +pub struct PassportElementErrorFile { + /// The section of the user's Telegram Passport which has the issue. + pub r#type: PassportElementErrorFileType, + + /// Base64-encoded file hash. + pub file_hash: String, +} + +impl PassportElementErrorFile { + pub fn new(r#type: PassportElementErrorFileType, file_hash: S) -> Self + where + S: Into, + { + Self { r#type, file_hash: file_hash.into() } + } + + #[must_use] + pub fn r#type(mut self, val: PassportElementErrorFileType) -> Self { + self.r#type = val; + self + } + + pub fn file_hash(mut self, val: S) -> Self + where + S: Into, + { + self.file_hash = val.into(); + self + } +} + +/// Represents an issue with a list of scans. +/// +/// The error is considered resolved when the list of files containing the scans +/// changes. +/// +/// [The official docs](https://core.telegram.org/bots/api#passportelementerrorfiles). +#[derive(Clone, Debug, Hash, Eq, PartialEq, Serialize, Deserialize)] +pub struct PassportElementErrorFiles { + /// The section of the user's Telegram Passport which has the issue. + pub r#type: PassportElementErrorFilesType, + + /// List of base64-encoded file hashes. + pub file_hashes: Vec, +} + +impl PassportElementErrorFiles { + pub fn new(r#type: PassportElementErrorFilesType, file_hashes: S) -> Self + where + S: IntoIterator, + { + Self { r#type, file_hashes: file_hashes.into_iter().collect() } + } + + #[must_use] + pub fn r#type(mut self, val: PassportElementErrorFilesType) -> Self { + self.r#type = val; + self + } + + pub fn file_hashes(mut self, val: S) -> Self + where + S: IntoIterator, + { + self.file_hashes = val.into_iter().collect(); + self + } +} + +/// Represents an issue with one of the files that constitute the +/// translation of a document. +/// +/// The error is considered resolved when the file changes. +/// +/// [The official docs](https://core.telegram.org/bots/api#passportelementerrortranslationfile). +#[derive(Clone, Debug, Hash, Eq, PartialEq, Serialize, Deserialize)] +pub struct PassportElementErrorTranslationFile { + /// Type of element of the user's Telegram Passport which has the + /// issue. + pub r#type: PassportElementErrorTranslationFileType, + + /// Base64-encoded file hash. + pub file_hash: String, +} + +impl PassportElementErrorTranslationFile { + pub fn new(r#type: PassportElementErrorTranslationFileType, file_hash: S) -> Self + where + S: Into, + { + Self { r#type, file_hash: file_hash.into() } + } + + #[must_use] + pub fn r#type(mut self, val: PassportElementErrorTranslationFileType) -> Self { + self.r#type = val; + self + } + + pub fn file_hash(mut self, val: S) -> Self + where + S: Into, + { + self.file_hash = val.into(); + self + } +} + +/// Represents an issue with the translated version of a document. +/// +/// The error is considered resolved when a file with the document translation +/// change. +/// +/// [The official docs](https://core.telegram.org/bots/api#passportelementerrortranslationfiles). +#[derive(Clone, Debug, Hash, Eq, PartialEq, Serialize, Deserialize)] +pub struct PassportElementErrorTranslationFiles { + /// Type of element of the user's Telegram Passport which has the issue + pub r#type: PassportElementErrorTranslationFilesType, + + /// List of base64-encoded file hashes + pub file_hashes: Vec, +} + +impl PassportElementErrorTranslationFiles { + pub fn new(r#type: PassportElementErrorTranslationFilesType, file_hashes: S) -> Self + where + S: IntoIterator, + { + Self { r#type, file_hashes: file_hashes.into_iter().collect() } + } + + #[must_use] + pub fn r#type(mut self, val: PassportElementErrorTranslationFilesType) -> Self { + self.r#type = val; + self + } + + pub fn file_hashes(mut self, val: S) -> Self + where + S: IntoIterator, + { + self.file_hashes = val.into_iter().collect(); + self + } +} + +/// Represents an issue in an unspecified place. +/// +/// The error is considered resolved when new data is added. +/// +/// [The official docs](https://core.telegram.org/bots/api#passportelementerrorunspecified). +#[derive(Clone, Debug, Hash, Eq, PartialEq, Serialize, Deserialize)] +pub struct PassportElementErrorUnspecified { + /// Type of element of the user's Telegram Passport which has the + /// issue. + pub r#type: PassportElementErrorUnspecifiedType, + + /// Base64-encoded element hash. + pub element_hash: String, +} + +impl PassportElementErrorUnspecified { + pub fn new(r#type: PassportElementErrorUnspecifiedType, file_hash: S) -> Self + where + S: Into, + { + Self { r#type, element_hash: file_hash.into() } + } + + #[must_use] + pub fn r#type(mut self, val: PassportElementErrorUnspecifiedType) -> Self { + self.r#type = val; + self + } + + pub fn element_hash(mut self, val: S) -> Self + where + S: Into, + { + self.element_hash = val.into(); + self + } +} + +#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum PassportElementErrorDataFieldType { + PersonalDetails, + Passport, + DriverLicense, + IdentityCard, + InternalPassport, + Address, +} + +#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum PassportElementErrorFrontSideType { + Passport, + DriverLicense, + IdentityCard, + InternalPassport, +} + +#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum PassportElementErrorReverseSideType { + DriverLicense, + IdentityCard, +} + +#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum PassportElementErrorSelfieType { + Passport, + DriverLicense, + IdentityCard, + InternalPassport, +} + +#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum PassportElementErrorFileType { + UtilityBill, + BankStatement, + RentalAgreement, + PassportRegistration, + TemporaryRegistration, +} + +#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum PassportElementErrorFilesType { + UtilityBill, + BankStatement, + RentalAgreement, + PassportRegistration, + TemporaryRegistration, +} + +#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum PassportElementErrorTranslationFileType { + Passport, + DriverLicense, + IdentityCard, + InternalPassport, + UtilityBill, + BankStatement, + RentalAgreement, + PassportRegistration, + TemporaryRegistration, +} + +#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum PassportElementErrorTranslationFilesType { + Passport, + DriverLicense, + IdentityCard, + InternalPassport, + UtilityBill, + BankStatement, + RentalAgreement, + PassportRegistration, + TemporaryRegistration, +} + +#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum PassportElementErrorUnspecifiedType { + DataField, + FrontSide, + ReverseSide, + Selfie, + File, + Files, + TranslationFile, + TranslationFiles, + Unspecified, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn serialize_data_field() { + let data = PassportElementError { + message: "This is an error message!".to_owned(), + kind: PassportElementErrorKind::DataField(PassportElementErrorDataField { + r#type: PassportElementErrorDataFieldType::InternalPassport, + field_name: "The field name".to_owned(), + data_hash: "This is a data hash".to_owned(), + }), + }; + + assert_eq!( + serde_json::to_string(&data).unwrap(), + r#"{"message":"This is an error message!","source":"data","type":"internal_passport","field_name":"The field name","data_hash":"This is a data hash"}"# + ); + } +} diff --git a/crates/teloxide-core/src/types/passport_file.rs b/crates/teloxide-core/src/types/passport_file.rs new file mode 100644 index 00000000..05b97225 --- /dev/null +++ b/crates/teloxide-core/src/types/passport_file.rs @@ -0,0 +1,24 @@ +use chrono::{DateTime, Utc}; +use derive_more::Deref; +use serde::{Deserialize, Serialize}; + +use crate::types::FileMeta; + +/// This object represents a file uploaded to Telegram Passport. +/// +/// Currently all Telegram Passport files are in JPEG format when decrypted and +/// don't exceed 10MB. +/// +/// [The official docs](https://core.telegram.org/bots/api#passportfile). +#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize, Deref)] +pub struct PassportFile { + /// Metadata of the passport file. + #[deref] + #[serde(flatten)] + pub file: FileMeta, + + /// Time when the file was uploaded. + #[serde(with = "crate::types::serde_date_from_unix_timestamp")] + #[serde(rename = "file_date")] + pub date: DateTime, +} diff --git a/crates/teloxide-core/src/types/photo_size.rs b/crates/teloxide-core/src/types/photo_size.rs new file mode 100644 index 00000000..f75955a2 --- /dev/null +++ b/crates/teloxide-core/src/types/photo_size.rs @@ -0,0 +1,39 @@ +use serde::{Deserialize, Serialize}; + +use crate::types::FileMeta; + +/// This object represents one size of a photo or a [file]/[sticker] thumbnail. +/// +/// [file]: crate::types::Document +/// [sticker]: crate::types::Sticker +#[serde_with_macros::skip_serializing_none] +#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)] +pub struct PhotoSize { + /// Metadata of the photo file. + #[serde(flatten)] + pub file: FileMeta, + + /// Photo width. + pub width: u32, + + /// Photo height. + pub height: u32, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn deserialize() { + let json = r#"{"file_id":"id","file_unique_id":"","width":320,"height":320, + "file_size":3452}"#; + let expected = PhotoSize { + file: FileMeta { id: "id".to_owned(), unique_id: "".to_owned(), size: 3452 }, + width: 320, + height: 320, + }; + let actual = serde_json::from_str::(json).unwrap(); + assert_eq!(actual, expected); + } +} diff --git a/crates/teloxide-core/src/types/poll.rs b/crates/teloxide-core/src/types/poll.rs new file mode 100644 index 00000000..4a35a7d5 --- /dev/null +++ b/crates/teloxide-core/src/types/poll.rs @@ -0,0 +1,111 @@ +use crate::types::{MessageEntity, PollType}; + +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; + +/// This object contains information about a poll. +/// +/// [The official docs](https://core.telegram.org/bots/api#poll). +#[serde_with_macros::skip_serializing_none] +#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)] +pub struct Poll { + /// Unique poll identifier. + pub id: String, + + /// Poll question, 1-255 characters. + pub question: String, + + /// List of poll options. + pub options: Vec, + + /// `true`, if the poll is closed. + pub is_closed: bool, + + /// Total number of users that voted in the poll + pub total_voter_count: i32, + + /// True, if the poll is anonymous + pub is_anonymous: bool, + + /// Poll type, currently can be “regular” or “quiz” + #[serde(rename = "type")] + pub poll_type: PollType, + + /// True, if the poll allows multiple answers + pub allows_multiple_answers: bool, + + /// 0-based identifier of the correct answer option. Available only for + /// polls in the quiz mode, which are closed, or was sent (not + /// forwarded) by the bot or to the private chat with the bot. + pub correct_option_id: Option, + + /// Text that is shown when a user chooses an incorrect answer or taps on + /// the lamp icon in a quiz-style poll, 0-200 characters. + pub explanation: Option, + + /// Special entities like usernames, URLs, bot commands, etc. that appear in + /// the explanation. + pub explanation_entities: Option>, + + /// Amount of time in seconds the poll will be active after creation. + pub open_period: Option, + + /// Point in time when the poll will be automatically closed. + #[serde(default, with = "crate::types::serde_opt_date_from_unix_timestamp")] + pub close_date: Option>, +} + +/// This object contains information about one answer option in a poll. +/// +/// [The official docs](https://core.telegram.org/bots/api#polloption). +#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)] +pub struct PollOption { + /// Option text, 1-100 characters. + pub text: String, + + /// Number of users that voted for this option. + pub voter_count: i32, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn deserialize() { + let data = r#" + { + "allows_multiple_answers": false, + "id": "5377643193141559299", + "is_anonymous": true, + "is_closed": false, + "options": [ + { + "text": "1", + "voter_count": 1 + }, + { + "text": "2", + "voter_count": 0 + }, + { + "text": "3", + "voter_count": 0 + }, + { + "text": "4", + "voter_count": 0 + }, + { + "text": "5", + "voter_count": 0 + } + ], + "question": "Rate me from 1 to 5.", + "total_voter_count": 1, + "type": "regular" + } + "#; + serde_json::from_str::(data).unwrap(); + } +} diff --git a/crates/teloxide-core/src/types/poll_answer.rs b/crates/teloxide-core/src/types/poll_answer.rs new file mode 100644 index 00000000..b8f26968 --- /dev/null +++ b/crates/teloxide-core/src/types/poll_answer.rs @@ -0,0 +1,16 @@ +use crate::types::User; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)] +pub struct PollAnswer { + /// Unique poll identifier. + pub poll_id: String, + + /// The user, who changed the answer to the poll. + pub user: User, + + /// 0-based identifiers of answer options, chosen by the user. + /// + /// May be empty if the user retracted their vote. + pub option_ids: Vec, +} diff --git a/crates/teloxide-core/src/types/poll_type.rs b/crates/teloxide-core/src/types/poll_type.rs new file mode 100644 index 00000000..61b8acd9 --- /dev/null +++ b/crates/teloxide-core/src/types/poll_type.rs @@ -0,0 +1,8 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum PollType { + Quiz, + Regular, +} diff --git a/crates/teloxide-core/src/types/pre_checkout_query.rs b/crates/teloxide-core/src/types/pre_checkout_query.rs new file mode 100644 index 00000000..75fb909c --- /dev/null +++ b/crates/teloxide-core/src/types/pre_checkout_query.rs @@ -0,0 +1,40 @@ +use serde::{Deserialize, Serialize}; + +use crate::types::{Currency, OrderInfo, User}; + +/// This object contains information about an incoming pre-checkout query. +/// +/// [The official docs](https://core.telegram.org/bots/api#precheckoutquery). +#[serde_with_macros::skip_serializing_none] +#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)] +pub struct PreCheckoutQuery { + /// Unique query identifier. + pub id: String, + + /// User who sent the query. + pub from: User, + + /// Three-letter ISO 4217 [currency] code. + /// + /// [currency]: https://core.telegram.org/bots/payments#supported-currencies + pub currency: Currency, + + /// Total price in the _smallest units_ of the currency (integer, **not** + /// float/double). For example, for a price of `US$ 1.45` pass `amount = + /// 145`. See the exp parameter in [`currencies.json`], it shows the number + /// of digits past the decimal point for each currency (2 for the + /// majority of currencies). + /// + /// [`currencies.json`]: https://core.telegram.org/bots/payments/currencies.json + pub total_amount: i32, + + /// Bot specified invoice payload. + pub invoice_payload: String, + + /// Identifier of the shipping option chosen by the user. + pub shipping_option_id: Option, + + /// Order info provided by the user. + #[serde(default)] + pub order_info: OrderInfo, +} diff --git a/crates/teloxide-core/src/types/proximity_alert_triggered.rs b/crates/teloxide-core/src/types/proximity_alert_triggered.rs new file mode 100644 index 00000000..a0addc95 --- /dev/null +++ b/crates/teloxide-core/src/types/proximity_alert_triggered.rs @@ -0,0 +1,17 @@ +use serde::{Deserialize, Serialize}; + +use crate::types::User; + +/// This object represents the content of a service message, sent whenever a +/// user in the chat triggers a proximity alert set by another user. +#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)] +pub struct ProximityAlertTriggered { + /// User that triggered the alert. + pub traveler: User, + + /// User that set the alert. + pub watcher: User, + + /// The distance between the users. + pub distance: u32, +} diff --git a/crates/teloxide-core/src/types/recipient.rs b/crates/teloxide-core/src/types/recipient.rs new file mode 100644 index 00000000..469aac97 --- /dev/null +++ b/crates/teloxide-core/src/types/recipient.rs @@ -0,0 +1,48 @@ +use derive_more::{Display, From}; +use serde::{Deserialize, Serialize}; + +use crate::types::{ChatId, UserId}; + +/// A unique identifier for the target chat or username of the target channel +/// (in the format `@channelusername`). +#[derive(Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Display, From)] +#[derive(Serialize, Deserialize)] +#[serde(untagged)] +pub enum Recipient { + /// A chat identifier. + #[display(fmt = "{}", _0)] + Id(ChatId), + + /// A channel username (in the format @channelusername). + #[display(fmt = "{}", _0)] + ChannelUsername(String), +} + +impl From for Recipient { + fn from(id: UserId) -> Self { + Self::Id(id.into()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn chat_id_id_serialization() { + let expected_json = String::from(r#"123456"#); + let actual_json = serde_json::to_string(&Recipient::Id(ChatId(123_456))).unwrap(); + + assert_eq!(expected_json, actual_json) + } + + #[test] + fn chat_id_channel_username_serialization() { + let expected_json = String::from(r#""@username""#); + let actual_json = + serde_json::to_string(&Recipient::ChannelUsername(String::from("@username"))).unwrap(); + + assert_eq!(expected_json, actual_json) + } +} diff --git a/crates/teloxide-core/src/types/reply_keyboard_markup.rs b/crates/teloxide-core/src/types/reply_keyboard_markup.rs new file mode 100644 index 00000000..649d9ee2 --- /dev/null +++ b/crates/teloxide-core/src/types/reply_keyboard_markup.rs @@ -0,0 +1,114 @@ +use serde::{Deserialize, Serialize}; + +use crate::types::KeyboardButton; + +/// This object represents a [custom keyboard] with reply options (see +/// [Introduction to bots] for details and examples). +/// +/// [The official docs](https://core.telegram.org/bots/api#replykeyboardmarkup). +/// +/// [custom keyboard]: https://core.telegram.org/bots#keyboards +/// [Introduction to bots]: https://core.telegram.org/bots#keyboards +#[serde_with_macros::skip_serializing_none] +#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize, Default)] +pub struct KeyboardMarkup { + /// Array of button rows, each represented by an Array of + /// [`KeyboardButton`] objects + /// + /// [`KeyboardButton`]: crate::types::KeyboardButton + pub keyboard: Vec>, + + /// Requests clients to resize the keyboard vertically for optimal fit + /// (e.g., make the keyboard smaller if there are just two rows of + /// buttons). Defaults to `false`, in which case the custom keyboard is + /// always of the same height as the app's standard keyboard. + pub resize_keyboard: Option, + + /// Requests clients to hide the keyboard as soon as it's been used. The + /// keyboard will still be available, but clients will automatically + /// display the usual letter-keyboard in the chat – the user can press a + /// special button in the input field to see the custom keyboard again. + /// Defaults to `false`. + pub one_time_keyboard: Option, + + /// The placeholder to be shown in the input field when the keyboard is + /// active; 1-64 characters. + pub input_field_placeholder: Option, + + /// Use this parameter if you want to show the keyboard to specific users + /// only. Targets: 1) users that are `@mentioned` in the `text` of the + /// [`Message`] object; 2) if the bot's message is a reply (has + /// `reply_to_message_id`), sender of the original message. + /// + /// Example: A user requests to change the bot‘s language, bot replies to + /// the request with a keyboard to select the new language. Other users + /// in the group don’t see the keyboard. + /// + /// [`Message`]: crate::types::Message + pub selective: Option, +} + +impl KeyboardMarkup { + pub fn new(keyboard: K) -> Self + where + K: IntoIterator, + K::Item: IntoIterator, + { + Self { + keyboard: keyboard.into_iter().map(<_>::into_iter).map(<_>::collect).collect(), + resize_keyboard: None, + one_time_keyboard: None, + input_field_placeholder: None, + selective: None, + } + } + + pub fn append_row(mut self, buttons: R) -> Self + where + R: IntoIterator, + { + self.keyboard.push(buttons.into_iter().collect()); + self + } + + #[must_use] + pub fn append_to_row(mut self, index: usize, button: KeyboardButton) -> Self { + match self.keyboard.get_mut(index) { + Some(buttons) => buttons.push(button), + None => self.keyboard.push(vec![button]), + }; + self + } + + pub fn resize_keyboard(mut self, val: T) -> Self + where + T: Into>, + { + self.resize_keyboard = val.into(); + self + } + + pub fn one_time_keyboard(mut self, val: T) -> Self + where + T: Into>, + { + self.one_time_keyboard = val.into(); + self + } + + pub fn input_field_placeholder(mut self, val: T) -> Self + where + T: Into>, + { + self.input_field_placeholder = val.into(); + self + } + + pub fn selective(mut self, val: T) -> Self + where + T: Into>, + { + self.selective = val.into(); + self + } +} diff --git a/crates/teloxide-core/src/types/reply_keyboard_remove.rs b/crates/teloxide-core/src/types/reply_keyboard_remove.rs new file mode 100644 index 00000000..0f48434b --- /dev/null +++ b/crates/teloxide-core/src/types/reply_keyboard_remove.rs @@ -0,0 +1,51 @@ +use serde::{Deserialize, Serialize}; + +use crate::types::True; + +/// Upon receiving a message with this object, Telegram clients will remove the +/// current custom keyboard and display the default letter-keyboard. +/// +/// By default, custom keyboards are displayed until a new keyboard is sent by a +/// bot. An exception is made for one-time keyboards that are hidden immediately +/// after the user presses a button (see [`KeyboardMarkup`]). +/// +/// [The official docs](https://core.telegram.org/bots/api#replykeyboardremove). +/// +/// [`KeyboardMarkup`]: crate::types::KeyboardMarkup +#[serde_with_macros::skip_serializing_none] +#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize)] +#[derive(Eq, Hash, PartialEq)] +pub struct KeyboardRemove { + /// Requests clients to remove the custom keyboard (user will not be able + /// to summon this keyboard; if you want to hide the keyboard from sight + /// but keep it accessible, use one_time_keyboard in + /// [`KeyboardMarkup`]). + /// + /// [`KeyboardMarkup`]: crate::types::KeyboardMarkup + pub remove_keyboard: True, + + /// Use this parameter if you want to remove the keyboard for specific + /// users only. Targets: 1) users that are `@mentioned` in the `text` of + /// the [`Message`] object; 2) if the bot's message is a reply (has + /// `reply_to_message_id`), sender of the original message. + /// + /// Example: A user votes in a poll, bot returns confirmation message in + /// reply to the vote and removes the keyboard for that user, while still + /// showing the keyboard with poll options to users who haven't voted yet. + /// + /// [`Message`]: crate::types::Message + pub selective: Option, +} + +impl KeyboardRemove { + #[must_use] + pub const fn new() -> Self { + Self { remove_keyboard: True, selective: None } + } + + #[must_use] + pub const fn selective(mut self, val: bool) -> Self { + self.selective = Some(val); + self + } +} diff --git a/crates/teloxide-core/src/types/reply_markup.rs b/crates/teloxide-core/src/types/reply_markup.rs new file mode 100644 index 00000000..d2e179bf --- /dev/null +++ b/crates/teloxide-core/src/types/reply_markup.rs @@ -0,0 +1,80 @@ +use derive_more::From; +use serde::{Deserialize, Serialize}; + +use crate::types::{ + ForceReply, InlineKeyboardButton, InlineKeyboardMarkup, KeyboardButton, KeyboardMarkup, + KeyboardRemove, +}; + +#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize, From)] +#[serde(untagged)] +pub enum ReplyMarkup { + InlineKeyboard(InlineKeyboardMarkup), + Keyboard(KeyboardMarkup), + KeyboardRemove(KeyboardRemove), + ForceReply(ForceReply), +} + +impl ReplyMarkup { + /// Constructor for [`InlineKeyboard`] variant. + /// + /// This is a shortcut to + /// `ReplyMarkup::InlineKeyboard(InlineKeyboardMarkup::new(_))`. + /// + /// [`InlineKeyboard`]: ReplyMarkup::InlineKeyboard + pub fn inline_kb(inline_keyboard: I) -> Self + where + I: IntoIterator, + I::Item: IntoIterator, + { + Self::InlineKeyboard(InlineKeyboardMarkup::new(inline_keyboard)) + } + + /// Constructor for [`Keyboard`] variant. + /// + /// This is a shortcut to + /// `ReplyMarkup::Keyboard(KeyboardMarkup::new(_))`. + /// + /// [`Keyboard`]: ReplyMarkup::Keyboard + pub fn keyboard(keyboard: K) -> Self + where + K: IntoIterator, + K::Item: IntoIterator, + { + Self::Keyboard(KeyboardMarkup::new(keyboard)) + } + + /// Constructor for [`KeyboardRemove`] variant. + /// + /// This is a shortcut to + /// `ReplyMarkup::KeyboardRemove(ReplyKeyboardRemove::new()))`. + /// + /// [`KeyboardRemove`]: ReplyMarkup::KeyboardRemove + #[must_use] + pub fn kb_remove() -> Self { + Self::KeyboardRemove(KeyboardRemove::new()) + } + + /// Constructor for [`ForceReply`] variant. + /// + /// This is a shortcut to `ReplyMarkup::ForceReply(ForceReply::new())`. + /// + /// [`ForceReply`]: ReplyMarkup::KeyboardRemove + #[must_use] + pub fn force_reply() -> Self { + Self::ForceReply(ForceReply::new()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn inline_keyboard_markup() { + let data = InlineKeyboardMarkup::default(); + let expected = ReplyMarkup::InlineKeyboard(data.clone()); + let actual: ReplyMarkup = data.into(); + assert_eq!(actual, expected) + } +} diff --git a/crates/teloxide-core/src/types/response_parameters.rs b/crates/teloxide-core/src/types/response_parameters.rs new file mode 100644 index 00000000..b792a626 --- /dev/null +++ b/crates/teloxide-core/src/types/response_parameters.rs @@ -0,0 +1,44 @@ +use std::time::Duration; + +use serde::{Deserialize, Serialize}; + +/// Contains information about why a request was unsuccessful. +/// +/// [The official docs](https://core.telegram.org/bots/api#responseparameters). +#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum ResponseParameters { + /// The group has been migrated to a supergroup with the specified + /// identifier. This number may be greater than 32 bits and some + /// programming languages may have difficulty/silent defects in + /// interpreting it. But it is smaller than 52 bits, so a signed 64 bit + /// integer or double-precision float type are safe for storing this + /// identifier. + MigrateToChatId(i64), + + /// In case of exceeding flood control, the number of seconds left to wait + /// before the request can be repeated. + RetryAfter(#[serde(with = "crate::types::duration_secs")] Duration), +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn migrate_to_chat_id_deserialization() { + let expected = ResponseParameters::MigrateToChatId(123_456); + let actual: ResponseParameters = + serde_json::from_str(r#"{"migrate_to_chat_id":123456}"#).unwrap(); + + assert_eq!(expected, actual); + } + + #[test] + fn retry_after_deserialization() { + let expected = ResponseParameters::RetryAfter(Duration::from_secs(123_456)); + let actual: ResponseParameters = serde_json::from_str(r#"{"retry_after":123456}"#).unwrap(); + + assert_eq!(expected, actual); + } +} diff --git a/crates/teloxide-core/src/types/sent_web_app_message.rs b/crates/teloxide-core/src/types/sent_web_app_message.rs new file mode 100644 index 00000000..2f39381d --- /dev/null +++ b/crates/teloxide-core/src/types/sent_web_app_message.rs @@ -0,0 +1,13 @@ +use serde::{Deserialize, Serialize}; + +/// Contains information about an inline message sent by a [Web App] on behalf +/// of a user. +/// +/// [Web App]: https://core.telegram.org/bots/webapps +#[serde_with_macros::skip_serializing_none] +#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)] +pub struct SentWebAppMessage { + /// Identifier of the sent inline message. Available only if there is an + /// inline keyboard attached to the message. + pub inline_message_id: Option, +} diff --git a/crates/teloxide-core/src/types/shipping_address.rs b/crates/teloxide-core/src/types/shipping_address.rs new file mode 100644 index 00000000..478ad714 --- /dev/null +++ b/crates/teloxide-core/src/types/shipping_address.rs @@ -0,0 +1,26 @@ +use crate::types::CountryCode; +use serde::{Deserialize, Serialize}; + +/// This object represents a shipping address. +/// +/// [The official docs](https://core.telegram.org/bots/api#shippingaddress). +#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)] +pub struct ShippingAddress { + /// ISO 3166-1 alpha-2 country code. + pub country_code: CountryCode, + + /// State, if applicable. + pub state: String, + + /// City. + pub city: String, + + /// First line for the address. + pub street_line1: String, + + /// Second line for the address. + pub street_line2: String, + + /// Address post code. + pub post_code: String, +} diff --git a/crates/teloxide-core/src/types/shipping_option.rs b/crates/teloxide-core/src/types/shipping_option.rs new file mode 100644 index 00000000..d1b4fe6e --- /dev/null +++ b/crates/teloxide-core/src/types/shipping_option.rs @@ -0,0 +1,70 @@ +use serde::{Deserialize, Serialize}; + +use crate::types::LabeledPrice; + +/// This object represents one shipping option. +/// +/// [The official docs](https://core.telegram.org/bots/api#shippingoption). +#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)] +pub struct ShippingOption { + /// Shipping option identifier. + pub id: String, + + /// Option title. + pub title: String, + + /// List of price portions. + pub prices: Vec, +} + +impl ShippingOption { + pub fn new(id: S1, title: S2, prices: P) -> Self + where + S1: Into, + S2: Into, + P: IntoIterator, + { + Self { id: id.into(), title: title.into(), prices: prices.into_iter().collect() } + } + + pub fn id(mut self, val: S) -> Self + where + S: Into, + { + self.id = val.into(); + self + } + + pub fn title(mut self, val: S) -> Self + where + S: Into, + { + self.title = val.into(); + self + } + + pub fn prices

(mut self, val: P) -> Self + where + P: IntoIterator, + { + self.prices = val.into_iter().collect(); + self + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn serialize() { + let shipping_option = ShippingOption { + id: "0".to_string(), + title: "Option".to_string(), + prices: vec![LabeledPrice { label: "Label".to_string(), amount: 60 }], + }; + let expected = r#"{"id":"0","title":"Option","prices":[{"label":"Label","amount":60}]}"#; + let actual = serde_json::to_string(&shipping_option).unwrap(); + assert_eq!(actual, expected); + } +} diff --git a/crates/teloxide-core/src/types/shipping_query.rs b/crates/teloxide-core/src/types/shipping_query.rs new file mode 100644 index 00000000..f8df55d4 --- /dev/null +++ b/crates/teloxide-core/src/types/shipping_query.rs @@ -0,0 +1,21 @@ +use serde::{Deserialize, Serialize}; + +use crate::types::{ShippingAddress, User}; + +/// This object contains information about an incoming shipping query. +/// +/// [The official docs](https://core.telegram.org/bots/api#shippingquery). +#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)] +pub struct ShippingQuery { + /// Unique query identifier. + pub id: String, + + /// User who sent the query. + pub from: User, + + /// Bot specified invoice payload. + pub invoice_payload: String, + + /// User specified shipping address. + pub shipping_address: ShippingAddress, +} diff --git a/crates/teloxide-core/src/types/sticker.rs b/crates/teloxide-core/src/types/sticker.rs new file mode 100644 index 00000000..0424b7ae --- /dev/null +++ b/crates/teloxide-core/src/types/sticker.rs @@ -0,0 +1,438 @@ +use std::{convert::TryFrom, ops::Deref}; + +use serde::{Deserialize, Serialize}; + +use crate::types::{FileMeta, MaskPosition, PhotoSize}; + +/// This object represents a sticker. +/// +/// [The official docs](https://core.telegram.org/bots/api#sticker). +#[serde_with_macros::skip_serializing_none] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct Sticker { + /// Metadata of the sticker file. + #[serde(flatten)] + pub file: FileMeta, + + /// Sticker width, in pixels. + /// + /// You can assume that `max(width, height) = 512`, `min(width, height) <= + /// 512`. In other words one dimension is exactly 512 pixels and the other + /// is at most 512 pixels. + pub width: u16, + + /// Sticker height, in pixels. + /// + /// You can assume that `max(width, height) = 512`, `min(width, height) <= + /// 512`. In other words one dimension is exactly 512 pixels and the other + /// is at most 512 pixels. + pub height: u16, + + /// Kind of this sticker - regular, mask or custom emoji. + /// + /// In other words this represent how the sticker is presented, as a big + /// picture/video, as a mask while editing pictures or as a custom emoji in + /// messages. + #[serde(flatten)] + pub kind: StickerKind, + + /// Format of this sticker - raster/`.webp`, animated/`.tgs` or + /// video/`.webm`. + /// + /// In other words this represents how the sticker is encoded. + #[serde(flatten)] + pub format: StickerFormat, + + /// Sticker thumbnail in the `.webp` or `.jpg` format. + pub thumb: Option, + + /// Emoji associated with the sticker. + pub emoji: Option, + + /// Name of the sticker set to which the sticker belongs. + pub set_name: Option, +} + +/// Kind of a [`Sticker`] - regular, mask or custom emoji. +/// +/// Dataful version of [`StickerType`]. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(tag = "type")] +#[serde(rename_all = "snake_case")] +pub enum StickerKind { + /// "Normal", raster, animated or video sticker. + Regular { + /// Premium animation for the sticker, if the sticker is premium. + premium_animation: Option, + }, + /// Mask sticker. + Mask { + /// For mask stickers, the position where the mask should be placed. + mask_position: MaskPosition, + }, + /// Custom emoji sticker. + CustomEmoji { + /// A unique identifier of the custom emoji. + // FIXME(waffle): newtype + custom_emoji_id: String, + }, +} + +/// Type of a [`Sticker`] - regular, mask or custom emoji. +/// +/// Dataless version of [`StickerType`]. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(tag = "sticker_type")] +#[serde(rename_all = "snake_case")] +pub enum StickerType { + /// "Normal", raster, animated or video sticker. + Regular, + /// Mask sticker. + Mask, + /// Custom emoji sticker. + CustomEmoji, +} + +/// Format of a [`Sticker`] - regular/webp, animated/tgs or video/webm. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(try_from = "StickerFormatRaw", into = "StickerFormatRaw")] +pub enum StickerFormat { + /// "Normal", raster, `.webp` sticker. + Raster, + /// [Animated], `.tgs` sticker. + /// + /// [Animated]: https://telegram.org/blog/animated-stickers + Animated, + /// [Video], `.webm` sticker. + /// + /// [Video]: https://telegram.org/blog/video-stickers-better-reactions + Video, +} + +/// This allows calling [`StickerKind`]'s methods directly on [`Sticker`]. +/// +/// ```no_run +/// use teloxide_core::types::Sticker; +/// +/// let sticker: Sticker = todo!(); +/// +/// let _ = sticker.is_regular(); +/// let _ = sticker.kind.is_regular(); +/// +/// let _ = sticker.mask_position(); +/// let _ = sticker.kind.mask_position(); +/// ``` +impl Deref for Sticker { + type Target = StickerKind; + + fn deref(&self) -> &Self::Target { + &self.kind + } +} + +impl Sticker { + /// Returns `true` is this is a "normal" raster sticker. + /// + /// Alias to [`self.format.is_raster()`]. + /// + /// [`self.format.is_raster()`]: StickerFormat::is_raster + #[must_use] + pub fn is_raster(&self) -> bool { + self.format.is_raster() + } + + /// Returns `true` is this is an [animated] sticker. + /// + /// Alias to [`self.format.is_animated()`]. + /// + /// [`self.format.is_animated()`]: StickerFormat::is_animated + /// [animated]: https://telegram.org/blog/animated-stickers + #[must_use] + pub fn is_animated(&self) -> bool { + self.format.is_animated() + } + + /// Returns `true` is this is a [video] sticker. + /// + /// Alias to [`self.format.is_video()`]. + /// + /// [`self.format.is_video()`]: StickerFormat::is_video + /// [video]: https://telegram.org/blog/video-stickers-better-reactions + #[must_use] + pub fn is_video(&self) -> bool { + self.format.is_video() + } +} + +impl StickerKind { + /// Converts [`StickerKind`] to [`StickerType`] + #[must_use] + pub fn type_(&self) -> StickerType { + match self { + StickerKind::Regular { .. } => StickerType::Regular, + StickerKind::Mask { .. } => StickerType::Mask, + StickerKind::CustomEmoji { .. } => StickerType::CustomEmoji, + } + } + + /// Returns `true` if the sticker kind is [`Regular`]. + /// + /// [`Regular`]: StickerKind::Regular + #[must_use] + pub fn is_regular(&self) -> bool { + self.type_().is_regular() + } + + /// Returns `true` if the sticker kind is [`Mask`]. + /// + /// [`Mask`]: StickerKind::Mask + #[must_use] + pub fn is_mask(&self) -> bool { + self.type_().is_mask() + } + + /// Returns `true` if the sticker kind is [`CustomEmoji`]. + /// + /// [`CustomEmoji`]: StickerKind::CustomEmoji + #[must_use] + pub fn is_custom_emoji(&self) -> bool { + self.type_().is_custom_emoji() + } + + /// Getter for [`StickerKind::Regular::premium_animation`]. + pub fn premium_animation(&self) -> Option<&FileMeta> { + if let Self::Regular { premium_animation } = self { + premium_animation.as_ref() + } else { + None + } + } + + /// Getter for [`StickerKind::Mask::mask_position`]. + pub fn mask_position(&self) -> Option { + if let Self::Mask { mask_position } = self { + Some(*mask_position) + } else { + None + } + } + + /// Getter for [`StickerKind::CustomEmoji::custom_emoji_id`]. + pub fn custom_emoji_id(&self) -> Option<&str> { + if let Self::CustomEmoji { custom_emoji_id } = self { + Some(custom_emoji_id) + } else { + None + } + } +} + +impl StickerType { + /// Returns `true` if the sticker type is [`Regular`]. + /// + /// [`Regular`]: StickerType::Regular + #[must_use] + pub fn is_regular(&self) -> bool { + matches!(self, Self::Regular) + } + + /// Returns `true` if the sticker type is [`Mask`]. + /// + /// [`Mask`]: StickerType::Mask + #[must_use] + pub fn is_mask(&self) -> bool { + matches!(self, Self::Mask) + } + + /// Returns `true` if the sticker type is [`CustomEmoji`]. + /// + /// [`CustomEmoji`]: StickerType::CustomEmoji + #[must_use] + pub fn is_custom_emoji(&self) -> bool { + matches!(self, Self::CustomEmoji) + } +} + +impl StickerFormat { + /// Returns `true` if the sticker format is [`Raster`]. + /// + /// [`Raster`]: StickerFormat::Raster + #[must_use] + pub fn is_raster(&self) -> bool { + matches!(self, Self::Raster) + } + + /// Returns `true` if the sticker format is [`Animated`]. + /// + /// [`Animated`]: StickerFormat::Animated + #[must_use] + pub fn is_animated(&self) -> bool { + matches!(self, Self::Animated) + } + + /// Returns `true` if the sticker format is [`Video`]. + /// + /// [`Video`]: StickerFormat::Video + #[must_use] + pub fn is_video(&self) -> bool { + matches!(self, Self::Video) + } +} + +#[derive(Serialize, Deserialize)] +struct StickerFormatRaw { + is_animated: bool, + is_video: bool, +} + +impl TryFrom for StickerFormat { + type Error = &'static str; + + fn try_from( + StickerFormatRaw { is_animated, is_video }: StickerFormatRaw, + ) -> Result { + let ret = match (is_animated, is_video) { + (false, false) => Self::Raster, + (true, false) => Self::Animated, + (false, true) => Self::Video, + (true, true) => return Err("`is_animated` and `is_video` present at the same time"), + }; + + Ok(ret) + } +} + +impl From for StickerFormatRaw { + fn from(kind: StickerFormat) -> Self { + match kind { + StickerFormat::Raster => Self { is_animated: false, is_video: false }, + StickerFormat::Animated => Self { is_animated: true, is_video: false }, + StickerFormat::Video => Self { is_animated: false, is_video: true }, + } + } +} + +#[cfg(test)] +mod tests { + use crate::types::{MaskPoint, Sticker, StickerFormat, StickerType}; + + #[test] + fn mask_serde() { + // Taken from a real (mask) sticker set + let json = r#"{ + "width": 512, + "height": 512, + "emoji": "🎭", + "set_name": "Coronamask", + "is_animated": false, + "is_video": false, + "type": "mask", + "mask_position": { + "point": "forehead", + "x_shift": -0.0125, + "y_shift": 0.5525, + "scale": 1.94 + }, + "thumb": { + "file_id": "AAMCAQADFQABYzA0qlYHijpjMzMwBFKnEVE5XdkAAjIKAAK_jJAE1TRw7D936M8BAAdtAAMpBA", + "file_unique_id": "AQADMgoAAr-MkARy", + "file_size": 11028, + "width": 320, + "height": 320 + }, + "file_id": "CAACAgEAAxUAAWMwNKpWB4o6YzMzMARSpxFROV3ZAAIyCgACv4yQBNU0cOw_d-jPKQQ", + "file_unique_id": "AgADMgoAAr-MkAQ", + "file_size": 18290 + }"#; + + let sticker: Sticker = serde_json::from_str(json).unwrap(); + + // Assert some basic properties are correctly deserialized + assert_eq!(sticker.type_(), StickerType::Mask); + assert_eq!(sticker.mask_position().unwrap().point, MaskPoint::Forehead); + assert_eq!(sticker.is_animated(), false); + assert_eq!(sticker.is_video(), false); + assert_eq!(sticker.thumb.clone().unwrap().file.size, 11028); + assert_eq!(sticker.file.size, 18290); + assert_eq!(sticker.width, 512); + assert_eq!(sticker.height, 512); + + let json2 = serde_json::to_string(&sticker).unwrap(); + let sticker2: Sticker = serde_json::from_str(&json2).unwrap(); + assert_eq!(sticker, sticker2); + } + + #[test] + fn regular_serde() { + // Taken from a real sticker set + let json = r#"{ + "width": 463, + "height": 512, + "emoji": "🍿", + "set_name": "menhera2", + "is_animated": false, + "is_video": false, + "type": "regular", + "thumb": { + "file_id": "AAMCAgADFQABYzBxOJ1GWrttqL7FSRwdAtrq-AkAAtkHAALBGJ4LUUUh5CUew90BAAdtAAMpBA", + "file_unique_id": "AQAD2QcAAsEYngty", + "file_size": 4558, + "width": 116, + "height": 128 + }, + "file_id": "CAACAgIAAxUAAWMwcTidRlq7bai-xUkcHQLa6vgJAALZBwACwRieC1FFIeQlHsPdKQQ", + "file_unique_id": "AgAD2QcAAsEYngs", + "file_size": 25734 + }"#; + + let sticker: Sticker = serde_json::from_str(json).unwrap(); + + // Assert some basic properties are correctly deserialized + assert_eq!(sticker.type_(), StickerType::Regular); + assert_eq!(sticker.premium_animation(), None); + assert_eq!(sticker.is_animated(), false); + assert_eq!(sticker.is_video(), false); + assert_eq!(sticker.thumb.clone().unwrap().file.size, 4558); + assert_eq!(sticker.file.size, 25734); + assert_eq!(sticker.width, 463); + assert_eq!(sticker.height, 512); + assert_eq!(sticker.set_name.as_deref(), Some("menhera2")); + + let json2 = serde_json::to_string(&sticker).unwrap(); + let sticker2: Sticker = serde_json::from_str(&json2).unwrap(); + assert_eq!(sticker, sticker2); + } + + #[test] + fn sticker_format_serde() { + { + let json = r#"{"is_animated":false,"is_video":false}"#; + let fmt: StickerFormat = serde_json::from_str(json).unwrap(); + assert_eq!(fmt, StickerFormat::Raster); + + let json2 = serde_json::to_string(&fmt).unwrap(); + assert_eq!(json, json2); + } + { + let json = r#"{"is_animated":true,"is_video":false}"#; + let fmt: StickerFormat = serde_json::from_str(json).unwrap(); + assert_eq!(fmt, StickerFormat::Animated); + + let json2 = serde_json::to_string(&fmt).unwrap(); + assert_eq!(json, json2); + } + { + let json = r#"{"is_animated":false,"is_video":true}"#; + let fmt: StickerFormat = serde_json::from_str(json).unwrap(); + assert_eq!(fmt, StickerFormat::Video); + + let json2 = serde_json::to_string(&fmt).unwrap(); + assert_eq!(json, json2); + } + { + let json = r#"{"is_animated":true,"is_video":true}"#; + let fmt: Result = serde_json::from_str(json); + assert!(fmt.is_err()); + } + } +} diff --git a/crates/teloxide-core/src/types/sticker_set.rs b/crates/teloxide-core/src/types/sticker_set.rs new file mode 100644 index 00000000..d7adaf29 --- /dev/null +++ b/crates/teloxide-core/src/types/sticker_set.rs @@ -0,0 +1,148 @@ +use std::ops::Deref; + +use serde::{Deserialize, Serialize}; + +use crate::types::{PhotoSize, Sticker, StickerFormat, StickerType}; + +/// This object represents a sticker set. +/// +/// [The official docs](https://core.telegram.org/bots/api#stickerset). +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct StickerSet { + /// Sticker set name. + pub name: String, + + /// Sticker set title. + pub title: String, + + /// Sticker type shared by all stickers in this set. + #[serde(flatten)] + pub kind: StickerType, + + /// Sticker format shared by all stickers in this set. + #[serde(flatten)] + pub format: StickerFormat, + + /// List of all set stickers. + pub stickers: Vec, + + /// Sticker set thumbnail in the `.webp`, `.tgs` or `.webm` format. + pub thumb: Option, +} + +/// This allows calling [`StickerType`]'s methods directly on [`StickerSet`]. +/// +/// ```no_run +/// use teloxide_core::types::StickerSet; +/// +/// let sticker: StickerSet = todo!(); +/// +/// let _ = sticker.is_mask(); +/// let _ = sticker.kind.is_mask(); +/// ``` +impl Deref for StickerSet { + type Target = StickerType; + + fn deref(&self) -> &Self::Target { + &self.kind + } +} + +impl StickerSet { + /// Returns `true` is this is a "normal" raster sticker. + /// + /// Alias to [`self.format.is_raster()`]. + /// + /// [`self.format.is_raster()`]: StickerFormat::is_raster + #[must_use] + pub fn is_raster(&self) -> bool { + self.format.is_raster() + } + + /// Returns `true` is this is an [animated] sticker. + /// + /// Alias to [`self.format.is_animated()`]. + /// + /// [`self.format.is_animated()`]: StickerFormat::is_animated + /// [animated]: https://telegram.org/blog/animated-stickers + #[must_use] + pub fn is_animated(&self) -> bool { + self.format.is_animated() + } + + /// Returns `true` is this is a [video] sticker. + /// + /// Alias to [`self.format.is_video()`]. + /// + /// [`self.format.is_video()`]: StickerFormat::is_video + /// [video]: https://telegram.org/blog/video-stickers-better-reactions + #[must_use] + pub fn is_video(&self) -> bool { + self.format.is_video() + } +} + +#[cfg(test)] +mod tests { + use crate::types::StickerSet; + + #[test] + fn smoke_serde() { + // https://t.me/addstickers/teloxide_test + let json = r#"{ + "name": "teloxide_test", + "title": "teloxide-test", + "is_animated": false, + "is_video": false, + "sticker_type": "regular", + "contains_masks": false, + "stickers": [ + { + "width": 512, + "height": 512, + "emoji": "⚙️", + "set_name": "teloxide_test", + "is_animated": false, + "is_video": false, + "type": "regular", + "thumb": { + "file_id": "AAMCAQADFQABYzB4ATH0sqXx351gZ5GpY1Z3Tl8AAlgCAAJ1t4hFbxNCoAg1-akBAAdtAAMpBA", + "file_unique_id": "AQADWAIAAnW3iEVy", + "file_size": 7698, + "width": 320, + "height": 320 + }, + "file_id": "CAACAgEAAxUAAWMweAEx9LKl8d-dYGeRqWNWd05fAAJYAgACdbeIRW8TQqAINfmpKQQ", + "file_unique_id": "AgADWAIAAnW3iEU", + "file_size": 12266 + }, + { + "width": 512, + "height": 512, + "emoji": "⚙️", + "set_name": "teloxide_test", + "is_animated": false, + "is_video": false, + "type": "regular", + "thumb": { + "file_id": "AAMCAQADFQABYzB4AcABR8-MuvGagis9Pk6liSAAAs8DAAL2YYBFNbvduoN1p7oBAAdtAAMpBA", + "file_unique_id": "AQADzwMAAvZhgEVy", + "file_size": 7780, + "width": 320, + "height": 320 + }, + "file_id": "CAACAgEAAxUAAWMweAHAAUfPjLrxmoIrPT5OpYkgAALPAwAC9mGARTW73bqDdae6KQQ", + "file_unique_id": "AgADzwMAAvZhgEU", + "file_size": 12158 + } + ] + }"#; + + let set: StickerSet = serde_json::from_str(json).unwrap(); + + assert!(set.is_raster()); + assert!(set.is_regular()); + assert!(set.thumb.is_none()); + assert_eq!(set.stickers.len(), 2); + } +} diff --git a/crates/teloxide-core/src/types/successful_payment.rs b/crates/teloxide-core/src/types/successful_payment.rs new file mode 100644 index 00000000..270768c4 --- /dev/null +++ b/crates/teloxide-core/src/types/successful_payment.rs @@ -0,0 +1,40 @@ +use serde::{Deserialize, Serialize}; + +use crate::types::{Currency, OrderInfo}; + +/// This object contains basic information about a successful payment. +/// +/// [The official docs](https://core.telegram.org/bots/api#successfulpayment). +#[serde_with_macros::skip_serializing_none] +#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)] +pub struct SuccessfulPayment { + /// Three-letter ISO 4217 [currency] code. + /// + /// [currency]: https://core.telegram.org/bots/payments#supported-currencies + pub currency: Currency, + + /// Total price in the smallest units of the currency (integer, not + /// float/double). For example, for a price of `US$ 1.45` pass `amount = + /// 145`. See the exp parameter in [`currencies.json`], it shows the + /// number of digits past the decimal point for each currency (2 for + /// the majority of currencies). + /// + /// [`currencies.json`]: https://core.telegram.org/bots/payments/currencies.json + pub total_amount: i32, + + /// Bot specified invoice payload. + pub invoice_payload: String, + + /// Identifier of the shipping option chosen by the user. + pub shipping_option_id: Option, + + /// Order info provided by the user. + #[serde(default)] + pub order_info: OrderInfo, + + /// Telegram payment identifier. + pub telegram_payment_charge_id: String, + + /// Provider payment identifier. + pub provider_payment_charge_id: String, +} diff --git a/crates/teloxide-core/src/types/target_message.rs b/crates/teloxide-core/src/types/target_message.rs new file mode 100644 index 00000000..e851be06 --- /dev/null +++ b/crates/teloxide-core/src/types/target_message.rs @@ -0,0 +1,23 @@ +use crate::types::{MessageId, Recipient}; + +use serde::{Deserialize, Serialize}; + +/// A message in chat or inline message. +#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)] +#[serde(untagged)] +pub enum TargetMessage { + Common { + chat_id: Recipient, + #[serde(flatten)] + message_id: MessageId, + }, + Inline { + inline_message_id: String, + }, +} + +impl From for TargetMessage { + fn from(inline_message_id: String) -> Self { + Self::Inline { inline_message_id } + } +} diff --git a/crates/teloxide-core/src/types/unit_false.rs b/crates/teloxide-core/src/types/unit_false.rs new file mode 100644 index 00000000..bde6410b --- /dev/null +++ b/crates/teloxide-core/src/types/unit_false.rs @@ -0,0 +1,76 @@ +use serde::{de::Visitor, Deserialize, Deserializer, Serialize, Serializer}; + +/// A type that is always false. +#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, Default)] +pub struct False; + +impl std::convert::TryFrom for False { + type Error = (); + + fn try_from(value: bool) -> Result { + match value { + true => Err(()), + false => Ok(False), + } + } +} + +impl<'de> Deserialize<'de> for False { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_bool(FalseVisitor) + } +} + +struct FalseVisitor; + +impl<'de> Visitor<'de> for FalseVisitor { + type Value = False; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(formatter, "bool, equal to `false`") + } + + fn visit_bool(self, value: bool) -> Result + where + E: serde::de::Error, + { + match value { + true => Err(E::custom("expected `false`, found `true`")), + false => Ok(False), + } + } +} + +impl Serialize for False { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_bool(false) + } +} + +#[cfg(test)] +mod tests { + use serde_json::{from_str, to_string}; + + use super::False; + + #[test] + fn unit_false_de() { + let json = "false"; + let expected = False; + let actual = from_str(json).unwrap(); + assert_eq!(expected, actual); + } + + #[test] + fn unit_false_se() { + let actual = to_string(&False).unwrap(); + let expected = "false"; + assert_eq!(expected, actual); + } +} diff --git a/crates/teloxide-core/src/types/unit_true.rs b/crates/teloxide-core/src/types/unit_true.rs new file mode 100644 index 00000000..cd71e5c2 --- /dev/null +++ b/crates/teloxide-core/src/types/unit_true.rs @@ -0,0 +1,79 @@ +use serde::{ + de::{self, Deserialize, Deserializer, Visitor}, + ser::{Serialize, Serializer}, +}; + +/// A type that is always true. +#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, Default)] +pub struct True; + +impl std::convert::TryFrom for True { + type Error = (); + + fn try_from(value: bool) -> Result { + match value { + true => Ok(True), + false => Err(()), + } + } +} + +impl<'de> Deserialize<'de> for True { + fn deserialize(des: D) -> Result + where + D: Deserializer<'de>, + { + des.deserialize_bool(TrueVisitor) + } +} + +struct TrueVisitor; + +impl<'de> Visitor<'de> for TrueVisitor { + type Value = True; + + fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "bool, equal to `true`") + } + + fn visit_bool(self, value: bool) -> Result + where + E: de::Error, + { + match value { + true => Ok(True), + false => Err(E::custom("expected `true`, found `false`")), + } + } +} + +impl Serialize for True { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_bool(true) + } +} + +#[cfg(test)] +mod tests { + use serde_json::{from_str, to_string}; + + use super::True; + + #[test] + fn unit_true_de() { + let json = "true"; + let expected = True; + let actual = from_str(json).unwrap(); + assert_eq!(expected, actual); + } + + #[test] + fn unit_true_se() { + let actual = to_string(&True).unwrap(); + let expected = "true"; + assert_eq!(expected, actual); + } +} diff --git a/crates/teloxide-core/src/types/update.rs b/crates/teloxide-core/src/types/update.rs new file mode 100644 index 00000000..d34c3798 --- /dev/null +++ b/crates/teloxide-core/src/types/update.rs @@ -0,0 +1,532 @@ +#![allow(clippy::large_enum_variant)] +use serde::{de::MapAccess, Deserialize, Serialize, Serializer}; +use serde_json::Value; + +use crate::types::{ + CallbackQuery, Chat, ChatJoinRequest, ChatMemberUpdated, ChosenInlineResult, InlineQuery, + Message, Poll, PollAnswer, PreCheckoutQuery, ShippingQuery, User, +}; + +/// This [object] represents an incoming update. +/// +/// [The official docs](https://core.telegram.org/bots/api#update). +/// +/// [object]: https://core.telegram.org/bots/api#available-types +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct Update { + /// The update‘s unique identifier. Update identifiers start from a certain + /// positive number and increase sequentially. This ID becomes especially + /// handy if you’re using webhooks, since it allows you to ignore + /// repeated updates or to restore the correct update sequence, should + /// they get out of order. If there are no new updates for at least a + /// week, then identifier of the next update will be chosen randomly + /// instead of sequentially. + #[serde(rename = "update_id")] + pub id: i32, + + #[serde(flatten)] + pub kind: UpdateKind, +} + +impl Update { + #[must_use] + pub fn user(&self) -> Option<&User> { + match &self.kind { + UpdateKind::Message(m) => m.from(), + UpdateKind::EditedMessage(m) => m.from(), + UpdateKind::CallbackQuery(query) => Some(&query.from), + UpdateKind::ChosenInlineResult(chosen) => Some(&chosen.from), + UpdateKind::InlineQuery(query) => Some(&query.from), + UpdateKind::ShippingQuery(query) => Some(&query.from), + UpdateKind::PreCheckoutQuery(query) => Some(&query.from), + UpdateKind::PollAnswer(answer) => Some(&answer.user), + _ => None, + } + } + + #[must_use] + pub fn chat(&self) -> Option<&Chat> { + match &self.kind { + UpdateKind::Message(m) => Some(&m.chat), + UpdateKind::EditedMessage(m) => Some(&m.chat), + UpdateKind::ChannelPost(p) => Some(&p.chat), + UpdateKind::EditedChannelPost(p) => Some(&p.chat), + UpdateKind::CallbackQuery(q) => Some(&q.message.as_ref()?.chat), + UpdateKind::ChatMember(m) => Some(&m.chat), + UpdateKind::MyChatMember(m) => Some(&m.chat), + UpdateKind::ChatJoinRequest(c) => Some(&c.chat), + _ => None, + } + } +} + +#[derive(Clone, Debug, PartialEq)] +pub enum UpdateKind { + /// New incoming message of any kind — text, photo, sticker, etc. + Message(Message), + + /// New version of a message that is known to the bot and was edited. + EditedMessage(Message), + + /// New incoming channel post of any kind — text, photo, sticker, etc. + ChannelPost(Message), + + /// New version of a channel post that is known to the bot and was edited. + EditedChannelPost(Message), + + /// New incoming [inline] query. + /// + /// [inline]: https://core.telegram.org/bots/api#inline-mode + InlineQuery(InlineQuery), + + /// The result of an [inline] query that was chosen by a user and sent to + /// their chat partner. Please see our documentation on the [feedback + /// collecting] for details on how to enable these updates for your bot. + /// + /// [inline]: https://core.telegram.org/bots/api#inline-mode + /// [feedback collecting]: https://core.telegram.org/bots/inline#collecting-feedback + ChosenInlineResult(ChosenInlineResult), + + /// New incoming callback query. + CallbackQuery(CallbackQuery), + + /// New incoming shipping query. Only for invoices with flexible price. + ShippingQuery(ShippingQuery), + + /// New incoming pre-checkout query. Contains full information about + /// checkout. + PreCheckoutQuery(PreCheckoutQuery), + + /// New poll state. Bots receive only updates about stopped polls and + /// polls, which are sent by the bot. + Poll(Poll), + + /// A user changed their answer in a non-anonymous poll. Bots receive new + /// votes only in polls that were sent by the bot itself. + PollAnswer(PollAnswer), + + /// The bot's chat member status was updated in a chat. For private chats, + /// this update is received only when the bot is blocked or unblocked by the + /// user. + MyChatMember(ChatMemberUpdated), + + /// A chat member's status was updated in a chat. The bot must be an + /// administrator in the chat and must explicitly specify + /// [`AllowedUpdate::ChatMember`] in the list of `allowed_updates` to + /// receive these updates. + /// + /// [`AllowedUpdate::ChatMember`]: crate::types::AllowedUpdate::ChatMember + ChatMember(ChatMemberUpdated), + + /// A request to join the chat has been sent. The bot must have the + /// can_invite_users administrator right in the chat to receive these + /// updates. + ChatJoinRequest(ChatJoinRequest), + + /// An error that happened during deserialization. + /// + /// This allows `teloxide` to continue working even if telegram adds a new + /// kind of updates. + Error(Value), +} + +impl<'de> Deserialize<'de> for UpdateKind { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct Visitor; + + impl<'de> serde::de::Visitor<'de> for Visitor { + type Value = UpdateKind; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a map") + } + + fn visit_map(self, mut map: A) -> Result + where + A: MapAccess<'de>, + { + let mut tmp = None; + + // Try to deserialize a borrowed-str key, or else try deserializing an owned + // string key + let k = map.next_key::<&str>().or_else(|_| { + map.next_key::().map(|k| { + tmp = k; + tmp.as_deref() + }) + }); + + if let Ok(Some(k)) = k { + let res = match k { + "message" => { + map.next_value::().map(UpdateKind::Message).map_err(|_| false) + } + "edited_message" => map + .next_value::() + .map(UpdateKind::EditedMessage) + .map_err(|_| false), + "channel_post" => map + .next_value::() + .map(UpdateKind::ChannelPost) + .map_err(|_| false), + "edited_channel_post" => map + .next_value::() + .map(UpdateKind::EditedChannelPost) + .map_err(|_| false), + "inline_query" => map + .next_value::() + .map(UpdateKind::InlineQuery) + .map_err(|_| false), + "chosen_inline_result" => map + .next_value::() + .map(UpdateKind::ChosenInlineResult) + .map_err(|_| false), + "callback_query" => map + .next_value::() + .map(UpdateKind::CallbackQuery) + .map_err(|_| false), + "shipping_query" => map + .next_value::() + .map(UpdateKind::ShippingQuery) + .map_err(|_| false), + "pre_checkout_query" => map + .next_value::() + .map(UpdateKind::PreCheckoutQuery) + .map_err(|_| false), + "poll" => map.next_value::().map(UpdateKind::Poll).map_err(|_| false), + "poll_answer" => map + .next_value::() + .map(UpdateKind::PollAnswer) + .map_err(|_| false), + "my_chat_member" => map + .next_value::() + .map(UpdateKind::MyChatMember) + .map_err(|_| false), + "chat_member" => map + .next_value::() + .map(UpdateKind::ChatMember) + .map_err(|_| false), + "chat_join_request" => map + .next_value::() + .map(UpdateKind::ChatJoinRequest) + .map_err(|_| false), + + _ => Err(true), + }; + + let value_available = match res { + Ok(ok) => return Ok(ok), + Err(e) => e, + }; + + let mut value = serde_json::Map::new(); + value.insert( + k.to_owned(), + if value_available { + map.next_value::().unwrap_or(Value::Null) + } else { + Value::Null + }, + ); + + while let Ok(Some((k, v))) = map.next_entry::<_, Value>() { + value.insert(k, v); + } + + return Ok(UpdateKind::Error(Value::Object(value))); + } + + Ok(UpdateKind::Error(Value::Object(<_>::default()))) + } + } + + deserializer.deserialize_any(Visitor) + } +} + +impl Serialize for UpdateKind { + fn serialize(&self, s: S) -> Result + where + S: Serializer, + { + let name = "UpdateKind"; + match self { + UpdateKind::Message(v) => s.serialize_newtype_variant(name, 0, "message", v), + UpdateKind::EditedMessage(v) => { + s.serialize_newtype_variant(name, 1, "edited_message", v) + } + UpdateKind::ChannelPost(v) => s.serialize_newtype_variant(name, 2, "channel_post", v), + UpdateKind::EditedChannelPost(v) => { + s.serialize_newtype_variant(name, 3, "edited_channel_post", v) + } + UpdateKind::InlineQuery(v) => s.serialize_newtype_variant(name, 4, "inline_query", v), + UpdateKind::ChosenInlineResult(v) => { + s.serialize_newtype_variant(name, 5, "chosen_inline_result", v) + } + UpdateKind::CallbackQuery(v) => { + s.serialize_newtype_variant(name, 6, "callback_query", v) + } + UpdateKind::ShippingQuery(v) => { + s.serialize_newtype_variant(name, 7, "shipping_query", v) + } + UpdateKind::PreCheckoutQuery(v) => { + s.serialize_newtype_variant(name, 8, "pre_checkout_query", v) + } + UpdateKind::Poll(v) => s.serialize_newtype_variant(name, 9, "poll", v), + UpdateKind::PollAnswer(v) => s.serialize_newtype_variant(name, 10, "poll_answer", v), + UpdateKind::MyChatMember(v) => { + s.serialize_newtype_variant(name, 11, "my_chat_member", v) + } + UpdateKind::ChatMember(v) => s.serialize_newtype_variant(name, 12, "chat_member", v), + UpdateKind::ChatJoinRequest(v) => { + s.serialize_newtype_variant(name, 13, "chat_join_request", v) + } + UpdateKind::Error(v) => v.serialize(s), + } + } +} + +#[cfg(test)] +mod test { + use crate::types::{ + Chat, ChatId, ChatKind, ChatPrivate, MediaKind, MediaText, Message, MessageCommon, + MessageId, MessageKind, Update, UpdateKind, User, UserId, + }; + + use chrono::{DateTime, NaiveDateTime, Utc}; + + // TODO: more tests for deserialization + #[test] + fn message() { + let timestamp = 1_569_518_342; + let date = + DateTime::from_utc(NaiveDateTime::from_timestamp_opt(timestamp, 0).unwrap(), Utc); + + let json = r#"{ + "update_id":892252934, + "message":{ + "message_id":6557, + "from":{ + "id":218485655, + "is_bot": false, + "first_name":"Waffle", + "username":"WaffleLapkin", + "language_code":"en" + }, + "chat":{ + "id":218485655, + "first_name":"Waffle", + "username":"WaffleLapkin", + "type":"private" + }, + "date":1569518342, + "text":"hello there" + } + }"#; + + let expected = Update { + id: 892_252_934, + kind: UpdateKind::Message(Message { + via_bot: None, + id: MessageId(6557), + date, + chat: Chat { + id: ChatId(218_485_655), + kind: ChatKind::Private(ChatPrivate { + username: Some(String::from("WaffleLapkin")), + first_name: Some(String::from("Waffle")), + last_name: None, + bio: None, + has_private_forwards: None, + has_restricted_voice_and_video_messages: None, + }), + photo: None, + pinned_message: None, + message_auto_delete_time: None, + }, + kind: MessageKind::Common(MessageCommon { + from: Some(User { + id: UserId(218_485_655), + is_bot: false, + first_name: String::from("Waffle"), + last_name: None, + username: Some(String::from("WaffleLapkin")), + language_code: Some(String::from("en")), + is_premium: false, + added_to_attachment_menu: false, + }), + reply_to_message: None, + forward: None, + edit_date: None, + media_kind: MediaKind::Text(MediaText { + text: String::from("hello there"), + entities: vec![], + }), + reply_markup: None, + sender_chat: None, + author_signature: None, + is_automatic_forward: false, + has_protected_content: false, + }), + }), + }; + + let actual = serde_json::from_str::(json).unwrap(); + assert_eq!(expected, actual); + } + + #[test] + fn de_private_chat_text_message() { + let text = r#" + { + "message": { + "chat": { + "first_name": "Hirrolot", + "id": 408258968, + "type": "private", + "username": "hirrolot" + }, + "date": 1581448857, + "from": { + "first_name": "Hirrolot", + "id": 408258968, + "is_bot": false, + "language_code": "en", + "username": "hirrolot" + }, + "message_id": 154, + "text": "4" + }, + "update_id": 306197398 + } +"#; + + let Update { kind, .. } = serde_json::from_str::(text).unwrap(); + match kind { + UpdateKind::Message(_) => {} + _ => panic!("Expected `Message`"), + } + } + + #[test] + fn pinned_message_works() { + let json = r#"{ + "message": { + "chat": { + "id": -1001276785818, + "title": "teloxide dev", + "type": "supergroup", + "username": "teloxide_dev" + }, + "date": 1582134655, + "from": { + "first_name": "Hirrolot", + "id": 408258968, + "is_bot": false, + "username": "hirrolot" + }, + "message_id": 20225, + "pinned_message": { + "chat": { + "id": -1001276785818, + "title": "teloxide dev", + "type": "supergroup", + "username": "teloxide_dev" + }, + "date": 1582134643, + "from": { + "first_name": "Hirrolot", + "id": 408258968, + "is_bot": false, + "username": "hirrolot" + }, + "message_id": 20224, + "text": "Faster than a bullet" + } + }, + "update_id": 845402291 +}"#; + + let Update { kind, .. } = serde_json::from_str(json).unwrap(); + match kind { + UpdateKind::Message(_) => {} + _ => panic!("Expected `Message`"), + } + } + + #[test] + fn dice_works() { + let json = r#" + { + "message": { + "chat": { + "id": -1001276785818, + "title": "bla bla bla chat", + "type": "supergroup", + "username": "teloxide_dev" + }, + "date": 1596014550, + "dice": { + "emoji": "🎲", + "value": 2 + }, + "from": { + "first_name": "Hirrolot", + "id": 408258968, + "is_bot": false, + "language_code": "en", + "username": "hirrolot" + }, + "message_id": 35410 + }, + "update_id": 573255266 +} + "#; + + let Update { kind, .. } = serde_json::from_str(json).unwrap(); + match kind { + UpdateKind::Message(_) => {} + _ => panic!("Expected `Message`"), + } + } + + #[test] + fn new_update_kind_error() { + let json = r#"{ + "new_update_kind": {"some_field_idk": 1}, + "update_id": 1 + }"#; + + let Update { kind, .. } = serde_json::from_str(json).unwrap(); + + match kind { + // Deserialization failed successfully + UpdateKind::Error(_) => {} + _ => panic!("Expected error"), + } + } + + #[test] + fn issue_523() { + let json = r#"{ + "update_id":0, + "my_chat_member": { + "chat":{"id":0,"first_name":"FN","last_name":"LN","username":"UN","type":"private"}, + "from":{"id":0,"is_bot":false,"first_name":"FN","last_name":"LN","username":"UN"}, + "date":1644677726, + "old_chat_member":{"user":{"id":1,"is_bot":true,"first_name":"bot","username":"unBot"},"status":"member"}, + "new_chat_member":{"user":{"id":1,"is_bot":true,"first_name":"bot","username":"unBot"},"status":"kicked","until_date":0} + } + }"#; + + let Update { kind, .. } = serde_json::from_str(json).unwrap(); + + match kind { + UpdateKind::MyChatMember(_) => {} + _ => panic!("Expected `MyChatMember`"), + } + } +} diff --git a/crates/teloxide-core/src/types/user.rs b/crates/teloxide-core/src/types/user.rs new file mode 100644 index 00000000..1dc0b672 --- /dev/null +++ b/crates/teloxide-core/src/types/user.rs @@ -0,0 +1,195 @@ +use serde::{Deserialize, Serialize}; + +use crate::types::UserId; + +/// This object represents a Telegram user or bot. +/// +/// [The official docs](https://core.telegram.org/bots/api#user). +#[serde_with_macros::skip_serializing_none] +#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)] +pub struct User { + /// Unique identifier for this user or bot. + pub id: UserId, + + /// `true`, if this user is a bot. + pub is_bot: bool, + + /// User‘s or bot’s first name. + pub first_name: String, + + /// User‘s or bot’s last name. + pub last_name: Option, + + /// User‘s or bot’s username. + pub username: Option, + + /// [IETF language tag] of the user's language. + /// + /// [IETF language tag]: https://en.wikipedia.org/wiki/IETF_language_tag + pub language_code: Option, + + /// `true`, if this user is a Telegram Premium user. + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + pub is_premium: bool, + + /// `true`, if this user added the bot to the attachment menu. + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + pub added_to_attachment_menu: bool, +} + +impl User { + /// Returns full name of this user, ie first and last names joined with a + /// space. + #[must_use] + pub fn full_name(&self) -> String { + match &self.last_name { + Some(last_name) => format!("{0} {1}", self.first_name, last_name), + None => self.first_name.clone(), + } + } + + /// Returns a username mention of this user. Returns `None` if + /// `self.username.is_none()`. + #[must_use] + pub fn mention(&self) -> Option { + Some(format!("@{}", self.username.as_ref()?)) + } + + /// Returns an URL that links to this user in the form of + /// `tg://user/?id=<...>`. + #[must_use] + pub fn url(&self) -> reqwest::Url { + self.id.url() + } + + /// Returns an URL that links to this user in the form of `t.me/<...>`. + /// Returns `None` if `self.username.is_none()`. + #[must_use] + pub fn tme_url(&self) -> Option { + Some(format!("https://t.me/{}", self.username.as_ref()?).parse().unwrap()) + } + + /// Returns an URL that links to this user in the form of `t.me/<...>` or + /// `tg://user/?id=<...>`, preferring `t.me` one when possible. + #[must_use] + pub fn preferably_tme_url(&self) -> reqwest::Url { + self.tme_url().unwrap_or_else(|| self.url()) + } + + /// Returns `true` if this is the special user used by telegram bot API to + /// denote an anonymous user that sends messages on behalf of a group. + #[must_use] + pub fn is_anonymous(&self) -> bool { + // Sanity check + debug_assert!( + !self.id.is_anonymous() + || (self.is_bot + && self.first_name == "Group" + && self.last_name.is_none() + && self.username.as_deref() == Some("GroupAnonymousBot")) + ); + + self.id.is_anonymous() + } + + /// Returns `true` if this is the special user used by telegram bot API to + /// denote an anonymous user that sends messages on behalf of a channel. + #[must_use] + pub fn is_channel(&self) -> bool { + // Sanity check + debug_assert!( + !self.id.is_channel() + || (self.is_bot + && self.first_name == "Channel" + && self.last_name.is_none() + && self.username.as_deref() == Some("Channel_Bot")) + ); + + self.id.is_channel() + } + + /// Returns `true` if this is the special user used by telegram itself. + /// + /// It is sometimes also used as a fallback, for example when a channel post + /// is automatically forwarded to a group, bots in a group will get a + /// message where `from` is the Telegram user. + #[must_use] + pub fn is_telegram(&self) -> bool { + // Sanity check + debug_assert!( + !self.id.is_telegram() + || (!self.is_bot + && self.first_name == "Telegram" + && self.last_name.is_none() + && self.username.is_none()) + ); + + self.id.is_telegram() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn deserialize() { + let json = r#"{ + "id":12345, + "is_bot":false, + "first_name":"firstName", + "last_name":"lastName", + "username":"Username", + "language_code":"ru" + }"#; + let expected = User { + id: UserId(12345), + is_bot: false, + first_name: "firstName".to_string(), + last_name: Some("lastName".to_string()), + username: Some("Username".to_string()), + language_code: Some(String::from("ru")), + is_premium: false, + added_to_attachment_menu: false, + }; + let actual = serde_json::from_str::(json).unwrap(); + assert_eq!(actual, expected) + } + + #[test] + fn convenience_methods_work() { + let user_a = User { + id: UserId(43), + is_bot: false, + first_name: "First".to_owned(), + last_name: Some("Last".to_owned()), + username: Some("aaaaaaaaaaaaaaaa".to_owned()), + language_code: None, + is_premium: false, + added_to_attachment_menu: false, + }; + + let user_b = User { + id: UserId(44), + is_bot: false, + first_name: ".".to_owned(), + last_name: None, + username: None, + language_code: None, + is_premium: false, + added_to_attachment_menu: false, + }; + + assert_eq!(user_a.full_name(), "First Last"); + assert_eq!(user_b.full_name(), "."); + + assert_eq!(user_a.mention(), Some("@aaaaaaaaaaaaaaaa".to_owned())); + assert_eq!(user_b.mention(), None); + + assert_eq!(user_a.tme_url(), Some("https://t.me/aaaaaaaaaaaaaaaa".parse().unwrap())); + assert_eq!(user_b.tme_url(), None); + + assert_eq!(user_a.preferably_tme_url(), "https://t.me/aaaaaaaaaaaaaaaa".parse().unwrap()); + assert_eq!(user_b.preferably_tme_url(), "tg://user/?id=44".parse().unwrap()); + } +} diff --git a/crates/teloxide-core/src/types/user_id.rs b/crates/teloxide-core/src/types/user_id.rs new file mode 100644 index 00000000..71d41984 --- /dev/null +++ b/crates/teloxide-core/src/types/user_id.rs @@ -0,0 +1,82 @@ +use serde::{Deserialize, Serialize}; + +/// Identifier of a user. +#[derive(Clone, Copy)] +#[derive(Debug, derive_more::Display)] +#[derive(PartialEq, Eq, PartialOrd, Ord, Hash)] +#[derive(Serialize, Deserialize)] +#[serde(transparent)] +pub struct UserId(pub u64); + +impl UserId { + /// Returns an URL that links to the user with this id in the form of + /// `tg://user/?id=<...>`. + #[must_use] + pub fn url(self) -> reqwest::Url { + reqwest::Url::parse(&format!("tg://user/?id={}", self)).unwrap() + } + + /// Returns `true` if this is the id of the special user used by telegram + /// bot API to denote an anonymous user that sends messages on behalf of + /// a group. + #[must_use] + pub fn is_anonymous(self) -> bool { + // https://github.com/tdlib/td/blob/4791fb6a2af0257f6cad8396e10424a79ee5f768/td/telegram/ContactsManager.cpp#L4941-L4943 + const ANON_ID: UserId = UserId(1087968824); + + self == ANON_ID + } + + /// Returns `true` if this is the id of the special user used by telegram + /// bot API to denote an anonymous user that sends messages on behalf of + /// a channel. + #[must_use] + pub fn is_channel(self) -> bool { + // https://github.com/tdlib/td/blob/4791fb6a2af0257f6cad8396e10424a79ee5f768/td/telegram/ContactsManager.cpp#L4945-L4947 + const ANON_CHANNEL_ID: UserId = UserId(136817688); + + self == ANON_CHANNEL_ID + } + + /// Returns `true` if this is the id of the special user used by telegram + /// itself. + /// + /// It is sometimes also used as a fallback, for example when a channel post + /// is automatically forwarded to a group, bots in a group will get a + /// message where `from` is the Telegram user. + #[must_use] + pub fn is_telegram(self) -> bool { + const TELEGRAM_USER_ID: UserId = UserId(777000); + + self == TELEGRAM_USER_ID + } +} + +#[cfg(test)] +mod tests { + use serde::{Deserialize, Serialize}; + + use crate::types::UserId; + + /// Test that `UserId` is serialized as the underlying integer + #[test] + fn deser() { + let user_id = S { user_id: UserId(17) }; + let json = r#"{"user_id":17}"#; + + #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] + struct S { + user_id: UserId, + } + + assert_eq!(serde_json::to_string(&user_id).unwrap(), json); + assert_eq!(user_id, serde_json::from_str(json).unwrap()); + } + + #[test] + fn url_works() { + let id = UserId(17); + + assert_eq!(id.url(), "tg://user/?id=17".parse().unwrap()); + } +} diff --git a/crates/teloxide-core/src/types/user_profile_photos.rs b/crates/teloxide-core/src/types/user_profile_photos.rs new file mode 100644 index 00000000..ac0a9b5d --- /dev/null +++ b/crates/teloxide-core/src/types/user_profile_photos.rs @@ -0,0 +1,15 @@ +use serde::{Deserialize, Serialize}; + +use crate::types::PhotoSize; + +/// This object represent a user's profile pictures. +/// +/// [The official docs](https://core.telegram.org/bots/api#userprofilephotos). +#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)] +pub struct UserProfilePhotos { + /// Total number of profile pictures the target user has. + pub total_count: u32, + + /// Requested profile pictures (in up to 4 sizes each). + pub photos: Vec>, +} diff --git a/crates/teloxide-core/src/types/venue.rs b/crates/teloxide-core/src/types/venue.rs new file mode 100644 index 00000000..6915472d --- /dev/null +++ b/crates/teloxide-core/src/types/venue.rs @@ -0,0 +1,33 @@ +use serde::{Deserialize, Serialize}; + +use crate::types::Location; + +/// This object represents a venue. +#[serde_with_macros::skip_serializing_none] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct Venue { + /// Venue location. + pub location: Location, + + /// Name of the venue. + pub title: String, + + /// Address of the venue. + pub address: String, + + /// Foursquare identifier of the venue. + pub foursquare_id: Option, + + /// Foursquare type of the venue. (For example, + /// `arts_entertainment/default`, `arts_entertainment/aquarium` or + /// `food/icecream`.) + pub foursquare_type: Option, + + /// Google Places identifier of the venue. + pub google_place_id: Option, + + /// Google Places type of the venue. (See [supported types].) + /// + /// [supported types]: https://developers.google.com/places/web-service/supported_types + pub google_place_type: Option, +} diff --git a/crates/teloxide-core/src/types/video.rs b/crates/teloxide-core/src/types/video.rs new file mode 100644 index 00000000..e127167b --- /dev/null +++ b/crates/teloxide-core/src/types/video.rs @@ -0,0 +1,34 @@ +use mime::Mime; +use serde::{Deserialize, Serialize}; + +use crate::types::{FileMeta, PhotoSize}; + +/// This object represents a video file. +/// +/// [The official docs](https://core.telegram.org/bots/api#video). +#[serde_with_macros::skip_serializing_none] +#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)] +pub struct Video { + /// Metadata of the video file. + #[serde(flatten)] + pub file: FileMeta, + + /// Video width as defined by sender. + pub width: u32, + + /// Video height as defined by sender. + pub height: u32, + + /// Duration of the video in seconds as defined by sender. + pub duration: u32, + + /// Video thumbnail. + pub thumb: Option, + + /// Original filename as defined by sender + pub file_name: Option, + + /// Mime type of a file as defined by sender. + #[serde(with = "crate::types::non_telegram_types::mime::opt_deser")] + pub mime_type: Option, +} diff --git a/crates/teloxide-core/src/types/video_chat_ended.rs b/crates/teloxide-core/src/types/video_chat_ended.rs new file mode 100644 index 00000000..96775afd --- /dev/null +++ b/crates/teloxide-core/src/types/video_chat_ended.rs @@ -0,0 +1,6 @@ +use serde::{Deserialize, Serialize}; + +/// This object represents a service message about a video chat ended in the +/// chat. +#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)] +pub struct VideoChatEnded {} diff --git a/crates/teloxide-core/src/types/video_chat_participants_invited.rs b/crates/teloxide-core/src/types/video_chat_participants_invited.rs new file mode 100644 index 00000000..1a3f364b --- /dev/null +++ b/crates/teloxide-core/src/types/video_chat_participants_invited.rs @@ -0,0 +1,11 @@ +use serde::{Deserialize, Serialize}; + +use crate::types::User; + +/// This object represents a service message about new members invited to a +/// video chat. +#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)] +pub struct VideoChatParticipantsInvited { + /// New members that were invited to the video chat + pub users: Option>, +} diff --git a/crates/teloxide-core/src/types/video_chat_scheduled.rs b/crates/teloxide-core/src/types/video_chat_scheduled.rs new file mode 100644 index 00000000..674b76d4 --- /dev/null +++ b/crates/teloxide-core/src/types/video_chat_scheduled.rs @@ -0,0 +1,12 @@ +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; + +/// This object represents a service message about a video chat scheduled in the +/// chat. +#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)] +pub struct VideoChatScheduled { + /// Point in time when the video chat is supposed to be started by a chat + /// administrator. + #[serde(with = "crate::types::serde_date_from_unix_timestamp")] + pub start_date: DateTime, +} diff --git a/crates/teloxide-core/src/types/video_chat_started.rs b/crates/teloxide-core/src/types/video_chat_started.rs new file mode 100644 index 00000000..32796c9b --- /dev/null +++ b/crates/teloxide-core/src/types/video_chat_started.rs @@ -0,0 +1,6 @@ +use serde::{Deserialize, Serialize}; + +/// This object represents a service message about a video chat started in the +/// chat. Currently holds no information. +#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)] +pub struct VideoChatStarted {} diff --git a/crates/teloxide-core/src/types/video_note.rs b/crates/teloxide-core/src/types/video_note.rs new file mode 100644 index 00000000..d112c672 --- /dev/null +++ b/crates/teloxide-core/src/types/video_note.rs @@ -0,0 +1,28 @@ +use serde::{Deserialize, Serialize}; + +use crate::types::{FileMeta, PhotoSize}; + +/// This object represents a [video message] (available in Telegram apps as of +/// [v.4.0]). +/// +/// [The official docs](https://core.telegram.org/bots/api#videonote). +/// +/// [video message]: https://telegram.org/blog/video-messages-and-telescope +/// [v4.0]: https://telegram.org/blog/video-messages-and-telescope +#[serde_with_macros::skip_serializing_none] +#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)] +pub struct VideoNote { + /// Metadata of the video note file. + #[serde(flatten)] + pub file: FileMeta, + + /// Video width and height (diameter of the video message) as defined by + /// sender. + pub length: u32, + + /// Duration of the video in seconds as defined by sender. + pub duration: u32, + + /// Video thumbnail. + pub thumb: Option, +} diff --git a/crates/teloxide-core/src/types/voice.rs b/crates/teloxide-core/src/types/voice.rs new file mode 100644 index 00000000..0dbcd7b8 --- /dev/null +++ b/crates/teloxide-core/src/types/voice.rs @@ -0,0 +1,22 @@ +use mime::Mime; +use serde::{Deserialize, Serialize}; + +use crate::types::FileMeta; + +/// This object represents a voice note. +/// +/// [The official docs](https://core.telegram.org/bots/api#voice). +#[serde_with_macros::skip_serializing_none] +#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)] +pub struct Voice { + /// Metadata of the voice file. + #[serde(flatten)] + pub file: FileMeta, + + /// Duration of the audio in seconds as defined by sender. + pub duration: u32, + + /// MIME type of the file as defined by sender. + #[serde(with = "crate::types::non_telegram_types::mime::opt_deser")] + pub mime_type: Option, +} diff --git a/crates/teloxide-core/src/types/web_app_data.rs b/crates/teloxide-core/src/types/web_app_data.rs new file mode 100644 index 00000000..d287168c --- /dev/null +++ b/crates/teloxide-core/src/types/web_app_data.rs @@ -0,0 +1,13 @@ +use serde::{Deserialize, Serialize}; + +/// Contains data sent from a Web App to the bot. +#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)] +pub struct WebAppData { + /// The data. Be aware that a bad client can send arbitrary data in this + /// field. + pub data: String, + + /// Text of the web_app keyboard button, from which the Web App was opened. + /// Be aware that a bad client can send arbitrary data in this field. + pub button_text: String, +} diff --git a/crates/teloxide-core/src/types/web_app_info.rs b/crates/teloxide-core/src/types/web_app_info.rs new file mode 100644 index 00000000..946f6181 --- /dev/null +++ b/crates/teloxide-core/src/types/web_app_info.rs @@ -0,0 +1,14 @@ +use serde::{Deserialize, Serialize}; +use url::Url; + +/// Contains information about a [Web App]. +/// +/// [Web App]: https://core.telegram.org/bots/webapps +#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)] +pub struct WebAppInfo { + /// An HTTPS URL of a Web App to be opened with additional data as specified + /// in [Initializing Web Apps]. + /// + /// [Initializing Web Apps]: https://core.telegram.org/bots/webapps#initializing-web-apps + pub url: Url, +} diff --git a/crates/teloxide-core/src/types/webhook_info.rs b/crates/teloxide-core/src/types/webhook_info.rs new file mode 100644 index 00000000..8fef2259 --- /dev/null +++ b/crates/teloxide-core/src/types/webhook_info.rs @@ -0,0 +1,72 @@ +use std::net::IpAddr; + +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; + +use crate::types::AllowedUpdate; + +/// Contains information about the current status of a webhook. +/// +/// [The official docs](https://core.telegram.org/bots/api#webhookinfo). +#[serde_with_macros::skip_serializing_none] +#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)] +pub struct WebhookInfo { + /// Webhook URL, `None` if webhook is not set up. + #[serde(with = "crate::types::option_url_from_string")] + pub url: Option, + + /// `true`, if a custom certificate was provided for webhook certificate + /// checks. + pub has_custom_certificate: bool, + + /// Number of updates awaiting delivery. + pub pending_update_count: u32, + + /// Currently used webhook IP address. + pub ip_address: Option, + + /// Time of the most recent error that happened when trying to + /// deliver an update via webhook. + #[serde(default, with = "crate::types::serde_opt_date_from_unix_timestamp")] + pub last_error_date: Option>, + + /// Error message in human-readable format for the most recent error that + /// happened when trying to deliver an update via webhook. + pub last_error_message: Option, + + /// Time of the most recent error that happened when trying to synchronize + /// available updates with Telegram data-centers. + #[serde(default, with = "crate::types::serde_opt_date_from_unix_timestamp")] + pub last_synchronization_error_date: Option>, + + /// Maximum allowed number of simultaneous HTTPS connections to the webhook + /// for update delivery. + pub max_connections: Option, + + /// A list of update types the bot is subscribed to. Defaults to all update + /// types. + pub allowed_updates: Option>, +} + +// Regression test for +#[test] +fn empty_url() { + let json = r#"{"url":"","has_custom_certificate":false,"pending_update_count":0,"allowed_updates":["message"]}"#; + let actual: WebhookInfo = serde_json::from_str(json).unwrap(); + let expected = WebhookInfo { + url: None, + has_custom_certificate: false, + pending_update_count: 0, + ip_address: None, + last_error_date: None, + last_error_message: None, + last_synchronization_error_date: None, + max_connections: None, + allowed_updates: Some(vec![AllowedUpdate::Message]), + }; + + assert_eq!(actual, expected); + + let json = r#"{"ok":true,"result":{"url":"","has_custom_certificate":false,"pending_update_count":0,"allowed_updates":["message"]}}"#; + serde_json::from_str::>(json).unwrap(); +} diff --git a/crates/teloxide-macros/CHANGELOG.md b/crates/teloxide-macros/CHANGELOG.md new file mode 100644 index 00000000..34b61e81 --- /dev/null +++ b/crates/teloxide-macros/CHANGELOG.md @@ -0,0 +1,144 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## unreleased + +## 0.7.0 - 2022-10-06 + +### Removed + +- `derive(DialogueState)` macro + +### Changed + +- `#[command(rename = "...")]` now always renames to `"..."`; to rename multiple commands using the same pattern, use `#[command(rename_rule = "snake_case")]` and the like. +- `#[command(parse_with = ...)]` now requires a path, instead of a string, when specifying custom parsers. + +### Fixed + +- `#[derive(BotCommands)]` even if the trait is not imported ([issue #717](https://github.com/teloxide/teloxide/issues/717)). + +## 0.6.3 - 2022-07-19 + +### Fixed + + - Allow specifying a path to a command handler in `parse_with` ([PR #27](https://github.com/teloxide/teloxide-macros/pull/27)). + +## 0.6.2 - 2022-05-27 + +### Fixed + + - Fix `#[command(rename = "...")]` for custom command names ([issue 633](https://github.com/teloxide/teloxide/issues/633)). + +## 0.6.1 - 2022-04-26 + +### Fixed + + - Fix `#[derive(DialogueState)]` (function return type `dptree::Handler`). + +## 0.6.0 - 2022-04-09 + +### Removed + + - Support for the old dispatching: `#[teloxide(subtransition)]` [**BC**]. + +### Deprecated + + - `#[derive(DialogueState)]` in favour of `teloxide::handler!`. + +## 0.5.1 - 2022-03-23 + +### Fixed + + - Make bot name check case-insensitive ([PR #16](https://github.com/teloxide/teloxide-macros/pull/16)). + +### Added + + - More command rename rules: `UPPERCASE`, `PascalCase`, `camelCase`, `snake_case`, `SCREAMING_SNAKE_CASE`, `kebab-case`, and `SCREAMING-KEBAB-CASE` ([PR #18](https://github.com/teloxide/teloxide-macros/pull/18)). + +## 0.5.0 - 2022-02-05 + +### Added + +- The `BotCommand::bot_commands()` method that returns `Vec` ([PR #13](https://github.com/teloxide/teloxide-macros/pull/13)). +- `#[derive(DialogueState)]`, `#[handler_out(...)]`, `#[handler(...)]`. + +## 0.4.1 - 2021-07-11 + +### Fixed + + - Fix generics support for a variant's arguments ([PR #8](https://github.com/teloxide/teloxide-macros/issues/8)). + +## 0.4.0 - 2021-03-19 + +### Changed + + - Adjust dialogues with the latest teloxide (v0.4.0). + +## 0.3.2 - 2020-07-27 + +### Added + - `#[derive(Transition)]` with `#[teloxide(subtransition)]`. + +### Removed + - The `dev` branch. + +## 0.3.1 - 2020-07-04 + +### Added + - Now you can remove command from showing in descriptions by defining `description` attribute as `"off"`. + +## 0.3.0 - 2020-07-03 + +### Changed + - The description in `Cargo.toml` was changed to from "The teloxide's macros for internal usage" to "The teloxide's procedural macros". + - Now parsing of arguments happens using special function. There are 3 possible variants: + - Using `default` parser, which only put all text in one String field. + - Using `split` parser, which split all text by `separator` (by default is whitespace) and then use FromStr::from_str to construct value. + - Using custom separator. + - Now function `parse` return Result instead of Option. + +### Added + - This `CHANGELOG.md`. + - `.gitignore`. + - `#[parse_with]` attribute. + - `#[separator='%sep%']` attribute. + +## 0.2.1 - 2020-02-25 + +### Changed + - The description in `Cargo.toml` was changed to from "The teloxide's macros for internal usage" to "The teloxide's procedural macros". + +### Added + - This `CHANGELOG.md`. + - `.gitignore`. + - The functionality to parse commands only with a correct bot's name (breaks backwards compatibility). + +## 0.1.2 - 2020-02-24 + +### Changed + - The same as v0.1.1, but fixes [the issue](https://github.com/teloxide/teloxide/issues/176) about backwards compatibility. + + +## 0.2.0 - YANKED + +### Changed + - Fixes [the issue](https://github.com/teloxide/teloxide/issues/176) about backwards compatibility, but fairly soon I realised that semver recommends to use v0.1.2 instead. + + +## 0.1.1 - 2020-02-23 + +### Added + - The `LICENSE` file. + +### Changed + - Backwards compatibility is broken and was fixed in v0.1.2. + + +## 0.1.0 - 2020-02-19 + +### Added + - This project. diff --git a/crates/teloxide-macros/Cargo.toml b/crates/teloxide-macros/Cargo.toml new file mode 100644 index 00000000..c6ea1d39 --- /dev/null +++ b/crates/teloxide-macros/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "teloxide-macros" +version = "0.7.0" +description = "The teloxide's procedural macros" +license = "MIT" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +proc-macro = true + +[dependencies] +quote = "1.0.7" +proc-macro2 = "1.0.19" +syn = { version = "1.0.13", features = ["full"] } +heck = "0.4.0" diff --git a/crates/teloxide-macros/LICENSE b/crates/teloxide-macros/LICENSE new file mode 120000 index 00000000..30cff740 --- /dev/null +++ b/crates/teloxide-macros/LICENSE @@ -0,0 +1 @@ +../../LICENSE \ No newline at end of file diff --git a/crates/teloxide-macros/src/attr.rs b/crates/teloxide-macros/src/attr.rs new file mode 100644 index 00000000..9f872946 --- /dev/null +++ b/crates/teloxide-macros/src/attr.rs @@ -0,0 +1,145 @@ +use crate::{error::compile_error_at, Result}; + +use proc_macro2::Span; +use syn::{ + parse::{Parse, ParseBuffer, ParseStream}, + spanned::Spanned, + Attribute, Ident, Lit, Path, Token, +}; + +pub(crate) fn fold_attrs( + attrs: &[Attribute], + filter: fn(&Attribute) -> bool, + parse: impl Fn(Attr) -> Result, + init: A, + f: impl Fn(A, R) -> Result, +) -> Result { + attrs + .iter() + .filter(|&a| filter(a)) + .flat_map(|attribute| { + // FIXME: don't allocate here + let attrs = match attribute.parse_args_with(|input: &ParseBuffer| { + input.parse_terminated::<_, Token![,]>(Attr::parse) + }) { + Ok(ok) => ok, + Err(err) => return vec![Err(err.into())], + }; + + attrs.into_iter().map(&parse).collect() + }) + .try_fold(init, |acc, r| r.and_then(|r| f(acc, r))) +} + +/// An attribute key-value pair. +/// +/// For example: +/// ```text +/// #[blahblah(key = "puff", value = 12, nope)] +/// ^^^^^^^^^^^^ ^^^^^^^^^^ ^^^^ +/// ``` +pub(crate) struct Attr { + pub key: Ident, + pub value: AttrValue, +} + +/// Value of an attribute. +/// +/// For example: +/// ```text +/// #[blahblah(key = "puff", value = 12, nope)] +/// ^^^^^^ ^^ ^-- (None pseudo-value) +/// ``` +pub(crate) enum AttrValue { + Path(Path), + Lit(Lit), + None(Span), +} + +impl Parse for Attr { + fn parse(input: ParseStream) -> syn::Result { + let key = input.parse::()?; + + let value = match input.peek(Token![=]) { + true => { + input.parse::()?; + input.parse::()? + } + false => AttrValue::None(input.span()), + }; + + Ok(Self { key, value }) + } +} + +impl Attr { + pub(crate) fn span(&self) -> Span { + self.key.span().join(self.value.span()).unwrap_or_else(|| self.key.span()) + } +} + +impl AttrValue { + /// Unwraps this value if it's a string literal. + pub fn expect_string(self) -> Result { + self.expect("a string", |this| match this { + AttrValue::Lit(Lit::Str(s)) => Ok(s.value()), + _ => Err(this), + }) + } + + // /// Unwraps this value if it's a path. + // pub fn expect_path(self) -> Result { + // self.expect("a path", |this| match this { + // AttrValue::Path(p) => Ok(p), + // _ => Err(this), + // }) + // } + + pub fn expect(self, expected: &str, f: impl FnOnce(Self) -> Result) -> Result { + f(self).map_err(|this| { + compile_error_at(&format!("expected {expected}, found {}", this.descr()), this.span()) + }) + } + + fn descr(&self) -> &'static str { + use Lit::*; + + match self { + Self::None(_) => "nothing", + Self::Lit(l) => match l { + Str(_) | ByteStr(_) => "a string", + Char(_) => "a character", + Byte(_) | Int(_) => "an integer", + Float(_) => "a floating point integer", + Bool(_) => "a boolean", + Verbatim(_) => ":shrug:", + }, + Self::Path(_) => "a path", + } + } + + /// Returns span of the value + /// + /// ```text + /// #[blahblah(key = "puff", value = 12, nope )] + /// ^^^^^^ ^^ ^ + /// ``` + fn span(&self) -> Span { + match self { + Self::Path(p) => p.span(), + Self::Lit(l) => l.span(), + Self::None(sp) => *sp, + } + } +} + +impl Parse for AttrValue { + fn parse(input: ParseStream) -> syn::Result { + let this = match input.peek(Lit) { + true => Self::Lit(input.parse()?), + false => Self::Path(input.parse()?), + }; + + Ok(this) + } +} diff --git a/crates/teloxide-macros/src/bot_commands.rs b/crates/teloxide-macros/src/bot_commands.rs new file mode 100644 index 00000000..b257d668 --- /dev/null +++ b/crates/teloxide-macros/src/bot_commands.rs @@ -0,0 +1,130 @@ +use crate::{ + command::Command, command_enum::CommandEnum, compile_error, fields_parse::impl_parse_args, + unzip::Unzip, Result, +}; + +use proc_macro2::TokenStream; +use quote::quote; +use syn::DeriveInput; + +pub(crate) fn bot_commands_impl(input: DeriveInput) -> Result { + let data_enum = get_enum_data(&input)?; + let command_enum = CommandEnum::from_attributes(&input.attrs)?; + + let Unzip(var_init, var_info) = data_enum + .variants + .iter() + .map(|variant| { + let command = Command::new(&variant.ident.to_string(), &variant.attrs, &command_enum)?; + + let variant_name = &variant.ident; + let self_variant = quote! { Self::#variant_name }; + + let parse = impl_parse_args(&variant.fields, self_variant, &command.parser); + + Ok((parse, command)) + }) + .collect::, Vec<_>>>>()?; + + let type_name = &input.ident; + let fn_descriptions = impl_descriptions(&var_info, &command_enum); + let fn_parse = impl_parse(&var_info, &var_init); + let fn_commands = impl_commands(&var_info); + + let trait_impl = quote! { + impl teloxide::utils::command::BotCommands for #type_name { + #fn_descriptions + #fn_parse + #fn_commands + } + }; + + Ok(trait_impl) +} + +fn impl_commands(infos: &[Command]) -> proc_macro2::TokenStream { + let commands = infos.iter().filter(|command| command.description_is_enabled()).map(|command| { + let c = command.get_prefixed_command(); + let d = command.description.as_deref().unwrap_or_default(); + quote! { BotCommand::new(#c,#d) } + }); + + quote! { + fn bot_commands() -> Vec { + use teloxide::types::BotCommand; + vec![#(#commands),*] + } + } +} + +fn impl_descriptions(infos: &[Command], global: &CommandEnum) -> proc_macro2::TokenStream { + let command_descriptions = infos + .iter() + .filter(|command| command.description_is_enabled()) + .map(|Command { prefix, name, description, ..}| { + let description = description.clone().unwrap_or_default(); + quote! { CommandDescription { prefix: #prefix, command: #name, description: #description } } + }); + + let global_description = match global.description.as_deref() { + Some(gd) => quote! { .global_description(#gd) }, + None => quote! {}, + }; + + quote! { + fn descriptions() -> teloxide::utils::command::CommandDescriptions<'static> { + use teloxide::utils::command::{CommandDescriptions, CommandDescription}; + use std::borrow::Cow; + + CommandDescriptions::new(&[ + #(#command_descriptions),* + ]) + #global_description + } + } +} + +fn impl_parse( + infos: &[Command], + variants_initialization: &[proc_macro2::TokenStream], +) -> proc_macro2::TokenStream { + let matching_values = infos.iter().map(|c| c.get_prefixed_command()); + + quote! { + fn parse(s: &str, bot_name: &str) -> Result { + // FIXME: we should probably just call a helper function from `teloxide`, instead of parsing command syntax ourselves + use std::str::FromStr; + use teloxide::utils::command::ParseError; + + // 2 is used to only split once (=> in two parts), + // we only need to split the command and the rest of arguments. + let mut words = s.splitn(2, ' '); + + // Unwrap: split iterators always have at least one item + let mut full_command = words.next().unwrap().split('@'); + let command = full_command.next().unwrap(); + + let bot_username = full_command.next(); + match bot_username { + None => {} + Some(username) if username.eq_ignore_ascii_case(bot_name) => {} + Some(n) => return Err(ParseError::WrongBotName(n.to_owned())), + } + + let args = words.next().unwrap_or("").to_owned(); + match command { + #( + #matching_values => Ok(#variants_initialization), + )* + _ => Err(ParseError::UnknownCommand(command.to_owned())), + } + } + } +} + +fn get_enum_data(input: &DeriveInput) -> Result<&syn::DataEnum> { + match &input.data { + syn::Data::Enum(data) => Ok(data), + _ => Err(compile_error("`BotCommands` is only allowed for enums")), + } +} diff --git a/crates/teloxide-macros/src/command.rs b/crates/teloxide-macros/src/command.rs new file mode 100644 index 00000000..fb421e8f --- /dev/null +++ b/crates/teloxide-macros/src/command.rs @@ -0,0 +1,61 @@ +use crate::{ + command_attr::CommandAttrs, command_enum::CommandEnum, error::compile_error_at, + fields_parse::ParserType, Result, +}; + +pub(crate) struct Command { + /// Prefix of this command, for example "/". + pub prefix: String, + /// Description for the command. + pub description: Option, + /// Name of the command, with all renames already applied. + pub name: String, + /// Parser for arguments of this command. + pub parser: ParserType, +} + +impl Command { + pub fn new( + name: &str, + attributes: &[syn::Attribute], + global_options: &CommandEnum, + ) -> Result { + let attrs = CommandAttrs::from_attributes(attributes)?; + let CommandAttrs { + prefix, + description, + rename_rule, + rename, + parser, + // FIXME: error on/do not ignore separator + separator: _, + } = attrs; + + let name = match (rename, rename_rule) { + (Some((rename, _)), None) => rename, + (Some(_), Some((_, sp))) => { + return Err(compile_error_at( + "`rename_rule` can't be applied to `rename`-d variant", + sp, + )) + } + (None, Some((rule, _))) => rule.apply(name), + (None, None) => global_options.rename_rule.apply(name), + }; + + let prefix = prefix.map(|(p, _)| p).unwrap_or_else(|| global_options.prefix.clone()); + let description = description.map(|(d, _)| d); + let parser = parser.map(|(p, _)| p).unwrap_or_else(|| global_options.parser_type.clone()); + + Ok(Self { prefix, description, parser, name }) + } + + pub fn get_prefixed_command(&self) -> String { + let Self { prefix, name, .. } = self; + format!("{prefix}{name}") + } + + pub(crate) fn description_is_enabled(&self) -> bool { + self.description != Some("off".to_owned()) + } +} diff --git a/crates/teloxide-macros/src/command_attr.rs b/crates/teloxide-macros/src/command_attr.rs new file mode 100644 index 00000000..96c9c2b7 --- /dev/null +++ b/crates/teloxide-macros/src/command_attr.rs @@ -0,0 +1,121 @@ +use crate::{ + attr::{fold_attrs, Attr}, + error::compile_error_at, + fields_parse::ParserType, + rename_rules::RenameRule, + Result, +}; + +use proc_macro2::Span; +use syn::Attribute; + +/// All attributes that can be used for `derive(BotCommands)` +pub(crate) struct CommandAttrs { + pub prefix: Option<(String, Span)>, + pub description: Option<(String, Span)>, + pub rename_rule: Option<(RenameRule, Span)>, + pub rename: Option<(String, Span)>, + pub parser: Option<(ParserType, Span)>, + pub separator: Option<(String, Span)>, +} + +/// A single k/v attribute for `BotCommands` derive macro. +/// +/// For example: +/// ```text +/// #[command(prefix = "!", rename_rule = "snake_case")] +/// /^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^---- CommandAttr { kind: RenameRule(SnakeCase) } +/// | +/// CommandAttr { kind: Prefix("!") } +/// ``` +struct CommandAttr { + kind: CommandAttrKind, + sp: Span, +} + +/// Kind of [`CommandAttr`]. +enum CommandAttrKind { + Prefix(String), + Description(String), + RenameRule(RenameRule), + Rename(String), + ParseWith(ParserType), + Separator(String), +} + +impl CommandAttrs { + pub fn from_attributes(attributes: &[Attribute]) -> Result { + use CommandAttrKind::*; + + fold_attrs( + attributes, + is_command_attribute, + CommandAttr::parse, + Self { + prefix: None, + description: None, + rename_rule: None, + rename: None, + parser: None, + separator: None, + }, + |mut this, attr| { + fn insert(opt: &mut Option<(T, Span)>, x: T, sp: Span) -> Result<()> { + match opt { + slot @ None => { + *slot = Some((x, sp)); + Ok(()) + } + Some(_) => Err(compile_error_at("duplicate attribute", sp)), + } + } + + match attr.kind { + Prefix(p) => insert(&mut this.prefix, p, attr.sp), + Description(d) => insert(&mut this.description, d, attr.sp), + RenameRule(r) => insert(&mut this.rename_rule, r, attr.sp), + Rename(r) => insert(&mut this.rename, r, attr.sp), + ParseWith(p) => insert(&mut this.parser, p, attr.sp), + Separator(s) => insert(&mut this.separator, s, attr.sp), + }?; + + Ok(this) + }, + ) + } +} + +impl CommandAttr { + fn parse(attr: Attr) -> Result { + use CommandAttrKind::*; + + let sp = attr.span(); + let Attr { key, value } = attr; + let kind = match &*key.to_string() { + "prefix" => Prefix(value.expect_string()?), + "description" => Description(value.expect_string()?), + "rename_rule" => { + RenameRule(value.expect_string().and_then(|r| self::RenameRule::parse(&r))?) + } + "rename" => Rename(value.expect_string()?), + "parse_with" => ParseWith(ParserType::parse(value)?), + "separator" => Separator(value.expect_string()?), + _ => { + return Err(compile_error_at( + "unexpected attribute name (expected one of `prefix`, `description`, \ + `rename`, `parse_with` and `separator`", + key.span(), + )) + } + }; + + Ok(Self { kind, sp }) + } +} + +fn is_command_attribute(a: &Attribute) -> bool { + match a.path.get_ident() { + Some(ident) => ident == "command", + _ => false, + } +} diff --git a/crates/teloxide-macros/src/command_enum.rs b/crates/teloxide-macros/src/command_enum.rs new file mode 100644 index 00000000..83f20dec --- /dev/null +++ b/crates/teloxide-macros/src/command_enum.rs @@ -0,0 +1,39 @@ +use crate::{ + command_attr::CommandAttrs, error::compile_error_at, fields_parse::ParserType, + rename_rules::RenameRule, Result, +}; + +pub(crate) struct CommandEnum { + pub prefix: String, + pub description: Option, + pub rename_rule: RenameRule, + pub parser_type: ParserType, +} + +impl CommandEnum { + pub fn from_attributes(attributes: &[syn::Attribute]) -> Result { + let attrs = CommandAttrs::from_attributes(attributes)?; + let CommandAttrs { prefix, description, rename_rule, rename, parser, separator } = attrs; + + if let Some((_rename, sp)) = rename { + return Err(compile_error_at( + "`rename` attribute can only be applied to enums *variants*", + sp, + )); + } + + let mut parser = parser.map(|(p, _)| p).unwrap_or(ParserType::Default); + + // FIXME: Error on unused separator + if let (ParserType::Split { separator }, Some((s, _))) = (&mut parser, &separator) { + *separator = Some(s.clone()) + } + + Ok(Self { + prefix: prefix.map(|(p, _)| p).unwrap_or_else(|| "/".to_owned()), + description: description.map(|(d, _)| d), + rename_rule: rename_rule.map(|(rr, _)| rr).unwrap_or(RenameRule::Identity), + parser_type: parser, + }) + } +} diff --git a/crates/teloxide-macros/src/error.rs b/crates/teloxide-macros/src/error.rs new file mode 100644 index 00000000..59088db2 --- /dev/null +++ b/crates/teloxide-macros/src/error.rs @@ -0,0 +1,52 @@ +use proc_macro2::{Span, TokenStream}; +use quote::{quote, ToTokens}; + +pub(crate) type Result = std::result::Result; + +#[derive(Debug)] +pub(crate) struct Error(TokenStream); + +pub(crate) fn compile_error(data: T) -> Error +where + T: ToTokens, +{ + Error(quote! { compile_error! { #data } }) +} + +pub(crate) fn compile_error_at(msg: &str, sp: Span) -> Error { + use proc_macro2::{Delimiter, Group, Ident, Literal, Punct, Spacing, TokenTree}; + // compile_error! { $msg } + let ts = TokenStream::from_iter(vec![ + TokenTree::Ident(Ident::new("compile_error", sp)), + TokenTree::Punct({ + let mut punct = Punct::new('!', Spacing::Alone); + punct.set_span(sp); + punct + }), + TokenTree::Group({ + let mut group = Group::new(Delimiter::Brace, { + TokenStream::from_iter(vec![TokenTree::Literal({ + let mut string = Literal::string(msg); + string.set_span(sp); + string + })]) + }); + group.set_span(sp); + group + }), + ]); + + Error(ts) +} + +impl From for proc_macro2::TokenStream { + fn from(Error(e): Error) -> Self { + e + } +} + +impl From for Error { + fn from(e: syn::Error) -> Self { + Self(e.to_compile_error()) + } +} diff --git a/crates/teloxide-macros/src/fields_parse.rs b/crates/teloxide-macros/src/fields_parse.rs new file mode 100644 index 00000000..ba7204d7 --- /dev/null +++ b/crates/teloxide-macros/src/fields_parse.rs @@ -0,0 +1,154 @@ +use quote::quote; +use syn::{Fields, FieldsNamed, FieldsUnnamed, Type}; + +use crate::{attr::AttrValue, error::Result}; + +#[derive(Clone)] +pub(crate) enum ParserType { + Default, + Split { separator: Option }, + Custom(syn::Path), +} + +impl ParserType { + pub fn parse(value: AttrValue) -> Result { + value.expect(r#""default", "split", or a path to a custom parser function"#, |v| match v { + AttrValue::Path(p) => Ok(ParserType::Custom(p)), + AttrValue::Lit(syn::Lit::Str(ref l)) => match &*l.value() { + "default" => Ok(ParserType::Default), + "split" => Ok(ParserType::Split { separator: None }), + _ => Err(v), + }, + _ => Err(v), + }) + } +} + +pub(crate) fn impl_parse_args( + fields: &Fields, + self_variant: proc_macro2::TokenStream, + parser: &ParserType, +) -> proc_macro2::TokenStream { + match fields { + Fields::Unit => self_variant, + Fields::Unnamed(fields) => impl_parse_args_unnamed(fields, self_variant, parser), + Fields::Named(named) => impl_parse_args_named(named, self_variant, parser), + } +} + +pub(crate) fn impl_parse_args_unnamed( + data: &FieldsUnnamed, + variant: proc_macro2::TokenStream, + parser_type: &ParserType, +) -> proc_macro2::TokenStream { + let get_arguments = create_parser(parser_type, data.unnamed.iter().map(|f| &f.ty)); + let iter = (0..data.unnamed.len()).map(syn::Index::from); + let mut initialization = quote! {}; + for i in iter { + initialization.extend(quote! { arguments.#i, }) + } + let res = quote! { + { + #get_arguments + #variant(#initialization) + } + }; + res +} + +pub(crate) fn impl_parse_args_named( + data: &FieldsNamed, + variant: proc_macro2::TokenStream, + parser_type: &ParserType, +) -> proc_macro2::TokenStream { + let get_arguments = create_parser(parser_type, data.named.iter().map(|f| &f.ty)); + let i = (0..).map(syn::Index::from); + let name = data.named.iter().map(|f| f.ident.as_ref().unwrap()); + let res = quote! { + { + #get_arguments + #variant { #(#name: arguments.#i),* } + } + }; + res +} + +fn create_parser<'a>( + parser_type: &ParserType, + mut types: impl ExactSizeIterator, +) -> proc_macro2::TokenStream { + let function_to_parse = match parser_type { + ParserType::Default => match types.len() { + 1 => { + let ty = types.next().unwrap(); + quote! { + ( + |s: String| { + let res = <#ty>::from_str(&s) + .map_err(|e| ParseError::IncorrectFormat(e.into()))?; + + Ok((res,)) + } + ) + } + } + _ => { + quote! { compile_error!("Default parser works only with exactly 1 field") } + } + }, + ParserType::Split { separator } => { + parser_with_separator(&separator.clone().unwrap_or_else(|| " ".to_owned()), types) + } + ParserType::Custom(path) => quote! { #path }, + }; + + quote! { + let arguments = #function_to_parse(args)?; + } +} + +fn parser_with_separator<'a>( + separator: &str, + types: impl ExactSizeIterator, +) -> proc_macro2::TokenStream { + let expected = types.len(); + let res = { + let found = 0usize..; + quote! { + ( + #( + { + let s = splitted.next().ok_or(ParseError::TooFewArguments { + expected: #expected, + found: #found, + message: format!("Expected but not found arg number {}", #found + 1), + })?; + + <#types>::from_str(s).map_err(|e| ParseError::IncorrectFormat(e.into()))? + } + ),* + ) + } + }; + + let res = quote! { + ( + |s: String| { + let mut splitted = s.split(#separator); + + let res = #res; + + match splitted.next() { + Some(d) => Err(ParseError::TooManyArguments { + expected: #expected, + found: #expected + 1, + message: format!("Excess argument: {}", d), + }), + None => Ok(res) + } + } + ) + }; + + res +} diff --git a/crates/teloxide-macros/src/lib.rs b/crates/teloxide-macros/src/lib.rs new file mode 100644 index 00000000..4d886239 --- /dev/null +++ b/crates/teloxide-macros/src/lib.rs @@ -0,0 +1,24 @@ +extern crate proc_macro; + +mod attr; +mod bot_commands; +mod command; +mod command_attr; +mod command_enum; +mod error; +mod fields_parse; +mod rename_rules; +mod unzip; + +pub(crate) use error::{compile_error, Result}; +use syn::{parse_macro_input, DeriveInput}; + +use crate::bot_commands::bot_commands_impl; +use proc_macro::TokenStream; + +#[proc_macro_derive(BotCommands, attributes(command))] +pub fn bot_commands_derive(tokens: TokenStream) -> TokenStream { + let input = parse_macro_input!(tokens as DeriveInput); + + bot_commands_impl(input).unwrap_or_else(<_>::into).into() +} diff --git a/crates/teloxide-macros/src/rename_rules.rs b/crates/teloxide-macros/src/rename_rules.rs new file mode 100644 index 00000000..b4fab615 --- /dev/null +++ b/crates/teloxide-macros/src/rename_rules.rs @@ -0,0 +1,169 @@ +// Some concepts are from Serde. + +use crate::error::{compile_error, Result}; + +use heck::{ + ToKebabCase, ToLowerCamelCase, ToPascalCase, ToShoutyKebabCase, ToShoutySnakeCase, ToSnakeCase, +}; + +#[derive(Copy, Clone, Debug)] +pub(crate) enum RenameRule { + /// -> `lowercase` + LowerCase, + /// -> `UPPERCASE` + UpperCase, + /// -> `PascalCase` + PascalCase, + /// -> `camelCase` + CamelCase, + /// -> `snake_case` + SnakeCase, + /// -> `SCREAMING_SNAKE_CASE` + ScreamingSnakeCase, + /// -> `kebab-case` + KebabCase, + /// -> `SCREAMING-KEBAB-CASE` + ScreamingKebabCase, + /// Leaves input as-is + Identity, +} + +impl RenameRule { + /// Apply a renaming rule to a string, returning the version expected in the + /// source. + /// + /// See tests for the details how it will work. + pub fn apply(self, input: &str) -> String { + use RenameRule::*; + + match self { + LowerCase => input.to_lowercase(), + UpperCase => input.to_uppercase(), + PascalCase => input.to_pascal_case(), + CamelCase => input.to_lower_camel_case(), + SnakeCase => input.to_snake_case(), + ScreamingSnakeCase => input.to_shouty_snake_case(), + KebabCase => input.to_kebab_case(), + ScreamingKebabCase => input.to_shouty_kebab_case(), + Identity => input.to_owned(), + } + } + + pub fn parse(rule: &str) -> Result { + use RenameRule::*; + + let rule = match rule { + "lowercase" => LowerCase, + "UPPERCASE" => UpperCase, + "PascalCase" => PascalCase, + "camelCase" => CamelCase, + "snake_case" => SnakeCase, + "SCREAMING_SNAKE_CASE" => ScreamingSnakeCase, + "kebab-case" => KebabCase, + "SCREAMING-KEBAB-CASE" => ScreamingKebabCase, + "identity" => Identity, + invalid => { + return Err(compile_error(format!( + "invalid rename rule `{invalid}` (supported rules: `lowercase`, `UPPERCASE`, \ + `PascalCase`, `camelCase`, `snake_case`, `SCREAMING_SNAKE_CASE`, \ + `kebab-case`, `SCREAMING-KEBAB-CASE` and `identity`)" + ))) + } + }; + + Ok(rule) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + macro_rules! test_eq { + ($input:expr => $output:expr) => { + let rule = RenameRule::parse(TYPE).unwrap(); + + assert_eq!(rule.apply($input), $output); + }; + } + + #[test] + fn test_lowercase() { + const TYPE: &str = "lowercase"; + + test_eq!("HelloWorld" => "helloworld"); + test_eq!("Hello_World" => "hello_world"); + test_eq!("Hello-World" => "hello-world"); + test_eq!("helloWorld" => "helloworld"); + } + + #[test] + fn test_uppercase() { + const TYPE: &str = "UPPERCASE"; + + test_eq!("HelloWorld" => "HELLOWORLD"); + test_eq!("Hello_World" => "HELLO_WORLD"); + test_eq!("Hello-World" => "HELLO-WORLD"); + test_eq!("helloWorld" => "HELLOWORLD"); + } + + #[test] + fn test_pascalcase() { + const TYPE: &str = "PascalCase"; + + test_eq!("HelloWorld" => "HelloWorld"); + test_eq!("Hello_World" => "HelloWorld"); + test_eq!("Hello-World" => "HelloWorld"); + test_eq!("helloWorld" => "HelloWorld"); + } + + #[test] + fn test_camelcase() { + const TYPE: &str = "camelCase"; + + test_eq!("HelloWorld" => "helloWorld"); + test_eq!("Hello_World" => "helloWorld"); + test_eq!("Hello-World" => "helloWorld"); + test_eq!("helloWorld" => "helloWorld"); + } + + #[test] + fn test_snakecase() { + const TYPE: &str = "snake_case"; + + test_eq!("HelloWorld" => "hello_world"); + test_eq!("Hello_World" => "hello_world"); + test_eq!("Hello-World" => "hello_world"); + test_eq!("helloWorld" => "hello_world"); + } + + #[test] + fn test_screaming_snakecase() { + const TYPE: &str = "SCREAMING_SNAKE_CASE"; + + test_eq!("HelloWorld" => "HELLO_WORLD"); + test_eq!("Hello_World" => "HELLO_WORLD"); + test_eq!("Hello-World" => "HELLO_WORLD"); + test_eq!("helloWorld" => "HELLO_WORLD"); + } + + #[test] + fn test_kebabcase() { + const TYPE: &str = "kebab-case"; + + test_eq!("HelloWorld" => "hello-world"); + test_eq!("Hello_World" => "hello-world"); + test_eq!("Hello-World" => "hello-world"); + test_eq!("helloWorld" => "hello-world"); + } + + #[test] + fn test_screaming_kebabcase() { + const TYPE: &str = "SCREAMING-KEBAB-CASE"; + + test_eq!("HelloWorld" => "HELLO-WORLD"); + test_eq!("Hello_World" => "HELLO-WORLD"); + test_eq!("Hello-World" => "HELLO-WORLD"); + test_eq!("helloWorld" => "HELLO-WORLD"); + } +} diff --git a/crates/teloxide-macros/src/unzip.rs b/crates/teloxide-macros/src/unzip.rs new file mode 100644 index 00000000..372ad2e2 --- /dev/null +++ b/crates/teloxide-macros/src/unzip.rs @@ -0,0 +1,20 @@ +use std::iter::FromIterator; + +pub(crate) struct Unzip(pub A, pub B); + +impl FromIterator<(T, U)> for Unzip +where + A: Default + Extend, + B: Default + Extend, +{ + fn from_iter>(iter: I) -> Self { + let (mut a, mut b): (A, B) = Default::default(); + + for (t, u) in iter { + a.extend([t]); + b.extend([u]); + } + + Unzip(a, b) + } +} diff --git a/crates/teloxide/Cargo.toml b/crates/teloxide/Cargo.toml new file mode 100644 index 00000000..455f13a9 --- /dev/null +++ b/crates/teloxide/Cargo.toml @@ -0,0 +1,167 @@ +[package] +name = "teloxide" +version = "0.11.1" +edition = "2021" +description = "An elegant Telegram bots framework for Rust" +repository = "https://github.com/teloxide/teloxide" +documentation = "https://docs.rs/teloxide/" +readme = "../../README.md" +keywords = ["teloxide", "telegram", "telegram-bot", "telegram-bot-api"] +categories = ["web-programming", "api-bindings", "asynchronous"] +license = "MIT" +exclude = ["media", "README.md"] + +[features] +default = ["native-tls", "ctrlc_handler", "teloxide-core/default", "auto-send"] + +webhooks = ["rand"] +webhooks-axum = ["webhooks", "axum", "tower", "tower-http"] + +sqlite-storage = ["sqlx"] +redis-storage = ["redis"] +cbor-serializer = ["serde_cbor"] +bincode-serializer = ["bincode"] + +macros = ["teloxide-macros"] + +ctrlc_handler = ["tokio/signal"] + +native-tls = ["teloxide-core/native-tls"] +rustls = ["teloxide-core/rustls"] +auto-send = ["teloxide-core/auto_send"] +throttle = ["teloxide-core/throttle"] +cache-me = ["teloxide-core/cache_me"] +trace-adaptor = ["teloxide-core/trace_adaptor"] +erased = ["teloxide-core/erased"] + +# currently used for `README.md` tests, building docs for `docsrs` to add `This is supported on feature="..." only.`, +# and for teloxide-core. +nightly = ["teloxide-core/nightly"] + +full = [ + "webhooks-axum", + "sqlite-storage", + "redis-storage", + "cbor-serializer", + "bincode-serializer", + "macros", + "ctrlc_handler", + "teloxide-core/full", + "native-tls", + "rustls", + "auto-send", + "throttle", + "cache-me", + "trace-adaptor", + "erased", +] + +[dependencies] +teloxide-core = { version = "0.8.0", default-features = false } +teloxide-macros = { version = "0.7.0", optional = true } + +serde_json = "1.0" +serde = { version = "1.0", features = ["derive"] } + +dptree = "0.3.0" + +# These lines are used only for development. +# teloxide-core = { path = "../teloxide-core", default-features = false } +# teloxide-macros = { path = "../teloxide-macros", optional = true } +# dptree = { git = "https://github.com/teloxide/dptree", rev = "df578e4" } + +tokio = { version = "1.8", features = ["fs"] } +tokio-util = "0.7" +tokio-stream = "0.1.8" + +url = "2.2.2" +log = "0.4" +bytes = "1.0" +mime = "0.3" + +derive_more = "0.99" +thiserror = "1.0" +futures = "0.3.15" +pin-project = "1.0" +serde_with_macros = "1.4" +aquamarine = "0.1.11" + +sqlx = { version = "0.6", optional = true, default-features = false, features = [ + "runtime-tokio-native-tls", + "macros", + "sqlite", +] } +redis = { version = "0.21", features = ["tokio-comp"], optional = true } +serde_cbor = { version = "0.11", optional = true } +bincode = { version = "1.3", optional = true } +axum = { version = "0.5.13", optional = true } +tower = { version = "0.4.12", optional = true } +tower-http = { version = "0.3.4", features = ["trace"], optional = true } +rand = { version = "0.8.5", optional = true } + +[dev-dependencies] +rand = "0.8.3" +pretty_env_logger = "0.4.0" +serde = "1" +serde_json = "1" +tokio = { version = "1.8", features = ["fs", "rt-multi-thread", "macros"] } +reqwest = "0.11.11" +chrono = "0.4" +tokio-stream = "0.1" + +[package.metadata.docs.rs] +all-features = true +# FIXME: Add back "-Znormalize-docs" when https://github.com/rust-lang/rust/issues/93703 is fixed +rustdoc-args = ["--cfg", "docsrs"] +rustc-args = ["--cfg", "dep_docsrs"] +cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples=examples"] + +[[test]] +name = "redis" +path = "tests/redis.rs" +required-features = ["redis-storage", "cbor-serializer", "bincode-serializer"] + +[[test]] +name = "sqlite" +path = "tests/sqlite.rs" +required-features = ["sqlite-storage", "cbor-serializer", "bincode-serializer"] + +[[example]] +name = "dialogue" +required-features = ["macros"] + +[[example]] +name = "command" +required-features = ["macros"] + +[[example]] +name = "db_remember" +required-features = ["sqlite-storage", "redis-storage", "bincode-serializer", "macros"] + +[[example]] +name = "inline" +required-features = ["macros"] + +[[example]] +name = "buttons" +required-features = ["macros"] + +[[example]] +name = "admin" +required-features = ["macros"] + +[[example]] +name = "dispatching_features" +required-features = ["macros"] + +[[example]] +name = "ngrok_ping_pong" +required-features = ["webhooks-axum"] + +[[example]] +name = "heroku_ping_pong" +required-features = ["webhooks-axum"] + +[[example]] +name = "purchase" +required-features = ["macros"] diff --git a/crates/teloxide/LICENSE b/crates/teloxide/LICENSE new file mode 120000 index 00000000..30cff740 --- /dev/null +++ b/crates/teloxide/LICENSE @@ -0,0 +1 @@ +../../LICENSE \ No newline at end of file diff --git a/crates/teloxide/README.md b/crates/teloxide/README.md new file mode 100644 index 00000000..e2ce90e9 --- /dev/null +++ b/crates/teloxide/README.md @@ -0,0 +1,3 @@ +See [`README.md`] at the root of the repository. + +[`README.md`]: ../../README.md diff --git a/examples/Procfile b/crates/teloxide/examples/Procfile similarity index 100% rename from examples/Procfile rename to crates/teloxide/examples/Procfile diff --git a/examples/README.md b/crates/teloxide/examples/README.md similarity index 100% rename from examples/README.md rename to crates/teloxide/examples/README.md diff --git a/examples/admin.rs b/crates/teloxide/examples/admin.rs similarity index 100% rename from examples/admin.rs rename to crates/teloxide/examples/admin.rs diff --git a/examples/buttons.rs b/crates/teloxide/examples/buttons.rs similarity index 100% rename from examples/buttons.rs rename to crates/teloxide/examples/buttons.rs diff --git a/examples/command.rs b/crates/teloxide/examples/command.rs similarity index 100% rename from examples/command.rs rename to crates/teloxide/examples/command.rs diff --git a/examples/db_remember.rs b/crates/teloxide/examples/db_remember.rs similarity index 100% rename from examples/db_remember.rs rename to crates/teloxide/examples/db_remember.rs diff --git a/examples/dialogue.rs b/crates/teloxide/examples/dialogue.rs similarity index 100% rename from examples/dialogue.rs rename to crates/teloxide/examples/dialogue.rs diff --git a/examples/dispatching_features.rs b/crates/teloxide/examples/dispatching_features.rs similarity index 100% rename from examples/dispatching_features.rs rename to crates/teloxide/examples/dispatching_features.rs diff --git a/examples/heroku_ping_pong.rs b/crates/teloxide/examples/heroku_ping_pong.rs similarity index 100% rename from examples/heroku_ping_pong.rs rename to crates/teloxide/examples/heroku_ping_pong.rs diff --git a/examples/inline.rs b/crates/teloxide/examples/inline.rs similarity index 100% rename from examples/inline.rs rename to crates/teloxide/examples/inline.rs diff --git a/examples/ngrok_ping_pong.rs b/crates/teloxide/examples/ngrok_ping_pong.rs similarity index 100% rename from examples/ngrok_ping_pong.rs rename to crates/teloxide/examples/ngrok_ping_pong.rs diff --git a/examples/purchase.rs b/crates/teloxide/examples/purchase.rs similarity index 100% rename from examples/purchase.rs rename to crates/teloxide/examples/purchase.rs diff --git a/examples/shared_state.rs b/crates/teloxide/examples/shared_state.rs similarity index 100% rename from examples/shared_state.rs rename to crates/teloxide/examples/shared_state.rs diff --git a/examples/throw_dice.rs b/crates/teloxide/examples/throw_dice.rs similarity index 100% rename from examples/throw_dice.rs rename to crates/teloxide/examples/throw_dice.rs diff --git a/src/dispatching.rs b/crates/teloxide/src/dispatching.rs similarity index 100% rename from src/dispatching.rs rename to crates/teloxide/src/dispatching.rs diff --git a/src/dispatching/dialogue.rs b/crates/teloxide/src/dispatching/dialogue.rs similarity index 100% rename from src/dispatching/dialogue.rs rename to crates/teloxide/src/dispatching/dialogue.rs diff --git a/src/dispatching/dialogue/get_chat_id.rs b/crates/teloxide/src/dispatching/dialogue/get_chat_id.rs similarity index 100% rename from src/dispatching/dialogue/get_chat_id.rs rename to crates/teloxide/src/dispatching/dialogue/get_chat_id.rs diff --git a/src/dispatching/dialogue/storage.rs b/crates/teloxide/src/dispatching/dialogue/storage.rs similarity index 93% rename from src/dispatching/dialogue/storage.rs rename to crates/teloxide/src/dispatching/dialogue/storage.rs index e8d2c158..b78cd72e 100644 --- a/src/dispatching/dialogue/storage.rs +++ b/crates/teloxide/src/dispatching/dialogue/storage.rs @@ -9,9 +9,6 @@ mod redis_storage; #[cfg(feature = "sqlite-storage")] mod sqlite_storage; -#[cfg(feature = "rocksdb-storage")] -mod rocksdb_storage; - use futures::future::BoxFuture; use teloxide_core::types::ChatId; @@ -28,9 +25,6 @@ use std::sync::Arc; #[cfg(feature = "sqlite-storage")] pub use sqlite_storage::{SqliteStorage, SqliteStorageError}; -#[cfg(feature = "rocksdb-storage")] -pub use rocksdb_storage::{RocksDbStorage, RocksDbStorageError}; - /// A storage with an erased error type. pub type ErasedStorage = dyn Storage> + Send + Sync; @@ -47,12 +41,10 @@ pub type ErasedStorage = /// /// - [`InMemStorage`] -- a storage based on [`std::collections::HashMap`]. /// - [`RedisStorage`] -- a Redis-based storage. -/// - [`RocksDbStorage`] -- a RocksDB-based persistent storage. /// - [`SqliteStorage`] -- an SQLite-based persistent storage. /// /// [`InMemStorage`]: crate::dispatching::dialogue::InMemStorage /// [`RedisStorage`]: crate::dispatching::dialogue::RedisStorage -/// [`RocksDbStorage`]: crate::dispatching::dialogue::RocksDbStorage /// [`SqliteStorage`]: crate::dispatching::dialogue::SqliteStorage pub trait Storage { type Error; diff --git a/src/dispatching/dialogue/storage/in_mem_storage.rs b/crates/teloxide/src/dispatching/dialogue/storage/in_mem_storage.rs similarity index 100% rename from src/dispatching/dialogue/storage/in_mem_storage.rs rename to crates/teloxide/src/dispatching/dialogue/storage/in_mem_storage.rs diff --git a/src/dispatching/dialogue/storage/redis_storage.rs b/crates/teloxide/src/dispatching/dialogue/storage/redis_storage.rs similarity index 100% rename from src/dispatching/dialogue/storage/redis_storage.rs rename to crates/teloxide/src/dispatching/dialogue/storage/redis_storage.rs diff --git a/src/dispatching/dialogue/storage/serializer.rs b/crates/teloxide/src/dispatching/dialogue/storage/serializer.rs similarity index 100% rename from src/dispatching/dialogue/storage/serializer.rs rename to crates/teloxide/src/dispatching/dialogue/storage/serializer.rs diff --git a/src/dispatching/dialogue/storage/sqlite_storage.rs b/crates/teloxide/src/dispatching/dialogue/storage/sqlite_storage.rs similarity index 100% rename from src/dispatching/dialogue/storage/sqlite_storage.rs rename to crates/teloxide/src/dispatching/dialogue/storage/sqlite_storage.rs diff --git a/src/dispatching/dialogue/storage/trace_storage.rs b/crates/teloxide/src/dispatching/dialogue/storage/trace_storage.rs similarity index 100% rename from src/dispatching/dialogue/storage/trace_storage.rs rename to crates/teloxide/src/dispatching/dialogue/storage/trace_storage.rs diff --git a/src/dispatching/dispatcher.rs b/crates/teloxide/src/dispatching/dispatcher.rs similarity index 100% rename from src/dispatching/dispatcher.rs rename to crates/teloxide/src/dispatching/dispatcher.rs diff --git a/src/dispatching/distribution.rs b/crates/teloxide/src/dispatching/distribution.rs similarity index 100% rename from src/dispatching/distribution.rs rename to crates/teloxide/src/dispatching/distribution.rs diff --git a/src/dispatching/filter_ext.rs b/crates/teloxide/src/dispatching/filter_ext.rs similarity index 100% rename from src/dispatching/filter_ext.rs rename to crates/teloxide/src/dispatching/filter_ext.rs diff --git a/src/dispatching/handler_description.rs b/crates/teloxide/src/dispatching/handler_description.rs similarity index 100% rename from src/dispatching/handler_description.rs rename to crates/teloxide/src/dispatching/handler_description.rs diff --git a/src/dispatching/handler_ext.rs b/crates/teloxide/src/dispatching/handler_ext.rs similarity index 100% rename from src/dispatching/handler_ext.rs rename to crates/teloxide/src/dispatching/handler_ext.rs diff --git a/src/dispatching/repls.rs b/crates/teloxide/src/dispatching/repls.rs similarity index 100% rename from src/dispatching/repls.rs rename to crates/teloxide/src/dispatching/repls.rs diff --git a/src/dispatching/repls/caution.md b/crates/teloxide/src/dispatching/repls/caution.md similarity index 100% rename from src/dispatching/repls/caution.md rename to crates/teloxide/src/dispatching/repls/caution.md diff --git a/src/dispatching/repls/commands_repl.rs b/crates/teloxide/src/dispatching/repls/commands_repl.rs similarity index 100% rename from src/dispatching/repls/commands_repl.rs rename to crates/teloxide/src/dispatching/repls/commands_repl.rs diff --git a/src/dispatching/repls/preamble.md b/crates/teloxide/src/dispatching/repls/preamble.md similarity index 100% rename from src/dispatching/repls/preamble.md rename to crates/teloxide/src/dispatching/repls/preamble.md diff --git a/src/dispatching/repls/repl.rs b/crates/teloxide/src/dispatching/repls/repl.rs similarity index 100% rename from src/dispatching/repls/repl.rs rename to crates/teloxide/src/dispatching/repls/repl.rs diff --git a/src/dispatching/repls/stopping.md b/crates/teloxide/src/dispatching/repls/stopping.md similarity index 100% rename from src/dispatching/repls/stopping.md rename to crates/teloxide/src/dispatching/repls/stopping.md diff --git a/src/dispatching/update_listeners.rs b/crates/teloxide/src/dispatching/update_listeners.rs similarity index 100% rename from src/dispatching/update_listeners.rs rename to crates/teloxide/src/dispatching/update_listeners.rs diff --git a/src/dispatching/update_listeners/polling.rs b/crates/teloxide/src/dispatching/update_listeners/polling.rs similarity index 100% rename from src/dispatching/update_listeners/polling.rs rename to crates/teloxide/src/dispatching/update_listeners/polling.rs diff --git a/src/dispatching/update_listeners/stateful_listener.rs b/crates/teloxide/src/dispatching/update_listeners/stateful_listener.rs similarity index 100% rename from src/dispatching/update_listeners/stateful_listener.rs rename to crates/teloxide/src/dispatching/update_listeners/stateful_listener.rs diff --git a/src/dispatching/update_listeners/webhooks.rs b/crates/teloxide/src/dispatching/update_listeners/webhooks.rs similarity index 100% rename from src/dispatching/update_listeners/webhooks.rs rename to crates/teloxide/src/dispatching/update_listeners/webhooks.rs diff --git a/src/dispatching/update_listeners/webhooks/axum.rs b/crates/teloxide/src/dispatching/update_listeners/webhooks/axum.rs similarity index 100% rename from src/dispatching/update_listeners/webhooks/axum.rs rename to crates/teloxide/src/dispatching/update_listeners/webhooks/axum.rs diff --git a/src/error_handlers.rs b/crates/teloxide/src/error_handlers.rs similarity index 100% rename from src/error_handlers.rs rename to crates/teloxide/src/error_handlers.rs diff --git a/src/features.md b/crates/teloxide/src/features.md similarity index 94% rename from src/features.md rename to crates/teloxide/src/features.md index a0456377..fb1680e9 100644 --- a/src/features.md +++ b/crates/teloxide/src/features.md @@ -16,13 +16,11 @@ | `native-tls` | Enables the [`native-tls`] TLS implementation (**enabled by default**). | | `rustls` | Enables the [`rustls`] TLS implementation. | | `redis-storage` | Enables the [Redis] storage support for dialogues. | -| `rocksdb-storage` | Enables the [RocksDB] storage support for dialogues. | | `sqlite-storage` | Enables the [Sqlite] storage support for dialogues. | | `cbor-serializer` | Enables the [CBOR] serializer for dialogues. | | `bincode-serializer` | Enables the [Bincode] serializer for dialogues. | [Redis]: https://redis.io/ -[RocksDB]: https://rocksdb.org/ [Sqlite]: https://www.sqlite.org/ [CBOR]: https://en.wikipedia.org/wiki/CBOR [Bincode]: https://github.com/servo/bincode diff --git a/src/lib.rs b/crates/teloxide/src/lib.rs similarity index 92% rename from src/lib.rs rename to crates/teloxide/src/lib.rs index f1e835e0..2fc500d9 100644 --- a/src/lib.rs +++ b/crates/teloxide/src/lib.rs @@ -39,10 +39,10 @@ // [2]: https://github.com/rust-lang/rustfmt/issues/4787 // [3]: https://github.com/rust-lang/rust/issues/82768#issuecomment-803935643 #![cfg_attr(feature = "nightly", cfg_attr(feature = "nightly", doc = include_str!("features.md")))] -// https://github.com/teloxide/teloxide/raw/master/logo.svg doesn't work in html_logo_url, I don't know why. +// https://github.com/teloxide/teloxide/raw/master/media/teloxide-logo.svg doesn't work in html_logo_url, I don't know why. #![doc( - html_logo_url = "https://github.com/teloxide/teloxide/raw/master/ICON.png", - html_favicon_url = "https://github.com/teloxide/teloxide/raw/master/ICON.png" + html_logo_url = "https://github.com/teloxide/teloxide/raw/master/media/teloxide-logo.png", + html_favicon_url = "https://github.com/teloxide/teloxide/raw/master/teloxide-logo.png" )] // To properly build docs of this crate run // ```console @@ -80,7 +80,7 @@ pub use dispatching::filter_command; pub use dptree::{self, case as handler}; #[cfg(all(feature = "nightly", doctest))] -#[cfg_attr(feature = "nightly", cfg_attr(feature = "nightly", doc = include_str!("../README.md")))] +#[cfg_attr(feature = "nightly", cfg_attr(feature = "nightly", doc = include_str!("../../../README.md")))] enum ReadmeDocTests {} use teloxide_core::requests::ResponseResult; diff --git a/src/prelude.rs b/crates/teloxide/src/prelude.rs similarity index 100% rename from src/prelude.rs rename to crates/teloxide/src/prelude.rs diff --git a/src/stop.rs b/crates/teloxide/src/stop.rs similarity index 100% rename from src/stop.rs rename to crates/teloxide/src/stop.rs diff --git a/src/utils.rs b/crates/teloxide/src/utils.rs similarity index 100% rename from src/utils.rs rename to crates/teloxide/src/utils.rs diff --git a/src/utils/command.rs b/crates/teloxide/src/utils/command.rs similarity index 100% rename from src/utils/command.rs rename to crates/teloxide/src/utils/command.rs diff --git a/src/utils/html.rs b/crates/teloxide/src/utils/html.rs similarity index 100% rename from src/utils/html.rs rename to crates/teloxide/src/utils/html.rs diff --git a/src/utils/markdown.rs b/crates/teloxide/src/utils/markdown.rs similarity index 100% rename from src/utils/markdown.rs rename to crates/teloxide/src/utils/markdown.rs diff --git a/src/utils/shutdown_token.rs b/crates/teloxide/src/utils/shutdown_token.rs similarity index 100% rename from src/utils/shutdown_token.rs rename to crates/teloxide/src/utils/shutdown_token.rs diff --git a/tests/command.rs b/crates/teloxide/tests/command.rs similarity index 100% rename from tests/command.rs rename to crates/teloxide/tests/command.rs diff --git a/tests/redis.rs b/crates/teloxide/tests/redis.rs similarity index 100% rename from tests/redis.rs rename to crates/teloxide/tests/redis.rs diff --git a/tests/sqlite.rs b/crates/teloxide/tests/sqlite.rs similarity index 100% rename from tests/sqlite.rs rename to crates/teloxide/tests/sqlite.rs diff --git a/media/example.gif b/media/example.gif new file mode 100644 index 00000000..a0605aa6 Binary files /dev/null and b/media/example.gif differ diff --git a/media/teloxide-core-logo.png b/media/teloxide-core-logo.png new file mode 100644 index 00000000..2155dd30 Binary files /dev/null and b/media/teloxide-core-logo.png differ diff --git a/media/teloxide-core-logo.svg b/media/teloxide-core-logo.svg new file mode 100644 index 00000000..08a5a668 --- /dev/null +++ b/media/teloxide-core-logo.svg @@ -0,0 +1,1907 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ICON.png b/media/teloxide-logo.png similarity index 100% rename from ICON.png rename to media/teloxide-logo.png diff --git a/logo.svg b/media/teloxide-logo.svg similarity index 100% rename from logo.svg rename to media/teloxide-logo.svg diff --git a/rust-toolchain.toml b/rust-toolchain.toml index eca55770..b4990f3d 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,4 +1,4 @@ [toolchain] -channel = "nightly-2022-09-01" +channel = "nightly-2022-09-23" components = ["rustfmt", "clippy"] profile = "minimal" diff --git a/rustfmt.toml b/rustfmt.toml index 38db4219..6353e228 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -4,3 +4,4 @@ format_strings = true imports_granularity = "Crate" use_small_heuristics = "Max" use_field_init_shorthand = true +merge_derives = false diff --git a/src/dispatching/dialogue/storage/rocksdb_storage.rs b/src/dispatching/dialogue/storage/rocksdb_storage.rs deleted file mode 100644 index d01d8fd9..00000000 --- a/src/dispatching/dialogue/storage/rocksdb_storage.rs +++ /dev/null @@ -1,113 +0,0 @@ -use super::{serializer::Serializer, Storage}; -use futures::future::BoxFuture; -use rocksdb::{DBCompressionType, DBWithThreadMode, MultiThreaded}; -use serde::{de::DeserializeOwned, Serialize}; -use std::{ - convert::Infallible, - fmt::{Debug, Display}, - str, - sync::Arc, -}; -use teloxide_core::types::ChatId; -use thiserror::Error; - -/// A persistent dialogue storage based on [RocksDb](http://rocksdb.org/). -pub struct RocksDbStorage { - db: DBWithThreadMode, - serializer: S, -} - -/// An error returned from [`RocksDbStorage`]. -#[derive(Debug, Error)] -pub enum RocksDbStorageError -where - SE: Debug + Display, -{ - #[error("dialogue serialization error: {0}")] - SerdeError(SE), - - #[error("RocksDb error: {0}")] - RocksDbError(#[from] rocksdb::Error), - - /// Returned from [`RocksDbStorage::remove_dialogue`]. - #[error("row not found")] - DialogueNotFound, -} - -impl RocksDbStorage { - pub async fn open( - path: &str, - serializer: S, - options: Option, - ) -> Result, RocksDbStorageError> { - let options = match options { - Some(opts) => opts, - None => { - let mut opts = rocksdb::Options::default(); - opts.set_compression_type(DBCompressionType::Lz4); - opts.create_if_missing(true); - opts - } - }; - - let db = DBWithThreadMode::::open(&options, path)?; - Ok(Arc::new(Self { db, serializer })) - } -} - -impl Storage for RocksDbStorage -where - S: Send + Sync + Serializer + 'static, - D: Send + Serialize + DeserializeOwned + 'static, - >::Error: Debug + Display, -{ - type Error = RocksDbStorageError<>::Error>; - - /// Returns [`RocksDbStorageError::DialogueNotFound`] if a dialogue does not - /// exist. - fn remove_dialogue( - self: Arc, - ChatId(chat_id): ChatId, - ) -> BoxFuture<'static, Result<(), Self::Error>> { - Box::pin(async move { - let key = chat_id.to_le_bytes(); - - if self.db.get(&key)?.is_none() { - return Err(RocksDbStorageError::DialogueNotFound); - } - - self.db.delete(&key).unwrap(); - - Ok(()) - }) - } - - fn update_dialogue( - self: Arc, - ChatId(chat_id): ChatId, - dialogue: D, - ) -> BoxFuture<'static, Result<(), Self::Error>> { - Box::pin(async move { - let d = - self.serializer.serialize(&dialogue).map_err(RocksDbStorageError::SerdeError)?; - - let key = chat_id.to_le_bytes(); - self.db.put(&key, &d)?; - - Ok(()) - }) - } - - fn get_dialogue( - self: Arc, - ChatId(chat_id): ChatId, - ) -> BoxFuture<'static, Result, Self::Error>> { - Box::pin(async move { - let key = chat_id.to_le_bytes(); - self.db - .get(&key)? - .map(|d| self.serializer.deserialize(&d).map_err(RocksDbStorageError::SerdeError)) - .transpose() - }) - } -} diff --git a/tests/rocksdb.rs b/tests/rocksdb.rs deleted file mode 100644 index 7366a262..00000000 --- a/tests/rocksdb.rs +++ /dev/null @@ -1,95 +0,0 @@ -use std::{ - fmt::{Debug, Display}, - fs, - sync::Arc, -}; -use teloxide::{ - dispatching::dialogue::{RocksDbStorage, RocksDbStorageError, Serializer, Storage}, - types::ChatId, -}; - -#[tokio::test(flavor = "multi_thread")] -async fn test_rocksdb_json() { - fs::remove_dir_all("./test_db1").ok(); - fs::create_dir("./test_db1").unwrap(); - let storage = RocksDbStorage::open( - "./test_db1/test_db1.rocksdb", - teloxide::dispatching::dialogue::serializer::Json, - None, - ) - .await - .unwrap(); - test_rocksdb(storage).await; - fs::remove_dir_all("./test_db1").unwrap(); -} - -#[tokio::test(flavor = "multi_thread")] -async fn test_rocksdb_bincode() { - fs::remove_dir_all("./test_db2").ok(); - fs::create_dir("./test_db2").unwrap(); - let storage = RocksDbStorage::open( - "./test_db2/test_db2.rocksdb", - teloxide::dispatching::dialogue::serializer::Bincode, - None, - ) - .await - .unwrap(); - test_rocksdb(storage).await; - fs::remove_dir_all("./test_db2").unwrap(); -} - -#[tokio::test(flavor = "multi_thread")] -async fn test_rocksdb_cbor() { - fs::remove_dir_all("./test_db3").ok(); - fs::create_dir("./test_db3").unwrap(); - let storage = RocksDbStorage::open( - "./test_db3/test_db3.rocksdb", - teloxide::dispatching::dialogue::serializer::Cbor, - None, - ) - .await - .unwrap(); - test_rocksdb(storage).await; - fs::remove_dir_all("./test_db3").unwrap(); -} - -type Dialogue = String; - -macro_rules! test_dialogues { - ($storage:expr, $_0:expr, $_1:expr, $_2:expr) => { - assert_eq!(Arc::clone(&$storage).get_dialogue(ChatId(1)).await.unwrap(), $_0); - assert_eq!(Arc::clone(&$storage).get_dialogue(ChatId(11)).await.unwrap(), $_1); - assert_eq!(Arc::clone(&$storage).get_dialogue(ChatId(256)).await.unwrap(), $_2); - }; -} - -async fn test_rocksdb(storage: Arc>) -where - S: Send + Sync + Serializer + 'static, - >::Error: Debug + Display, -{ - test_dialogues!(storage, None, None, None); - - Arc::clone(&storage).update_dialogue(ChatId(1), "ABC".to_owned()).await.unwrap(); - Arc::clone(&storage).update_dialogue(ChatId(11), "DEF".to_owned()).await.unwrap(); - Arc::clone(&storage).update_dialogue(ChatId(256), "GHI".to_owned()).await.unwrap(); - - test_dialogues!( - storage, - Some("ABC".to_owned()), - Some("DEF".to_owned()), - Some("GHI".to_owned()) - ); - - Arc::clone(&storage).remove_dialogue(ChatId(1)).await.unwrap(); - Arc::clone(&storage).remove_dialogue(ChatId(11)).await.unwrap(); - Arc::clone(&storage).remove_dialogue(ChatId(256)).await.unwrap(); - - test_dialogues!(storage, None, None, None); - - // Check that a try to remove a non-existing dialogue results in an error. - assert!(matches!( - Arc::clone(&storage).remove_dialogue(ChatId(1)).await.unwrap_err(), - RocksDbStorageError::DialogueNotFound - )); -}