Merge pull request #759 from teloxide/project-restructure

Project restructure
This commit is contained in:
Sima Kinsart 2022-11-08 04:47:58 +06:00 committed by GitHub
commit 0df19e5fc9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
349 changed files with 38912 additions and 203 deletions

View file

@ -1,12 +1,14 @@
[alias] [alias]
# We pass "--cfg docsrs" when building docs to turn on nightly-only rustdoc features like # Using `--features=full --features=nightly` instead of `--all-features` because of
# `This is supported on feature="..." only.` # https://github.com/rust-lang/cargo/issues/10333
# #
# "--cfg dep_docsrs" is used for the same reason, but for `teloxide-core`. # "tokio/macros" and "tokio/rt-multi-thread" are required for examples
docs = """ docs = """doc
doc -Zrustdoc-scrape-examples=examples
--all-features --features=full --features=nightly
--config build.rustflags=["--cfg=dep_docsrs"] --features=tokio/macros --features=tokio/rt-multi-thread
--config build.rustdocflags=["--cfg=docsrs","-Znormalize-docs"]
-Zrustdoc-scrape-examples=examples
""" """
[build]
# We pass "--cfg docsrs" when building docs to add `This is supported on feature="..." only.`
rustdocflags = ["--cfg", "docsrs", "-Znormalize-docs"]

View file

@ -15,13 +15,18 @@ env:
CARGO_NET_RETRY: 10 CARGO_NET_RETRY: 10
RUSTUP_MAX_RETRIES: 10 RUSTUP_MAX_RETRIES: 10
rust_nightly: nightly-2022-09-01
# When updating this, also update: # When updating this, also update:
# - README.md # - crates/teloxide-core/src/codegen.rs
# - src/lib.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 # - down below in a matrix
rust_msrv: 1.64.0 rust_msrv: 1.64.0
CI: 1
jobs: jobs:
# Depends on all action that are required for a "successful" CI run. # Depends on all action that are required for a "successful" CI run.
ci-pass: ci-pass:
@ -82,7 +87,7 @@ jobs:
toolchain: beta toolchain: beta
features: "--features full" features: "--features full"
- rust: nightly - rust: nightly
toolchain: nightly-2022-09-01 toolchain: nightly-2022-09-23
features: "--all-features" features: "--all-features"
- rust: msrv - rust: msrv
toolchain: 1.64.0 toolchain: 1.64.0

2
.gitignore vendored
View file

@ -1,7 +1,5 @@
/target /target
**/*.rs.bk
Cargo.lock Cargo.lock
.idea/ .idea/
.vscode/ .vscode/
examples/*/target
*.sqlite *.sqlite

View file

@ -1,172 +1,2 @@
[package] [workspace]
name = "teloxide" members = ["crates/*"]
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"]
[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"]

View file

@ -1,7 +1,7 @@
> [v0.11 -> v0.11.1 migration guide >>](MIGRATION_GUIDE.md#011---0111) > [v0.11 -> v0.11.1 migration guide >>](MIGRATION_GUIDE.md#011---0111)
<div align="center"> <div align="center">
<img src="./ICON.png" width="250"/> <img src="./media/teloxide-logo.png" width="250"/>
<h1><code>teloxide</code></h1> <h1><code>teloxide</code></h1>
<a href="https://docs.rs/teloxide/"> <a href="https://docs.rs/teloxide/">
<img src="https://docs.rs/teloxide/badge.svg"> <img src="https://docs.rs/teloxide/badge.svg">
@ -85,7 +85,7 @@ tokio = { version = "1.8", features = ["rt-multi-thread", "macros"] }
This bot replies with a die throw to each received message: 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 ```rust,no_run
use teloxide::prelude::*; use teloxide::prelude::*;
@ -120,7 +120,7 @@ Commands are strongly typed and defined declaratively, similar to how we define
[structopt]: https://docs.rs/structopt/0.3.9/structopt/ [structopt]: https://docs.rs/structopt/0.3.9/structopt/
[serde-json]: https://github.com/serde-rs/json [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 ```rust,no_run
use teloxide::{prelude::*, utils::command::BotCommands}; use teloxide::{prelude::*, utils::command::BotCommands};
@ -174,7 +174,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: 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 ```rust,ignore
use teloxide::{dispatching::dialogue::InMemStorage, prelude::*}; use teloxide::{dispatching::dialogue::InMemStorage, prelude::*};
@ -285,7 +285,7 @@ async fn receive_location(
<img src="./media/dialogue.gif" width="420" /> <img src="./media/dialogue.gif" width="420" />
</div> </div>
[More examples >>](examples/) [More examples >>](crates/teloxide/examples/)
## FAQ ## FAQ
@ -307,11 +307,11 @@ A: No, only the bots API.
**Q: Can I use webhooks?** **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?** **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 ## Community bots

View file

@ -0,0 +1,632 @@
# 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.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<E>` now implements `Clone` even if `E` is not `Clone` ([#244][pr244])
[pr244]: https://github.com/teloxide/teloxide-core/pull/244
### Added
- `From<ApiError>`, `From<DownloadError>` and `From<std::io::Error>` 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<String>` 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<Target = User>` 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<Utc>` 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<String>` to `Option<std::net::IpAddr>` ([#172][pr172])
- Type of `WebhookInfo::allowed_updates` from `Option<Vec<String>>` to `Option<Vec<AllowedUpdate>>` ([#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<Utc>` ([#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<Target = ChatMemberKind>` 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<Utc>` 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<i32>`=> `Option<u32>` ([#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<ChatMember>` ([#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<T>` alias to `<<T as HasPayload>::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<Update>`
(instead of `Vec<Result<Update, (Value, serde_json::Error)>>`)
[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<Vec<_>>` by `IntoIterator<Item = _>` 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<Result<Bytes>>``, instead of `Future<Result<Stream<Result<Bytes>>>>``
- 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<Item = T>` bound instead of `_: Into<Vec<T>>` 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

View file

@ -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"]

View file

@ -0,0 +1 @@
../../LICENSE

View file

@ -0,0 +1,34 @@
<div align="center">
<img src="../../media/teloxide-core-logo.svg" width="250"/>
<h1>teloxide-core</h1>
<a href="https://github.com/teloxide/teloxide-core/actions">
<img src="https://github.com/teloxide/teloxide-core/workflows/Continuous%20integration/badge.svg">
</a>
<a href="https://docs.rs/teloxide_core/">
<img src="https://docs.rs/teloxide-core/badge.svg">
</a>
<a href="LICENSE">
<img src="https://img.shields.io/badge/license-MIT-blue.svg">
</a>
<a href="https://core.telegram.org/bots/api">
<img src="https://img.shields.io/badge/API%20coverage-Up%20to%206.2%20(inclusively)-green.svg">
</a>
<a href="https://crates.io/crates/teloxide_core">
<img src="https://img.shields.io/crates/v/teloxide_core.svg">
</a>
<a href="https://t.me/teloxide">
<img src="https://img.shields.io/badge/official%20chat-t.me%2Fteloxide-blueviolet">
</a>
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`].
</div>
```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

View file

@ -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<dyn std::error::Error>> {
pretty_env_logger::init();
let chat_id =
ChatId(std::env::var("CHAT_ID").expect("Expected CHAT_ID env var").parse::<i64>()?);
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(())
}

View file

@ -0,0 +1,21 @@
use teloxide_core::{
prelude::*,
types::{DiceEmoji, Me, ParseMode},
};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
pretty_env_logger::init();
let chat_id =
ChatId(std::env::var("CHAT_ID").expect("Expected CHAT_ID env var").parse::<i64>()?);
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(())
}

File diff suppressed because it is too large Load diff

View file

@ -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;

View file

@ -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<B> {
bot: B,
}
impl<B> AutoSend<B> {
/// 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<B> {
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<B::$T>
};
}
impl<B> Requester for AutoSend<B>
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<B>
{ this => this.inner() }
}
#[must_use = "Futures are lazy and do nothing unless polled or awaited"]
pub struct AutoRequest<R>(R);
impl<R> AutoRequest<R>
where
R: Request,
{
pub fn new(inner: R) -> Self {
Self(inner)
}
}
impl<R> Request for AutoRequest<R>
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<R: Request> IntoFuture for AutoRequest<R> {
type Output = Result<Output<Self>, <Self as Request>::Err>;
type IntoFuture = <Self as Request>::Send;
fn into_future(self) -> Self::IntoFuture {
self.send()
}
}
impl<R: Request> HasPayload for AutoRequest<R> {
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()
}
}

View file

@ -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<B> {
bot: B,
me: Arc<OnceCell<Me>>,
}
impl<B> CacheMe<B> {
/// 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<B> {
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<Me> {
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<B> Requester for CacheMe<B>
where
B: Requester,
{
type Err = B::Err;
type GetMe = CachedMeRequest<B::GetMe>;
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<B>
{ this => this.inner() }
}
#[must_use = "Requests are lazy and do nothing unless sent"]
pub struct CachedMeRequest<R: Request<Payload = GetMe>>(Inner<R>, GetMe);
enum Inner<R: Request<Payload = GetMe>> {
Ready(Me),
Pending(R, Arc<OnceCell<Me>>),
}
impl<R> Request for CachedMeRequest<R>
where
R: Request<Payload = GetMe>,
{
type Err = R::Err;
type Send = Send<R>;
type SendRef = SendRef<R>;
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<R: Request<Payload = GetMe>> HasPayload for CachedMeRequest<R> {
type Payload = GetMe;
fn payload_mut(&mut self) -> &mut Self::Payload {
&mut self.1
}
fn payload_ref(&self) -> &Self::Payload {
&self.1
}
}
impl<R: Request<Payload = GetMe>> IntoFuture for CachedMeRequest<R> {
type Output = Result<Me, R::Err>;
type IntoFuture = Send<R>;
fn into_future(self) -> Self::IntoFuture {
self.send()
}
}
type ReadyMe<Err> = Ready<Result<Me, Err>>;
#[pin_project::pin_project]
pub struct Send<R: Request<Payload = GetMe>>(
#[pin] future::Either<ReadyMe<R::Err>, Init<R::Send, Me>>,
);
impl<R: Request<Payload = GetMe>> Future for Send<R> {
type Output = Result<Me, R::Err>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.project();
this.0.poll(cx)
}
}
#[pin_project::pin_project]
pub struct SendRef<R: Request<Payload = GetMe>>(
#[pin] future::Either<ReadyMe<R::Err>, Init<R::SendRef, Me>>,
);
impl<R: Request<Payload = GetMe>> Future for SendRef<R> {
type Output = Result<Me, R::Err>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.project();
this.0.poll(cx)
}
}
#[pin_project::pin_project]
struct Init<F, T>(#[pin] F, Arc<OnceCell<T>>);
impl<F: Future<Output = Result<T, E>>, T: Clone, E> Future for Init<F, T> {
type Output = Result<T, E>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
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,
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -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<B> {
bot: B,
mode: ParseMode,
}
impl<B> DefaultParseMode<B> {
/// 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<B: Requester> Requester for DefaultParseMode<B> {
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<C, Q, O>(&self, chat_id: C, question: Q, options: O) -> Self::SendPoll
where
C: Into<Recipient>,
Q: Into<String>,
O: IntoIterator<Item = String>,
{
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<B>
{ this => this.inner() }
}

View file

@ -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<Throttle<Bot>>` not `Throttle<SomeWrapper<Bot>>`) 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<B> {
bot: B,
// `RequestLock` allows to unlock requests (allowing them to be sent).
queue: mpsc::Sender<(ChatIdHash, RequestLock)>,
info_tx: mpsc::Sender<InfoMessage>,
}
impl<B> Throttle<B> {
/// 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<Output = ()>)
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<Output = ()>)
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)
}
}
}
}

View file

@ -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<R: HasPayload> {
pub(super) request: Arc<R>,
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<R: Request>(#[pin] BoxFuture<'static, Result<Output<R>, R::Err>>);
enum ShareableRequest<R> {
Shared(Arc<R>),
// Option is used to `take` ownership
Owned(Option<R>),
}
impl<R: HasPayload + Clone> HasPayload for ThrottlingRequest<R> {
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<R> Request for ThrottlingRequest<R>
where
R: Request + Clone + Send + Sync + 'static, // TODO: rem static
R::Err: AsResponseParameters + Send,
Output<R>: Send,
{
type Err = R::Err;
type Send = ThrottlingSend<R>;
type SendRef = ThrottlingSend<R>;
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<R> IntoFuture for ThrottlingRequest<R>
where
R: Request + Clone + Send + Sync + 'static,
R::Err: AsResponseParameters + Send,
Output<R>: Send,
{
type Output = Result<Output<Self>, <Self as Request>::Err>;
type IntoFuture = <Self as Request>::Send;
fn into_future(self) -> Self::IntoFuture {
self.send()
}
}
impl<R: Request> Future for ThrottlingSend<R>
where
R::Err: AsResponseParameters,
{
type Output = Result<Output<R>, R::Err>;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
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<R>(
mut request: ShareableRequest<R>,
chat: ChatIdHash,
worker: mpsc::Sender<(ChatIdHash, RequestLock)>,
) -> Result<Output<R>, R::Err>
where
R: Request + Send + Sync + 'static,
R::Err: AsResponseParameters + Send,
Output<R>: 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,
};
}
}

View file

@ -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<FreezeUntil>)>);
#[must_use]
#[pin_project::pin_project]
pub(super) struct RequestWaiter(#[pin] Receiver<(bool, mpsc::Sender<FreezeUntil>)>);
impl RequestLock {
pub(super) fn unlock(self, retry: bool, freeze: mpsc::Sender<FreezeUntil>) -> Result<(), ()> {
self.0.send((retry, freeze)).map_err(drop)
}
}
impl Future for RequestWaiter {
type Output = (bool, mpsc::Sender<FreezeUntil>);
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
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,
}
}
}

View file

@ -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<B::$T>
};
}
macro_rules! fid {
($m:ident $this:ident ($($arg:ident : $T:ty),*)) => {
$this.inner().$m($($arg),*)
};
}
macro_rules! ftyid {
($T:ident) => {
B::$T
};
}
impl<B: Requester> Requester for Throttle<B>
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<B>
{ this => this.inner() }
}

View file

@ -0,0 +1,110 @@
use std::pin::Pin;
use futures::{future::ready, Future};
// Required to not trigger `clippy::type-complexity` lint
type BoxedFnMut<I, O> = Box<dyn FnMut(I) -> O + Send>;
type BoxedFuture = Pin<Box<dyn Future<Output = ()> + 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<usize, BoxedFuture>,
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<F, Fut>(mut self, mut val: F) -> Self
where
F: FnMut(usize) -> Fut + Send + 'static,
Fut: Future<Output = ()> + 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,
}
}
}

View file

@ -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<Limits> },
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<ChatIdHash, RequestsSent>,
per_sec: HashMap<ChatIdHash, RequestsSent>,
}
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<ChatId, Count>`). (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<B>(
Settings { mut limits, mut on_queue_full, retry, check_slow_mode }: Settings,
mut rx: mpsc::Receiver<(ChatIdHash, RequestLock)>,
mut info_rx: mpsc::Receiver<InfoMessage>,
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<HashMap<ChatIdHash, (Duration, Instant)>> =
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::<FreezeUntil>(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<InfoMessage>, 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<FreezeUntil>,
mut slow_mode: Option<&mut HashMap<ChatIdHash, (Duration, Instant)>>,
bot: &impl Requester,
mut imm: Option<FreezeUntil>,
) {
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<T>(rx: &mut mpsc::Receiver<T>, queue: &mut Vec<T>, 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;
}
}

View file

@ -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<B> {
inner: B,
settings: Settings,
}
impl<B> Trace<B> {
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<B::$T>
};
}
macro_rules! fwd_inner {
($m:ident $this:ident ($($arg:ident : $T:ty),*)) => {
TraceRequest {
inner: $this.inner().$m($($arg),*),
settings: $this.settings
}
};
}
impl<B> Requester for Trace<B>
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<R> {
inner: R,
settings: Settings,
}
impl<R> TraceRequest<R>
where
R: Request,
{
fn trace_request(&self)
where
R::Payload: Debug,
{
if self.settings.contains(Settings::TRACE_REQUESTS_VERBOSE) {
log::trace!(
"Sending `{}` request: {:?}",
<R::Payload as Payload>::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<Output<R>, R::Err>)
where
Output<R>: 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<R> HasPayload for TraceRequest<R>
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<R> Request for TraceRequest<R>
where
R: Request,
Output<R>: Debug,
R::Err: Debug,
R::Payload: Debug,
{
type Err = R::Err;
type Send = Send<R::Send>;
type SendRef = Send<R::SendRef>;
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<R> IntoFuture for TraceRequest<R>
where
R: Request,
Output<R>: Debug,
R::Err: Debug,
R::Payload: Debug,
{
type Output = Result<Output<Self>, <Self as Request>::Err>;
type IntoFuture = <Self as Request>::Send;
fn into_future(self) -> Self::IntoFuture {
self.send()
}
}
#[pin_project::pin_project]
pub struct Send<F>
where
F: Future,
{
trace_fn: fn(&F::Output),
#[pin]
inner: F,
}
impl<F> Future for Send<F>
where
F: Future,
{
type Output = F::Output;
fn poll(self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> Poll<Self::Output> {
let this = self.project();
let ret = ready!(this.inner.poll(cx));
(this.trace_fn)(&ret);
Poll::Ready(ret)
}
}

View file

@ -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<str>,
api_url: Arc<reqwest::Url>,
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<S>(token: S) -> Self
where
S: Into<String>,
{
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<S>(token: S, client: Client) -> Self
where
S: Into<String>,
{
let token = Into::<String>::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<P>(
&self,
payload: &P,
) -> impl Future<Output = ResponseResult<P::Output>> + '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&params
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<P>(
&self,
payload: &mut P,
) -> impl Future<Output = ResponseResult<P::Output>>
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&params
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<P>(
&self,
payload: &P,
) -> impl Future<Output = ResponseResult<P::Output>>
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&params
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))
}

File diff suppressed because it is too large Load diff

View file

@ -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<Bytes, Self::StreamErr>>;
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()
}
}

View file

@ -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<Item = (&'a Path, &'a str)>,
) {
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}"))
}

View file

@ -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()),
}
}

View file

@ -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<token>/<file_path>`, where `<file_path>` 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();
}
}

View file

@ -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::<Schema>(&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<Method>,
pub tg_categories: HashMap<String, String>,
}
#[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<Doc>,
pub params: Vec<Param>,
#[serde(default)]
pub sibling: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Doc {
pub md: String,
#[serde(default)]
pub md_links: HashMap<String, String>,
}
#[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<Type>),
ArrayOf(Box<Type>),
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<Utc>"),
}
}
}

View file

@ -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<str>,
},
/// 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<ResponseParameters>;
fn retry_after(&self) -> Option<Duration> {
self.response_parameters().and_then(|rp| match rp {
ResponseParameters::RetryAfter(n) => Some(n),
_ => None,
})
}
fn migrate_to_chat_id(&self) -> Option<i64> {
self.response_parameters().and_then(|rp| match rp {
ResponseParameters::MigrateToChatId(id) => Some(id),
_ => None,
})
}
}
impl AsResponseParameters for crate::RequestError {
fn response_parameters(&self) -> Option<ResponseParameters> {
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\_<bot\_username>”. <bot\_username>
/// 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<DownloadError> 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<reqwest::Error> for DownloadError {
fn from(error: reqwest::Error) -> Self {
DownloadError::Network(hide_token(error))
}
}
impl From<reqwest::Error> 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<token>/..." or "file/bot<token>/...".
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()
}

View file

@ -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<dyn std::error::Error>>(()) };
//! ```
//!
//! <div align="center">
//! <img src=https://user-images.githubusercontent.com/38225716/103929465-6b91e100-512e-11eb-826d-39b096f16548.gif />
//! </div>
//!
//! [`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;

File diff suppressed because it is too large Load diff

View file

@ -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"
);
}
}

View file

@ -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<Output = Result<(), Self::Err>> + 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<dyn std::error::Error>> {
/// 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<Item = Result<Bytes, Self::StreamErr>> + 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<Output = Result<(), DownloadError>> + '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<Item = reqwest::Result<Bytes>> + '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)))),
}
})
}

View file

@ -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<T>(
client: &Client,
token: &str,
api_url: reqwest::Url,
method_name: &str,
params: reqwest::multipart::Form,
_timeout_hint: Option<Duration>,
) -> ResponseResult<T>
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<T>(
client: &Client,
token: &str,
api_url: reqwest::Url,
method_name: &str,
params: Vec<u8>,
_timeout_hint: Option<Duration>,
) -> ResponseResult<T>
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<T>(response: Response) -> ResponseResult<T>
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::<TelegramResponse<T>>(&text)
.map_err(|source| RequestError::InvalidJson { source, raw: text.into() })?
.into()
}

View file

@ -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<R> {
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<ResponseParameters>,
},
}
impl<R> From<TelegramResponse<R>> for ResponseResult<R> {
fn from(this: TelegramResponse<R>) -> ResponseResult<R> {
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::<TelegramResponse<Update>>(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::<TelegramResponse<Update>>(s).unwrap();
assert!(
matches!(val, TelegramResponse::Err { error: ApiError::Unknown(s), .. } if s == "Unknown description that won't match anything")
);
}
}

View file

@ -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;

View file

@ -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,
}
}
}

View file

@ -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,
}
}
}

View file

@ -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<InlineQueryResult> [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],
}
}
}

View file

@ -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],
}
}
}

View file

@ -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<ShippingOption> [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],
}
}
}

View file

@ -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,
}
}
}

View file

@ -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,
}
}
}

View file

@ -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<Utc> [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,
}
}
}

View file

@ -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],
}
}
}

View file

@ -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 {
}
}

View file

@ -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<Item = impl Borrow<Param>>) -> String {
params
.map(|param| {
let param = param.borrow();
let doc = render_doc(&param.descr, None).replace('\n', "\n ");
let field = &param.name;
let ty = &param.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<Vec<&str>> {
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))
}

View file

@ -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<MessageEntity> [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],
}
}
}

View file

@ -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<Utc> [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,
}
}
}

View file

@ -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<LabeledPrice> [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<u32> [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,
}
}
}

View file

@ -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\_<bot username>”. <bot\_username>_ 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,
}
}
}

View file

@ -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,
}
}
}

View file

@ -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],
}
}
}

View file

@ -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],
}
}
}

View file

@ -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,
}
}
}

View file

@ -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],
}
}
}

View file

@ -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],
}
}
}

View file

@ -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,
}
}
}

View file

@ -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<Utc> [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,
}
}
}

View file

@ -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<MessageEntity> [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,
}
}
}

View file

@ -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<MessageEntity> [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,
}
}
}

View file

@ -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],
}
}
}

View file

@ -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],
}
}
}

View file

@ -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,
}
}
}

View file

@ -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,
}
}
}

View file

@ -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,
}
}
}

View file

@ -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,
}
}
}

View file

@ -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<MessageEntity> [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,
}
}
}

View file

@ -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<MessageEntity> [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,
}
}
}

View file

@ -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],
}
}
}

View file

@ -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,
}
}
}

View file

@ -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],
}
}
}

View file

@ -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<ChatMember> {
required {
/// Unique identifier for the target chat or username of the target channel (in the format `@channelusername`)
pub chat_id: Recipient [into],
}
}
}

View file

@ -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,
}
}
}

View file

@ -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],
}
}
}

View file

@ -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],
}
}
}

View file

@ -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],
}
}
}

View file

@ -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<Sticker> {
required {
/// List of custom emoji identifiers. At most 200 custom emoji identifiers can be specified.
pub custom_emoji_ids: Vec<String> [collect],
}
}
}

View file

@ -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<token>/<file_path>`, where `<file_path>` 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],
}
}
}

View file

@ -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],
}
}
}

View file

@ -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 {
}
}

View file

@ -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<BotCommand> {
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],
}
}
}

View file

@ -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,
}
}
}

View file

@ -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],
}
}
}

View file

@ -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<Update> {
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<AllowedUpdate> [collect],
}
}
}

View file

@ -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,
}
}
}

View file

@ -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 {
}
}

View file

@ -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<Utc> [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,
}
}
}

View file

@ -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],
}
}
}

View file

@ -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 {
}
}

View file

@ -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,
}
}
}

View file

@ -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,
}
}
}

View file

@ -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<Utc> [into],
}
}
}

View file

@ -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],
}
}
}

View file

@ -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://<file_attach_name>” if the thumbnail was uploaded using multipart/form-data under <file_attach_name>. [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<MessageEntity> [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],
}
}
}

View file

@ -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<MessageEntity> [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://<file_attach_name>” if the thumbnail was uploaded using multipart/form-data under <file_attach_name>. [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],
}
}
}

View file

@ -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,
}
}
}

Some files were not shown because too many files have changed in this diff Show more