diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dba02ce0..c2ccac7e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -123,6 +123,7 @@ jobs: args: --tests --verbose ${{ matrix.features }} - name: Test documentation tests + if: ${{ matrix.rust != 'msrv' }} uses: actions-rs/cargo@v1 with: command: test diff --git a/CHANGELOG.md b/CHANGELOG.md index 67ef18dd..2198111f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,23 +6,31 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## unreleased +## 0.10.0 - 2022-07-21 + ### Added -- Security checks based on `secret_token` param of `set_webhook` to built-in webhooks -- `dispatching::update_listeners::{PollingBuilder, Polling, PollingStream}` + - Security checks based on `secret_token` param of `set_webhook` to built-in webhooks. + - `dispatching::update_listeners::{PollingBuilder, Polling, PollingStream}`. + - `DispatcherBuilder::enable_ctrlc_handler` method. ### Fixed - `Dispatcher` no longer "leaks" memory for every inactive user ([PR 657](https://github.com/teloxide/teloxide/pull/657)). + - Allow specifying a path to a custom command parser in `parse_with` ([issue 668](https://github.com/teloxide/teloxide/issues/668)). ### Changed - Add the `Key: Clone` requirement for `impl Dispatcher` [**BC**]. - - `dispatching::update_listeners::{polling_default, polling}` now return a named, `Polling<_>` type + - `dispatching::update_listeners::{polling_default, polling}` now return a named, `Polling<_>` type. + - Update teloxide-core to v0.7.0 with Bot API 6.1 support, see [its changelog][core07c] for more information [**BC**]. + +[core07c]: https://github.com/teloxide/teloxide-core/blob/master/CHANGELOG.md#070---2022-07-19 ### Deprecated -- `dispatching::update_listeners::polling` +- The `dispatching::update_listeners::polling` function. +- `Dispatcher::setup_ctrlc_handler` method. ## 0.9.2 - 2022-06-07 diff --git a/Cargo.toml b/Cargo.toml index 5dfc708b..df04f34e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "teloxide" -version = "0.9.2" +version = "0.10.0" edition = "2021" description = "An elegant Telegram bots framework for Rust" repository = "https://github.com/teloxide/teloxide" @@ -57,17 +57,21 @@ full = [ ] [dependencies] -#teloxide-core = { version = "0.6.0", default-features = false } -teloxide-core = { git = "https://github.com/teloxide/teloxide-core", rev = "b13393d", default-features = false } -teloxide-macros = { version = "0.6.2", optional = true } +teloxide-core = { version = "0.7.0", default-features = false } +teloxide-macros = { version = "0.6.3", optional = true } serde_json = "1.0" serde = { version = "1.0", features = ["derive"] } -dptree = "0.2.1" +dptree = "0.3.0" + +# These lines are used only for development. +# teloxide-core = { git = "https://github.com/teloxide/teloxide-core", rev = "b13393d", default-features = false } +# teloxide-macros = { git = "https://github.com/teloxide/teloxide-macros", rev = "44d91c5", optional = true } +# dptree = { git = "https://github.com/teloxide/dptree", rev = "df578e4" } tokio = { version = "1.8", features = ["fs"] } -tokio-util = "0.6" +tokio-util = "0.7" tokio-stream = "0.1.8" url = "2.2.2" @@ -82,17 +86,17 @@ pin-project = "1.0" serde_with_macros = "1.4" aquamarine = "0.1.11" -sqlx = { version = "0.5", optional = true, default-features = false, features = [ +sqlx = { version = "0.6", optional = true, default-features = false, features = [ "runtime-tokio-native-tls", "macros", "sqlite", ] } -redis = { version = "0.20", features = ["tokio-comp"], optional = true } +redis = { version = "0.21", features = ["tokio-comp"], optional = true } serde_cbor = { version = "0.11", optional = true } bincode = { version = "1.3", optional = true } -axum = { version = "0.4.8", optional = true } +axum = { version = "0.5.13", optional = true } tower = { version = "0.4.12", optional = true } -tower-http = { version = "0.2.5", features = ["trace"], optional = true } +tower-http = { version = "0.3.4", features = ["trace"], optional = true } rand = { version = "0.8.5", optional = true } [dev-dependencies] @@ -101,7 +105,7 @@ pretty_env_logger = "0.4.0" serde = "1" serde_json = "1" tokio = { version = "1.8", features = ["fs", "rt-multi-thread", "macros"] } -reqwest = "0.10.4" +reqwest = "0.11.11" chrono = "0.4" tokio-stream = "0.1" diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index 315cd9f1..78003f48 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -1,6 +1,47 @@ This document describes breaking changes of `teloxide` crate, as well as the ways to update code. Note that the list of required changes is not fully exhaustive and it may lack something in rare cases. +## 0.9 -> 0.10 + +### core + +We've added some convenience functions to `InlineKeyboardButton` so it's easier to construct it. Consider using them instead of variants: +```diff +-InlineKeyboardButton::new("text", InlineKeyboardButtonKind::Url(url)) ++InlineKeyboardButton::url("text", url) +``` + +`file_size` fields are now `u32`, you may need to update your code accordingly: + +```diff +-let file_size: u64 = audio.file_size?; ++let file_size: u32 = audio.file_size; +``` + +Some places now use `FileMeta` instead of `File`, you may need to change types. + +`Sticker` and `StickerSet` now has a `kind` field instead of `is_animated` and `is_video`: + +```diff ++use teloxide::types::StickerKind::*; +-match () { ++match sticker.kind { +- _ if sticker.is_animated => /* handle animated */, ++ Animated => /* handle animated */, +- _ if sticker.is_video => /* handle video */, ++ Video => /* handle video */, +- _ => /* handle normal */, ++ Webp => /* handle normal */, +} +``` + +### teloxide + +Teloxide itself doesn't have any major API changes. +Note however that some function were deprecated: +- Instead of `dispatching::update_listeners::polling` use `polling_builder` +- Instead of `Dispatcher::setup_ctrlc_handler` use `DispatcherBuilder::enable_ctrlc_handler` + ## 0.7 -> 0.8 ### core @@ -8,7 +49,7 @@ Note that the list of required changes is not fully exhaustive and it may lack s `user.id` now uses `UserId` type, `ChatId` now represents only _chat id_, not channel username, all `chat_id` function parameters now accept `Recipient` (if they allow for channel usernames). If you used to work with chat/user ids (for example saving them to a database), you may need to change your code to account for new types. Some examples how that may look like: -```diff, +```diff -let user_id: i64 = user.id; +let UserId(user_id) = user.id; db.save(user_id, ...); diff --git a/README.md b/README.md index 8a5e386d..b05f48f8 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,4 @@ -> [v0.7 -> v0.8 migration guide >>](MIGRATION_GUIDE.md#07---08) - -> `teloxide-core` versions less that `0.4.5` (`teloxide` versions less than 0.7.3) have a low-severity security vulnerability, [learn more >>](https://github.com/teloxide/teloxide/discussions/574) +> [v0.9 -> v0.10 migration guide >>](MIGRATION_GUIDE.md#09---010)
@@ -15,7 +13,7 @@ - + @@ -72,7 +70,7 @@ $ rustup override set nightly 5. Run `cargo new my_bot`, enter the directory and put these lines into your `Cargo.toml`: ```toml [dependencies] -teloxide = { version = "0.9", features = ["macros", "auto-send"] } +teloxide = { version = "0.10", features = ["macros", "auto-send"] } log = "0.4" pretty_env_logger = "0.4" tokio = { version = "1.8", features = ["rt-multi-thread", "macros"] } @@ -225,8 +223,8 @@ async fn main() { ), ) .dependencies(dptree::deps![InMemStorage::::new()]) + .enable_ctrlc_handler() .build() - .setup_ctrlc_handler() .dispatch() .await; } @@ -321,11 +319,7 @@ A: No, only the bots API. **Q: Can I use webhooks?** -A: teloxide doesn't provide a special API for working with webhooks due to their nature with lots of subtle settings. Instead, you should setup your webhook by yourself, as shown in [`examples/ngrok_ping_pong_bot`](examples/ngrok_ping_pong.rs) and [`examples/heroku_ping_pong_bot`](examples/heroku_ping_pong.rs). - -Associated links: - - [Marvin's Marvellous Guide to All Things Webhook](https://core.telegram.org/bots/webhooks) - - [Using self-signed certificates](https://core.telegram.org/bots/self-signed) +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). **Q: Can I handle both callback queries and messages within a single dialogue?** @@ -335,32 +329,32 @@ A: Yes, see [`examples/purchase.rs`](examples/purchase.rs). Feel free to propose your own bot to our collection! - - [WaffleLapkin/crate_upd_bot](https://github.com/WaffleLapkin/crate_upd_bot) — A bot that notifies about crate updates. - - [mxseev/logram](https://github.com/mxseev/logram) — Utility that takes logs from anywhere and sends them to Telegram. - - [alexkonovalov/PedigreeBot](https://github.com/alexkonovalov/PedigreeBot) — A Telegram bot for building family trees. - - [Hermitter/tepe](https://github.com/Hermitter/tepe) — A CLI to command a bot to send messages and files over Telegram. - - [mattrighetti/GroupActivityBot](https://github.com/mattrighetti/group-activity-bot-rs) — Telegram bot that keeps track of user activity in groups. - - [mattrighetti/libgen-bot-rs](https://github.com/mattrighetti/libgen-bot-rs) — Telgram bot to interface with libgen - - [dracarys18/grpmr-rs](https://github.com/dracarys18/grpmr-rs) — A Telegram group manager bot with variety of extra features. - - [steadylearner/subreddit_reader](https://github.com/steadylearner/Rust-Full-Stack/tree/master/commits/teloxide/subreddit_reader) — A bot that shows the latest posts at Rust subreddit. - - [myblackbeard/basketball-betting-bot](https://github.com/myblackbeard/basketball-betting-bot) — The bot lets you bet on NBA games against your buddies. - - [ArtHome12/vzmuinebot](https://github.com/ArtHome12/vzmuinebot) — Telegram bot for food menu navigate. - - [ArtHome12/cognito_bot](https://github.com/ArtHome12/cognito_bot) — The bot is designed to anonymize messages to a group. - - [pro-vim/tg-vimhelpbot](https://github.com/pro-vim/tg-vimhelpbot) — Link `:help` for Vim in Telegram. - - [sschiz/janitor-bot](https://github.com/sschiz/janitor-bot) — A bot that removes users trying to join to a chat that is designed for comments. - - [slondr/BeerHolderBot](https://gitlab.com/slondr/BeerHolderBot) — A bot that holds your beer. - - [MustafaSalih1993/Miss-Vodka-Telegram-Bot](https://github.com/MustafaSalih1993/Miss-Vodka-Telegram-Bot) — A Telegram bot written in rust using "Teloxide" library. - - [x13a/tg-prompt](https://github.com/x13a/tg-prompt) — Telegram prompt. - - [magnickolas/remindee-bot](https://github.com/magnickolas/remindee-bot) — Telegram bot for managing reminders. - - [cyberknight777/knight-bot](https://gitlab.com/cyberknight777/knight-bot) — A Telegram bot with variety of fun features. - - [wa7sa34cx/the-black-box-bot](https://github.com/wa7sa34cx/the-black-box-bot) — This is the Black Box Telegram bot. You can hold any items in it. - - [crapstone/hsctt](https://codeberg.org/crapstones-bots/hsctt) — A Telegram bot that searches for HTTP status codes in all messages and replies with the text form. - - [alenpaul2001/AurSearchBot](https://gitlab.com/alenpaul2001/aursearchbot) — Telegram bot for searching AUR in inline mode. - - [studiedlist/EddieBot](https://gitlab.com/studiedlist/eddie-bot) — Chatting bot with several entertainment features. - - [modos189/tg_blackbox_bot](https://gitlab.com/modos189/tg_blackbox_bot) — Anonymous feedback for your Telegram project. This bot in Docker from scratch container. - - [0xNima/spacecraft](https://github.com/0xNima/spacecraft) — Yet another telegram bot to downloading Twitter spaces. - - [0xNima/Twideo](https://github.com/0xNima/Twideo) — Telegram Bot for downloading videos from Twitter via their links, as well as converting tweets to telegram messages. - - [raine/tgreddit](https://github.com/raine/tgreddit) — A bot that sends the top posts of your favorite subreddits to Telegram. + - [`raine/tgreddit`](https://github.com/raine/tgreddit) — A bot that sends the top posts of your favorite subreddits to Telegram. + - [`magnickolas/remindee-bot`](https://github.com/magnickolas/remindee-bot) — Telegram bot for managing reminders. + - [`WaffleLapkin/crate_upd_bot`](https://github.com/WaffleLapkin/crate_upd_bot) — A bot that notifies about crate updates. + - [`mattrighetti/GroupActivityBot`](https://github.com/mattrighetti/group-activity-bot-rs) — Telegram bot that keeps track of user activity in groups. + - [`alenpaul2001/AurSearchBot`](https://gitlab.com/alenpaul2001/aursearchbot) — Telegram bot for searching in Arch User Repository (AUR). + - [`ArtHome12/vzmuinebot`](https://github.com/ArtHome12/vzmuinebot) — Telegram bot for food menu navigate. + - [`studiedlist/EddieBot`](https://gitlab.com/studiedlist/eddie-bot) — Chatting bot with several entertainment features. + - [`modos189/tg_blackbox_bot`](https://gitlab.com/modos189/tg_blackbox_bot) — Anonymous feedback for your Telegram project. + - [`0xNima/spacecraft`](https://github.com/0xNima/spacecraft) — Yet another telegram bot to downloading Twitter spaces. + - [`0xNima/Twideo`](https://github.com/0xNima/Twideo) — Simple Telegram Bot for downloading videos from Twitter via their links. + - [`mattrighetti/libgen-bot-rs`](https://github.com/mattrighetti/libgen-bot-rs) — Telgram bot to interface with libgen. + - [`zamazan4ik/npaperbot-telegram`](https://github.com/zamazan4ik/npaperbot-telegram) — Telegram bot for searching via C++ proposals. + +
+Show bots using teloxide older than v0.6.0 + + - [`mxseev/logram`](https://github.com/mxseev/logram) — Utility that takes logs from anywhere and sends them to Telegram. + - [`alexkonovalov/PedigreeBot`](https://github.com/alexkonovalov/PedigreeBot) — A Telegram bot for building family trees. + - [`Hermitter/tepe`](https://github.com/Hermitter/tepe) — A CLI to command a bot to send messages and files over Telegram. + - [`myblackbeard/basketball-betting-bot`](https://github.com/myblackbeard/basketball-betting-bot) — The bot lets you bet on NBA games against your buddies. + - [`dracarys18/grpmr-rs`](https://github.com/dracarys18/grpmr-rs) — Modular Telegram Group Manager Bot written in Rust. + - [`ArtHome12/vzmuinebot`](https://github.com/ArtHome12/vzmuinebot) — Telegram bot for food menu navigate. + - [`ArtHome12/cognito_bot`](https://github.com/ArtHome12/cognito_bot) — The bot is designed to anonymize messages to a group. + - [`crapstone/hsctt`](https://codeberg.org/crapstones-bots/hsctt) — A bot that converts HTTP status codes into text. + +
## Contributing diff --git a/examples/buttons.rs b/examples/buttons.rs index 5b16fe35..b510e24a 100644 --- a/examples/buttons.rs +++ b/examples/buttons.rs @@ -30,7 +30,7 @@ async fn main() -> Result<(), Box> { .branch(Update::filter_callback_query().endpoint(callback_handler)) .branch(Update::filter_inline_query().endpoint(inline_query_handler)); - Dispatcher::builder(bot, handler).build().setup_ctrlc_handler().dispatch().await; + Dispatcher::builder(bot, handler).enable_ctrlc_handler().build().dispatch().await; Ok(()) } diff --git a/examples/db_remember.rs b/examples/db_remember.rs index 206768d9..1aed2808 100644 --- a/examples/db_remember.rs +++ b/examples/db_remember.rs @@ -54,8 +54,8 @@ async fn main() { Dispatcher::builder(bot, handler) .dependencies(dptree::deps![storage]) + .enable_ctrlc_handler() .build() - .setup_ctrlc_handler() .dispatch() .await; } diff --git a/examples/dialogue.rs b/examples/dialogue.rs index 30636924..8e30219d 100644 --- a/examples/dialogue.rs +++ b/examples/dialogue.rs @@ -51,8 +51,8 @@ async fn main() { ), ) .dependencies(dptree::deps![InMemStorage::::new()]) + .enable_ctrlc_handler() .build() - .setup_ctrlc_handler() .dispatch() .await; } diff --git a/examples/dispatching_features.rs b/examples/dispatching_features.rs index 91ef0808..dcaf4fcc 100644 --- a/examples/dispatching_features.rs +++ b/examples/dispatching_features.rs @@ -87,8 +87,8 @@ async fn main() { .error_handler(LoggingErrorHandler::with_custom_text( "An error has occurred in the dispatcher", )) + .enable_ctrlc_handler() .build() - .setup_ctrlc_handler() .dispatch() .await; } diff --git a/examples/inline.rs b/examples/inline.rs index a85de2c2..837fa30d 100644 --- a/examples/inline.rs +++ b/examples/inline.rs @@ -60,5 +60,5 @@ async fn main() { }, )); - Dispatcher::builder(bot, handler).build().setup_ctrlc_handler().dispatch().await; + Dispatcher::builder(bot, handler).enable_ctrlc_handler().build().dispatch().await; } diff --git a/examples/purchase.rs b/examples/purchase.rs index 74a0cd6d..0eac1ad3 100644 --- a/examples/purchase.rs +++ b/examples/purchase.rs @@ -13,10 +13,7 @@ // ``` use teloxide::{ - dispatching::{ - dialogue::{self, InMemStorage}, - UpdateHandler, - }, + dispatching::{dialogue, dialogue::InMemStorage, UpdateHandler}, prelude::*, types::{InlineKeyboardButton, InlineKeyboardMarkup}, utils::command::BotCommands, @@ -55,29 +52,30 @@ async fn main() { Dispatcher::builder(bot, schema()) .dependencies(dptree::deps![InMemStorage::::new()]) + .enable_ctrlc_handler() .build() - .setup_ctrlc_handler() .dispatch() .await; } fn schema() -> UpdateHandler> { + use dptree::case; + let command_handler = teloxide::filter_command::() .branch( - dptree::case![State::Start] - .branch(dptree::case![Command::Help].endpoint(help)) - .branch(dptree::case![Command::Start].endpoint(start)), + case![State::Start] + .branch(case![Command::Help].endpoint(help)) + .branch(case![Command::Start].endpoint(start)), ) - .branch(dptree::case![Command::Cancel].endpoint(cancel)); + .branch(case![Command::Cancel].endpoint(cancel)); let message_handler = Update::filter_message() .branch(command_handler) - .branch(dptree::case![State::ReceiveFullName].endpoint(receive_full_name)) + .branch(case![State::ReceiveFullName].endpoint(receive_full_name)) .branch(dptree::endpoint(invalid_state)); - let callback_query_handler = Update::filter_callback_query().chain( - dptree::case![State::ReceiveProductChoice { full_name }] - .endpoint(receive_product_selection), + let callback_query_handler = Update::filter_callback_query().branch( + case![State::ReceiveProductChoice { full_name }].endpoint(receive_product_selection), ); dialogue::enter::, State, _>() diff --git a/examples/shared_state.rs b/examples/shared_state.rs index c09fab93..21a0fcc0 100644 --- a/examples/shared_state.rs +++ b/examples/shared_state.rs @@ -27,8 +27,8 @@ async fn main() { Dispatcher::builder(bot, handler) // Pass the shared state to the handler as a dependency. .dependencies(dptree::deps![messages_total]) + .enable_ctrlc_handler() .build() - .setup_ctrlc_handler() .dispatch() .await; } diff --git a/src/dispatching.rs b/src/dispatching.rs index 1c093048..e0440705 100644 --- a/src/dispatching.rs +++ b/src/dispatching.rs @@ -1,76 +1,198 @@ //! An update dispatching model based on [`dptree`]. //! -//! In teloxide, updates are dispatched by a pipeline. The central type is -//! [`dptree::Handler`] -- it represents a handler of an update; since the API -//! is highly declarative, you can combine handlers with each other via such -//! methods as [`dptree::Handler::chain`] and [`dptree::Handler::branch`]. The -//! former method pipes one handler to another one, whilst the latter creates a -//! new node, as communicated by the name. For more information, please refer to -//! the documentation of [`dptree`]. +//! In teloxide, update dispatching is declarative: it takes the form of a +//! [chain of responsibility] pattern enriched with a number of combinator +//! functions, which together form an instance of the [`dptree::Handler`] type. //! -//! The pattern itself is called [chain of responsibility], a well-known design -//! technique across OOP developers. But unlike typical object-oriented design, -//! we employ declarative FP-style functions like [`dptree::filter`], -//! [`dptree::filter_map`], and [`dptree::endpoint`]; these functions create -//! special forms of [`dptree::Handler`]; for more information, please refer to -//! their respective documentation. Each of these higher-order functions accept -//! a closure that is made into a handler -- this closure can take any -//! additional parameters, which must be supplied while creating [`Dispatcher`] -//! (see [`DispatcherBuilder::dependencies`]). -//! -//! The [`Dispatcher`] type puts all these things together: it only provides -//! [`Dispatcher::dispatch`] and a handful of other methods. Once you call -//! `.dispatch()`, it will retrieve updates from the Telegram server and pass -//! them to your handler, which is a parameter of [`Dispatcher::builder`]. -//! -//! Let us look at a simple example: -//! -//! -//! ([Full](https://github.com/teloxide/teloxide/blob/master/examples/shared_state.rs)) +//! Take [`examples/purchase.rs`] as an example of dispatching logic. First, we +//! define a type named `State` to represent the current state of a dialogue: //! //! ```no_run -//! // TODO: examples/purchase.rs -//! fn main() {} +//! #[derive(Clone, Default)] +//! pub enum State { +//! #[default] +//! Start, +//! ReceiveFullName, +//! ReceiveProductChoice { +//! full_name: String, +//! }, +//! } //! ``` //! -//! 1. First, we create the bot: `let bot = Bot::from_env().auto_send()`. -//! 2. Then we construct an update handler. While it is possible to handle all -//! kinds of [`crate::types::Update`], here we are only interested in -//! [`crate::types::Message`]: [`UpdateFilterExt::filter_message`] create a -//! handler object which filters all messages out of a generic update. -//! 3. By doing `.endpoint(...)` we set up a custom handling closure that -//! receives `msg: Message` and `bot: AutoSend`. There are -//! called dependencies: `msg` is supplied by -//! [`UpdateFilterExt::filter_message`], while `bot` is supplied by -//! [`Dispatcher`]. +//! Then, we define a type `Command` to represent user commands such as +//! `/start` or `/help`: //! -//! That being said, if we receive a message, the dispatcher will call our -//! handler, but if we receive something other than a message (e.g., a channel -//! post), you will see an unhandled update notice in your terminal. +//! ```no_run +//! # use teloxide::utils::command::BotCommands; +//! #[derive(BotCommands, Clone)] +//! #[command(rename = "lowercase", description = "These commands are supported:")] +//! enum Command { +//! #[command(description = "display this text.")] +//! Help, +//! #[command(description = "start the purchase procedure.")] +//! Start, +//! #[command(description = "cancel the purchase procedure.")] +//! Cancel, +//! } +//! ``` //! -//! This is a very limited example of update pipelining facilities. In more -//! involved scenarios, there are multiple branches and chains; if one element -//! of a chain fails to handle an update, the update will be passed forwards; if -//! no handler succeeds at handling the update, [`Dispatcher`] will invoke a -//! default handler set up via [`DispatcherBuilder::default_handler`]. +//! Now the key question: how to elegantly dispatch on different combinations of +//! `State`, `Command`, and Telegram updates? -- i.e., we may want to execute +//! specific endpoints only in response to specific user commands and while we +//! are in a given dialogue state (and possibly under other circumstances!). The +//! solution is to use [`dptree`]: //! -//! Update pipelining provides several advantages over the typical `match -//! (update.kind) { ... }` approach: +//! ```no_run +//! # // That's a lot of context needed to compile this, oof +//! # use teloxide::dispatching::{UpdateHandler, UpdateFilterExt, dialogue, dialogue::InMemStorage}; +//! # use teloxide::utils::command::BotCommands; +//! # use teloxide::types::Update; +//! # #[derive(Clone, Default)] pub enum State { #[default] Start, ReceiveFullName, ReceiveProductChoice { full_name: String } } +//! # #[derive(BotCommands, Clone)] enum Command { Help, Start, Cancel } +//! # type HandlerResult = Result<(), Box>; +//! # async fn help() -> HandlerResult { Ok(()) } +//! # async fn start() -> HandlerResult { Ok(()) } +//! # async fn cancel() -> HandlerResult { Ok(()) } +//! # async fn receive_full_name() -> HandlerResult { Ok(()) } +//! # async fn invalid_state() -> HandlerResult { Ok(()) } +//! # async fn receive_product_selection() -> HandlerResult { Ok(()) } +//! # +//! fn schema() -> UpdateHandler> { +//! use dptree::case; //! -//! 1. It supports _extension_: e.g., you -//! can define extension filters or some other handlers and then combine them in -//! a single place, thus facilitating loose coupling. -//! 2. Pipelining exhibits a natural syntax for expressing message processing. -//! 3. Lastly, it provides a primitive form of [dependency injection (DI)], -//! which allows you to deal with such objects as a bot and various update types -//! easily. +//! let command_handler = teloxide::filter_command::() +//! .branch( +//! case![State::Start] +//! .branch(case![Command::Help].endpoint(help)) +//! .branch(case![Command::Start].endpoint(start)), +//! ) +//! .branch(case![Command::Cancel].endpoint(cancel)); //! -//! For a more involved example, see [`examples/dispatching_features.rs`](https://github.com/teloxide/teloxide/blob/master/examples/dispatching_features.rs). +//! let message_handler = Update::filter_message() +//! .branch(command_handler) +//! .branch(case![State::ReceiveFullName].endpoint(receive_full_name)) +//! .branch(dptree::endpoint(invalid_state)); //! -//! TODO: explain a more involved example with multiple branches. +//! let callback_query_handler = Update::filter_callback_query().branch( +//! case![State::ReceiveProductChoice { full_name }].endpoint(receive_product_selection), +//! ); //! +//! dialogue::enter::, State, _>() +//! .branch(message_handler) +//! .branch(callback_query_handler) +//! } +//! ``` +//! +//! The overall logic should be clear. Throughout the above example, we use +//! several techniques: +//! +//! - **Branching:** `a.branch(b)` roughly means "try to handle an update with +//! `a`, then, if it +//! neglects the update, try `b`". +//! - **Pattern matching:** We also use the [`dptree::case!`] macro +//! extensively, which acts as a filter on an enumeration: if it is of a +//! certain variant, it passes the variant's payload down the handler chain; +//! otherwise, it neglects an update. +//! - **Endpoints:** To specify the final function to handle an update, we use +//! [`dptree::Handler::endpoint`]. +//! +//! Notice the clear and uniform code structure: regardless of the dispatch +//! criteria, we use the same program constructions. In future, you may want to +//! introduce your application-specific filters or data structures to match upon +//! -- no problem, reuse [`dptree::Handler::filter`], [`dptree::case!`], and +//! other combinators in the same way! +//! +//! Finally, we define our endpoints like this: +//! +//! ```no_run +//! # use teloxide::{Bot, adaptors::AutoSend}; +//! # use teloxide::types::{Message, CallbackQuery}; +//! # use teloxide::dispatching::dialogue::{InMemStorage, Dialogue}; +//! # enum State{} +//! # +//! type MyDialogue = Dialogue>; +//! type HandlerResult = Result<(), Box>; +//! +//! async fn start(bot: AutoSend, msg: Message, dialogue: MyDialogue) -> HandlerResult { +//! todo!() +//! } +//! +//! async fn help(bot: AutoSend, msg: Message) -> HandlerResult { +//! todo!() +//! } +//! +//! async fn cancel(bot: AutoSend, msg: Message, dialogue: MyDialogue) -> HandlerResult { +//! todo!() +//! } +//! +//! async fn invalid_state(bot: AutoSend, msg: Message) -> HandlerResult { +//! todo!() +//! } +//! +//! async fn receive_full_name( +//! bot: AutoSend, +//! msg: Message, +//! dialogue: MyDialogue, +//! ) -> HandlerResult { +//! todo!() +//! } +//! +//! async fn receive_product_selection( +//! bot: AutoSend, +//! q: CallbackQuery, +//! dialogue: MyDialogue, +//! full_name: String, +//! ) -> HandlerResult { +//! todo!() +//! } +//! ``` +//! +//! Each parameter is supplied as a dependency by teloxide. In particular: +//! - `bot: AutoSend` comes from the dispatcher (see below); +//! - `msg: Message` comes from [`Update::filter_message`]; +//! - `q: CallbackQuery` comes from [`Update::filter_callback_query`]; +//! - `dialogue: MyDialogue` comes from [`dialogue::enter`]; +//! - `full_name: String` comes from `dptree::case![State::ReceiveProductChoice +//! { full_name }]`. +//! +//! Inside `main`, we plug the schema into [`Dispatcher`] like this: +//! +//! ```no_run +//! # use teloxide::Bot; +//! # use teloxide::requests::RequesterExt; +//! # use teloxide::dispatching::{Dispatcher, dialogue::InMemStorage}; +//! # enum State {} +//! # fn schema() -> teloxide::dispatching::UpdateHandler> { teloxide::dptree::entry() } +//! #[tokio::main] +//! async fn main() { +//! let bot = Bot::from_env().auto_send(); +//! +//! Dispatcher::builder(bot, schema()) +//! .dependencies(dptree::deps![InMemStorage::::new()]) +//! .enable_ctrlc_handler() +//! .build() +//! .dispatch() +//! .await; +//! } +//! ``` +//! +//! In a call to [`DispatcherBuilder::dependencies`], we specify a list of +//! additional dependencies that all handlers will receive as parameters. Here, +//! we only specify an in-memory storage of dialogues needed for +//! [`dialogue::enter`]. However, in production bots, you normally also pass a +//! database connection, configuration, and other stuff. +//! +//! All in all, [`dptree`] can be seen as an extensible alternative to pattern +//! matching, with support for [dependency injection (DI)] and a few other +//! useful features. See [`examples/dispatching_features.rs`] as a more involved +//! example. +//! +//! [`examples/purchase.rs`]: https://github.com/teloxide/teloxide/blob/master/examples/purchase.rs +//! [`Update::filter_message`]: crate::types::Update::filter_message +//! [`Update::filter_callback_query`]: crate::types::Update::filter_callback_query //! [chain of responsibility]: https://en.wikipedia.org/wiki/Chain-of-responsibility_pattern //! [dependency injection (DI)]: https://en.wikipedia.org/wiki/Dependency_injection +//! [`examples/dispatching_features.rs`]: https://github.com/teloxide/teloxide/blob/master/examples/dispatching_features.rs #[cfg(all(feature = "ctrlc_handler"))] pub mod repls; diff --git a/src/dispatching/dialogue.rs b/src/dispatching/dialogue.rs index 83b0239d..3af1ecc0 100644 --- a/src/dispatching/dialogue.rs +++ b/src/dispatching/dialogue.rs @@ -4,10 +4,11 @@ //! wrapper over [`Storage`] and a chat ID. All it does is provides convenient //! method for manipulating the dialogue state. [`Storage`] is where all //! dialogue states are stored; it can be either [`InMemStorage`], which is a -//! simple hash map, or database wrappers such as [`SqliteStorage`]. In the -//! latter case, your dialogues are _persistent_, meaning that you can safely -//! restart your bot and all dialogues will remain in the database -- this is a -//! preferred method for production bots. +//! simple hash map from [`std::collections`], or an advanced database wrapper +//! such as [`SqliteStorage`]. In the latter case, your dialogues are +//! _persistent_, meaning that you can safely restart your bot and all ongoing +//! dialogues will remain in the database -- this is a preferred method for +//! production bots. //! //! [`examples/dialogue.rs`] clearly demonstrates the typical usage of //! dialogues. Your dialogue state can be represented as an enumeration: @@ -31,8 +32,8 @@ //! bot: AutoSend, //! msg: Message, //! dialogue: MyDialogue, -//! (full_name,): (String,), // Available from `State::ReceiveAge`. -//! ) -> anyhow::Result<()> { +//! full_name: String, // Available from `State::ReceiveAge`. +//! ) -> HandlerResult { //! match msg.text().map(|text| text.parse::()) { //! Some(Ok(age)) => { //! bot.send_message(msg.chat.id, "What's your location?").await?; @@ -47,11 +48,12 @@ //! } //! ``` //! -//! Variant's fields are passed to state handlers as tuples: `(full_name,): -//! (String,)`. Using [`Dialogue::update`], you can update the dialogue with a -//! new state, in our case -- `State::ReceiveLocation { full_name, age }`. To -//! exit the dialogue, just call [`Dialogue::exit`] and it will be removed from -//! the inner storage: +//! Variant's fields are passed to state handlers as single arguments like +//! `full_name: String` or tuples in case of two or more variant parameters (see +//! below). Using [`Dialogue::update`], you can update the dialogue with a new +//! state, in our case -- `State::ReceiveLocation { full_name, age }`. To exit +//! the dialogue, just call [`Dialogue::exit`] and it will be removed from the +//! underlying storage: //! //! ```ignore //! async fn receive_location( diff --git a/src/dispatching/dispatcher.rs b/src/dispatching/dispatcher.rs index 74eace82..6ff568e6 100644 --- a/src/dispatching/dispatcher.rs +++ b/src/dispatching/dispatcher.rs @@ -33,6 +33,7 @@ pub struct DispatcherBuilder { handler: Arc>, default_handler: DefaultHandler, error_handler: Arc + Send + Sync>, + ctrlc_handler: bool, distribution_f: fn(&Update) -> Option, worker_queue_size: usize, } @@ -78,6 +79,14 @@ where Self { dependencies, ..self } } + /// Enables the `^C` handler that [`shutdown`]s dispatching. + /// + /// [`shutdown`]: ShutdownToken::shutdown + #[cfg(feature = "ctrlc_handler")] + pub fn enable_ctrlc_handler(self) -> Self { + Self { ctrlc_handler: true, ..self } + } + /// Specifies size of the queue for workers. /// /// By default it's 64. @@ -101,6 +110,7 @@ where handler, default_handler, error_handler, + ctrlc_handler, distribution_f: _, worker_queue_size, } = self; @@ -111,6 +121,7 @@ where handler, default_handler, error_handler, + ctrlc_handler, distribution_f: f, worker_queue_size, } @@ -127,9 +138,10 @@ where error_handler, distribution_f, worker_queue_size, + ctrlc_handler, } = self; - Dispatcher { + let dp = Dispatcher { bot, dependencies, handler, @@ -142,7 +154,18 @@ where default_worker: None, current_number_of_active_workers: Default::default(), max_number_of_active_workers: Default::default(), + }; + + #[cfg(feature = "ctrlc_handler")] + { + if ctrlc_handler { + let mut dp = dp; + dp.setup_ctrlc_handler_inner(); + return dp; + } } + + dp } } @@ -212,6 +235,7 @@ where Box::pin(async {}) }), error_handler: LoggingErrorHandler::new(), + ctrlc_handler: false, worker_queue_size: DEFAULT_WORKER_QUEUE_SIZE, distribution_f: default_distribution_function, } @@ -238,7 +262,6 @@ where /// - [`crate::types::Me`] (can be used in [`HandlerExt::filter_command`]). /// /// [`shutdown`]: ShutdownToken::shutdown - /// [a ctrlc signal]: Dispatcher::setup_ctrlc_handler /// [`HandlerExt::filter_command`]: crate::dispatching::HandlerExt::filter_command pub async fn dispatch(&mut self) where @@ -258,7 +281,6 @@ where /// This method adds the same dependencies as [`Dispatcher::dispatch`]. /// /// [`shutdown`]: ShutdownToken::shutdown - /// [a ctrlc signal]: Dispatcher::setup_ctrlc_handler pub async fn dispatch_with_listener<'a, UListener, ListenerE, Eh>( &'a mut self, mut update_listener: UListener, @@ -425,7 +447,22 @@ where /// /// [`shutdown`]: ShutdownToken::shutdown #[cfg(feature = "ctrlc_handler")] + #[deprecated(since = "0.10.0", note = "use `enable_ctrlc_handler` on builder instead")] pub fn setup_ctrlc_handler(&mut self) -> &mut Self { + self.setup_ctrlc_handler_inner(); + self + } + + /// Returns a shutdown token, which can later be used to shutdown + /// dispatching. + pub fn shutdown_token(&self) -> ShutdownToken { + self.state.clone() + } +} + +impl Dispatcher { + #[cfg(feature = "ctrlc_handler")] + fn setup_ctrlc_handler_inner(&mut self) { let token = self.state.clone(); tokio::spawn(async move { loop { @@ -443,14 +480,6 @@ where } } }); - - self - } - - /// Returns a shutdown token, which can later be used to shutdown - /// dispatching. - pub fn shutdown_token(&self) -> ShutdownToken { - self.state.clone() } } diff --git a/src/dispatching/handler_description.rs b/src/dispatching/handler_description.rs index cf3b4e4f..3ed2f621 100644 --- a/src/dispatching/handler_description.rs +++ b/src/dispatching/handler_description.rs @@ -1,44 +1,27 @@ use std::collections::HashSet; -use dptree::{description::EventKind, HandlerDescription}; +use dptree::{ + description::{EventKind, InterestSet}, + HandlerDescription, +}; use teloxide_core::types::AllowedUpdate; /// Handler description that is used by [`Dispatcher`]. /// /// [`Dispatcher`]: crate::dispatching::Dispatcher pub struct DpHandlerDescription { - allowed: EventKind, + allowed: InterestSet, } impl DpHandlerDescription { pub(crate) fn of(allowed: AllowedUpdate) -> Self { let mut set = HashSet::with_capacity(1); - set.insert(allowed); - Self { allowed: EventKind::InterestList(set) } + set.insert(Kind(allowed)); + Self { allowed: InterestSet::new_filter(set) } } pub(crate) fn allowed_updates(&self) -> Vec { - use AllowedUpdate::*; - - match &self.allowed { - EventKind::InterestList(set) => set.iter().copied().collect(), - EventKind::Entry => panic!("No updates were allowed"), - EventKind::UserDefined => vec![ - Message, - EditedMessage, - ChannelPost, - EditedChannelPost, - InlineQuery, - ChosenInlineResult, - CallbackQuery, - ShippingQuery, - PreCheckoutQuery, - Poll, - PollAnswer, - MyChatMember, - ChatMember, - ], - } + self.allowed.observed.iter().map(|Kind(x)| x).copied().collect() } } @@ -59,3 +42,70 @@ impl HandlerDescription for DpHandlerDescription { Self { allowed: self.allowed.merge_branch(&other.allowed) } } } + +#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)] +struct Kind(AllowedUpdate); + +impl EventKind for Kind { + fn full_set() -> HashSet { + use AllowedUpdate::*; + + [ + Message, + EditedMessage, + ChannelPost, + EditedChannelPost, + InlineQuery, + ChosenInlineResult, + CallbackQuery, + ShippingQuery, + PreCheckoutQuery, + Poll, + PollAnswer, + MyChatMember, + ChatMember, + ] + .into_iter() + .map(Kind) + .collect() + } + + fn empty_set() -> HashSet { + HashSet::new() + } +} + +#[cfg(test)] +mod tests { + use crate::{ + dispatching::{HandlerExt, UpdateFilterExt}, + types::{AllowedUpdate::*, Update}, + utils::command::BotCommands, + }; + + use crate as teloxide; // fixup for the `BotCommands` macro + + #[derive(BotCommands, Clone)] + #[command(rename = "lowercase")] + enum Cmd { + B, + } + + // + #[test] + fn discussion_648() { + let h = + dptree::entry().branch(Update::filter_my_chat_member().endpoint(|| async {})).branch( + Update::filter_message() + .branch(dptree::entry().filter_command::().endpoint(|| async {})) + .endpoint(|| async {}), + ); + + let mut v = h.description().allowed_updates(); + + // Hash set randomizes element order, so to compare we need to sort + v.sort_by_key(|&a| a as u8); + + assert_eq!(v, [Message, MyChatMember]) + } +} diff --git a/src/dispatching/repls/commands_repl.rs b/src/dispatching/repls/commands_repl.rs index fd04964a..7f7db10f 100644 --- a/src/dispatching/repls/commands_repl.rs +++ b/src/dispatching/repls/commands_repl.rs @@ -14,7 +14,12 @@ use teloxide_core::requests::Requester; /// /// All errors from an update listener and handler will be logged. /// +/// REPLs are meant only for simple bots and rapid prototyping. If you need to +/// supply dependencies or describe more complex dispatch logic, please use +/// [`Dispatcher`]. +/// /// ## Caution +/// /// **DO NOT** use this function together with [`Dispatcher`] and other REPLs, /// because Telegram disallow multiple requests at the same time from the same /// bot. @@ -49,7 +54,12 @@ where /// /// All errors from an update listener and handler will be logged. /// +/// REPLs are meant only for simple bots and rapid prototyping. If you need to +/// supply dependencies or describe more complex dispatch logic, please use +/// [`Dispatcher`]. +/// /// ## Caution +/// /// **DO NOT** use this function together with [`Dispatcher`] and other REPLs, /// because Telegram disallow multiple requests at the same time from the same /// bot. @@ -86,8 +96,8 @@ pub async fn commands_repl_with_listener<'a, R, Cmd, H, L, ListenerE, E, Args>( Update::filter_message().filter_command::().chain(dptree::endpoint(handler)), ) .default_handler(ignore_update) + .enable_ctrlc_handler() .build() - .setup_ctrlc_handler() .dispatch_with_listener( listener, LoggingErrorHandler::with_custom_text("An error from the update listener"), diff --git a/src/dispatching/repls/repl.rs b/src/dispatching/repls/repl.rs index eec73f1f..cecf90ad 100644 --- a/src/dispatching/repls/repl.rs +++ b/src/dispatching/repls/repl.rs @@ -11,7 +11,12 @@ use teloxide_core::requests::Requester; /// /// All errors from an update listener and a handler will be logged. /// -/// # Caution +/// REPLs are meant only for simple bots and rapid prototyping. If you need to +/// supply dependencies or describe more complex dispatch logic, please use +/// [`Dispatcher`]. +/// +/// ## Caution +/// /// **DO NOT** use this function together with [`Dispatcher`] and other REPLs, /// because Telegram disallow multiple requests at the same time from the same /// bot. @@ -35,7 +40,12 @@ where /// /// All errors from an update listener and handler will be logged. /// +/// REPLs are meant only for simple bots and rapid prototyping. If you need to +/// supply dependencies or describe more complex dispatch logic, please use +/// [`Dispatcher`]. +/// /// # Caution +/// /// **DO NOT** use this function together with [`Dispatcher`] and other REPLs, /// because Telegram disallow multiple requests at the same time from the same /// bot. @@ -61,8 +71,8 @@ where Dispatcher::builder(bot, Update::filter_message().chain(dptree::endpoint(handler))) .default_handler(ignore_update) + .enable_ctrlc_handler() .build() - .setup_ctrlc_handler() .dispatch_with_listener( listener, LoggingErrorHandler::with_custom_text("An error from the update listener"), diff --git a/src/dispatching/update_listeners/webhooks/axum.rs b/src/dispatching/update_listeners/webhooks/axum.rs index ab08cd92..6c16bdf0 100644 --- a/src/dispatching/update_listeners/webhooks/axum.rs +++ b/src/dispatching/update_listeners/webhooks/axum.rs @@ -272,7 +272,7 @@ impl FromRequest for XTelegramBotApiSecretToken { let res = req .headers_mut() - .and_then(|map| map.remove("x-telegram-bot-api-secret-token")) + .remove("x-telegram-bot-api-secret-token") .map(|header| { check_secret(header.as_bytes()) .map(<_>::to_owned) diff --git a/src/features.md b/src/features.md index 1d199a80..2c4b2951 100644 --- a/src/features.md +++ b/src/features.md @@ -1,24 +1,24 @@ ## Cargo features -| Feature | Description | -|----------------------|------------------------------------------------------------------------------------| -| `webhooks` | Enables general webhook utilities (almost useless on its own) | -| `webhooks-axum` | Enables webhook implementation based on axum framework | -| `macros` | Re-exports macros from [`teloxide-macros`]. | -| `ctrlc_handler` | Enables the [`Dispatcher::setup_ctrlc_handler`] function (**enabled by default**). | -| `auto-send` | Enables the [`AutoSend`](adaptors::AutoSend) bot adaptor (**enabled by default**). | -| `throttle` | Enables the [`Throttle`](adaptors::Throttle) bot adaptor. | -| `cache-me` | Enables the [`CacheMe`](adaptors::CacheMe) bot adaptor. | -| `trace-adaptor` | Enables the [`Trace`](adaptors::Trace) bot adaptor. | -| `erased` | Enables the [`ErasedRequester`](adaptors::ErasedRequester) bot adaptor. | -| `full` | Enables all the features except `nightly`. | -| `nightly` | Enables nightly-only features (see the [teloxide-core features]). | -| `native-tls` | Enables the [`native-tls`] TLS implementation (**enabled by default**). | -| `rustls` | Enables the [`rustls`] TLS implementation. | -| `redis-storage` | Enables the [Redis] storage support for dialogues. | -| `sqlite-storage` | Enables the [Sqlite] storage support for dialogues. | -| `cbor-serializer` | Enables the [CBOR] serializer for dialogues. | -| `bincode-serializer` | Enables the [Bincode] serializer for dialogues. | +| Feature | Description | +|----------------------|--------------------------------------------------------------------------------------------| +| `webhooks` | Enables general webhook utilities (almost useless on its own) | +| `webhooks-axum` | Enables webhook implementation based on axum framework | +| `macros` | Re-exports macros from [`teloxide-macros`]. | +| `ctrlc_handler` | Enables the [`DispatcherBuilder::enable_ctrlc_handler`] function (**enabled by default**). | +| `auto-send` | Enables the [`AutoSend`](adaptors::AutoSend) bot adaptor (**enabled by default**). | +| `throttle` | Enables the [`Throttle`](adaptors::Throttle) bot adaptor. | +| `cache-me` | Enables the [`CacheMe`](adaptors::CacheMe) bot adaptor. | +| `trace-adaptor` | Enables the [`Trace`](adaptors::Trace) bot adaptor. | +| `erased` | Enables the [`ErasedRequester`](adaptors::ErasedRequester) bot adaptor. | +| `full` | Enables all the features except `nightly`. | +| `nightly` | Enables nightly-only features (see the [teloxide-core features]). | +| `native-tls` | Enables the [`native-tls`] TLS implementation (**enabled by default**). | +| `rustls` | Enables the [`rustls`] TLS implementation. | +| `redis-storage` | Enables the [Redis] storage support for dialogues. | +| `sqlite-storage` | Enables the [Sqlite] storage support for dialogues. | +| `cbor-serializer` | Enables the [CBOR] serializer for dialogues. | +| `bincode-serializer` | Enables the [Bincode] serializer for dialogues. | [Redis]: https://redis.io/ @@ -31,4 +31,4 @@ [`teloxide::utils::UpState`]: utils::UpState [teloxide-core features]: https://docs.rs/teloxide-core/latest/teloxide_core/#cargo-features -[`Dispatcher::setup_ctrlc_handler`]: dispatching::Dispatcher::setup_ctrlc_handler \ No newline at end of file +[`DispatcherBuilder::enable_ctrlc_handler`]: dispatching::DispatcherBuilder::enable_ctrlc_handler \ No newline at end of file diff --git a/tests/command.rs b/tests/command.rs index b3e6609e..4b59ac48 100644 --- a/tests/command.rs +++ b/tests/command.rs @@ -2,7 +2,7 @@ #![allow(clippy::nonstandard_macro_braces)] #[cfg(feature = "macros")] -use teloxide::utils::command::{BotCommands, ParseError}; +use teloxide::utils::command::BotCommands; // We put tests here because macro expand in unit tests in module // teloxide::utils::command was a failure @@ -141,22 +141,33 @@ fn parse_with_split2() { #[test] #[cfg(feature = "macros")] fn parse_custom_parser() { - fn custom_parse_function(s: String) -> Result<(u8, String), ParseError> { - let vec = s.split_whitespace().collect::>(); - let (left, right) = match vec.as_slice() { - [l, r] => (l, r), - _ => return Err(ParseError::IncorrectFormat("might be 2 arguments!".into())), - }; - left.parse::() - .map(|res| (res, (*right).to_string())) - .map_err(|_| ParseError::Custom("First argument must be a integer!".to_owned().into())) + mod parser { + use teloxide::utils::command::ParseError; + + pub fn custom_parse_function(s: String) -> Result<(u8, String), ParseError> { + let vec = s.split_whitespace().collect::>(); + let (left, right) = match vec.as_slice() { + [l, r] => (l, r), + _ => return Err(ParseError::IncorrectFormat("might be 2 arguments!".into())), + }; + left.parse::().map(|res| (res, (*right).to_string())).map_err(|_| { + ParseError::Custom("First argument must be a integer!".to_owned().into()) + }) + } } + use parser::custom_parse_function; + #[derive(BotCommands, Debug, PartialEq)] #[command(rename = "lowercase")] enum DefaultCommands { #[command(parse_with = "custom_parse_function")] Start(u8, String), + + // Test . + #[command(parse_with = "parser::custom_parse_function")] + TestPath(u8, String), + Help, } @@ -164,6 +175,10 @@ fn parse_custom_parser() { DefaultCommands::Start(10, "hello".to_string()), DefaultCommands::parse("/start 10 hello", "").unwrap() ); + assert_eq!( + DefaultCommands::TestPath(10, "hello".to_string()), + DefaultCommands::parse("/testpath 10 hello", "").unwrap() + ); } #[test]