mirror of
https://github.com/teloxide/teloxide.git
synced 2024-12-23 06:51:01 +01:00
Update the dispatching example explanation
Former-commit-id: bc8b86181f
This commit is contained in:
parent
4262175be2
commit
411c851a2f
3 changed files with 172 additions and 74 deletions
|
@ -13,10 +13,7 @@
|
||||||
// ```
|
// ```
|
||||||
|
|
||||||
use teloxide::{
|
use teloxide::{
|
||||||
dispatching::{
|
dispatching::{dialogue::InMemStorage, UpdateHandler},
|
||||||
dialogue::{self, InMemStorage},
|
|
||||||
UpdateHandler,
|
|
||||||
},
|
|
||||||
prelude::*,
|
prelude::*,
|
||||||
types::{InlineKeyboardButton, InlineKeyboardMarkup},
|
types::{InlineKeyboardButton, InlineKeyboardMarkup},
|
||||||
utils::command::BotCommands,
|
utils::command::BotCommands,
|
||||||
|
@ -75,12 +72,12 @@ fn schema() -> UpdateHandler<Box<dyn std::error::Error + Send + Sync + 'static>>
|
||||||
.branch(dptree::case![State::ReceiveFullName].endpoint(receive_full_name))
|
.branch(dptree::case![State::ReceiveFullName].endpoint(receive_full_name))
|
||||||
.branch(dptree::endpoint(invalid_state));
|
.branch(dptree::endpoint(invalid_state));
|
||||||
|
|
||||||
let callback_query_handler = Update::filter_callback_query().chain(
|
let callback_query_handler = Update::filter_callback_query().branch(
|
||||||
dptree::case![State::ReceiveProductChoice { full_name }]
|
dptree::case![State::ReceiveProductChoice { full_name }]
|
||||||
.endpoint(receive_product_selection),
|
.endpoint(receive_product_selection),
|
||||||
);
|
);
|
||||||
|
|
||||||
dialogue::enter::<Update, InMemStorage<State>, State, _>()
|
teloxide::dispatching::dialogue::enter::<Update, InMemStorage<State>, State, _>()
|
||||||
.branch(message_handler)
|
.branch(message_handler)
|
||||||
.branch(callback_query_handler)
|
.branch(callback_query_handler)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,76 +1,175 @@
|
||||||
//! An update dispatching model based on [`dptree`].
|
//! An update dispatching model based on [`dptree`].
|
||||||
//!
|
//!
|
||||||
//! In teloxide, updates are dispatched by a pipeline. The central type is
|
//! In teloxide, update dispatching is declarative: it takes the form of a
|
||||||
//! [`dptree::Handler`] -- it represents a handler of an update; since the API
|
//! [chain of responsibility] pattern enriched with a number of combinator
|
||||||
//! is highly declarative, you can combine handlers with each other via such
|
//! functions, which together form an instance of the [`dptree::Handler`] type.
|
||||||
//! 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`].
|
|
||||||
//!
|
//!
|
||||||
//! The pattern itself is called [chain of responsibility], a well-known design
|
//! Let us look at this simple example:
|
||||||
//! 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))
|
|
||||||
//!
|
//!
|
||||||
|
//! [[`examples/purchase.rs`](https://github.com/teloxide/teloxide/blob/master/examples/purchase.rs)]
|
||||||
//! ```no_run
|
//! ```no_run
|
||||||
//! // TODO: examples/purchase.rs
|
//! // Imports omitted...
|
||||||
//! fn main() {}
|
//! # use teloxide::{
|
||||||
|
//! # dispatching::{dialogue::InMemStorage, UpdateHandler},
|
||||||
|
//! # prelude::*,
|
||||||
|
//! # types::{InlineKeyboardButton, InlineKeyboardMarkup},
|
||||||
|
//! # utils::command::BotCommands,
|
||||||
|
//! # };
|
||||||
|
//!
|
||||||
|
//! type MyDialogue = Dialogue<State, InMemStorage<State>>;
|
||||||
|
//! type HandlerResult = Result<(), Box<dyn std::error::Error + Send + Sync>>;
|
||||||
|
//!
|
||||||
|
//! #[derive(Clone, Default)]
|
||||||
|
//! pub enum State {
|
||||||
|
//! #[default]
|
||||||
|
//! Start,
|
||||||
|
//! ReceiveFullName,
|
||||||
|
//! ReceiveProductChoice {
|
||||||
|
//! full_name: String,
|
||||||
|
//! },
|
||||||
|
//! }
|
||||||
|
//!
|
||||||
|
//! #[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,
|
||||||
|
//! }
|
||||||
|
//!
|
||||||
|
//! #[tokio::main]
|
||||||
|
//! async fn main() {
|
||||||
|
//! // Setup is omitted...
|
||||||
|
//! # pretty_env_logger::init();
|
||||||
|
//! # log::info!("Starting purchase bot...");
|
||||||
|
//! #
|
||||||
|
//! # let bot = Bot::from_env().auto_send();
|
||||||
|
//! #
|
||||||
|
//! # Dispatcher::builder(bot, schema())
|
||||||
|
//! # .dependencies(dptree::deps![InMemStorage::<State>::new()])
|
||||||
|
//! # .build()
|
||||||
|
//! # .setup_ctrlc_handler()
|
||||||
|
//! # .dispatch()
|
||||||
|
//! # .await;
|
||||||
|
//! }
|
||||||
|
//!
|
||||||
|
//! fn schema() -> UpdateHandler<Box<dyn std::error::Error + Send + Sync + 'static>> {
|
||||||
|
//! let command_handler = teloxide::filter_command::<Command, _>()
|
||||||
|
//! .branch(
|
||||||
|
//! dptree::case![State::Start]
|
||||||
|
//! .branch(dptree::case![Command::Help].endpoint(help))
|
||||||
|
//! .branch(dptree::case![Command::Start].endpoint(start)),
|
||||||
|
//! )
|
||||||
|
//! .branch(dptree::case![Command::Cancel].endpoint(cancel));
|
||||||
|
//!
|
||||||
|
//! let message_handler = Update::filter_message()
|
||||||
|
//! .branch(command_handler)
|
||||||
|
//! .branch(dptree::case![State::ReceiveFullName].endpoint(receive_full_name))
|
||||||
|
//! .branch(dptree::endpoint(invalid_state));
|
||||||
|
//!
|
||||||
|
//! let callback_query_handler = Update::filter_callback_query().branch(
|
||||||
|
//! dptree::case![State::ReceiveProductChoice { full_name }]
|
||||||
|
//! .endpoint(receive_product_selection),
|
||||||
|
//! );
|
||||||
|
//!
|
||||||
|
//! teloxide::dispatching::dialogue::enter::<Update, InMemStorage<State>, State, _>()
|
||||||
|
//! .branch(message_handler)
|
||||||
|
//! .branch(callback_query_handler)
|
||||||
|
//! }
|
||||||
|
//!
|
||||||
|
//! // Handler definitions omitted...
|
||||||
|
//!
|
||||||
|
//! async fn start(bot: AutoSend<Bot>, msg: Message, dialogue: MyDialogue) -> HandlerResult {
|
||||||
|
//! todo!()
|
||||||
|
//! }
|
||||||
|
//!
|
||||||
|
//! async fn help(bot: AutoSend<Bot>, msg: Message) -> HandlerResult {
|
||||||
|
//! todo!()
|
||||||
|
//! }
|
||||||
|
//!
|
||||||
|
//! async fn cancel(bot: AutoSend<Bot>, msg: Message, dialogue: MyDialogue) -> HandlerResult {
|
||||||
|
//! todo!()
|
||||||
|
//! }
|
||||||
|
//!
|
||||||
|
//! async fn invalid_state(bot: AutoSend<Bot>, msg: Message) -> HandlerResult {
|
||||||
|
//! todo!()
|
||||||
|
//! }
|
||||||
|
//!
|
||||||
|
//! async fn receive_full_name(
|
||||||
|
//! bot: AutoSend<Bot>,
|
||||||
|
//! msg: Message,
|
||||||
|
//! dialogue: MyDialogue,
|
||||||
|
//! ) -> HandlerResult {
|
||||||
|
//! todo!()
|
||||||
|
//! }
|
||||||
|
//!
|
||||||
|
//! async fn receive_product_selection(
|
||||||
|
//! bot: AutoSend<Bot>,
|
||||||
|
//! q: CallbackQuery,
|
||||||
|
//! dialogue: MyDialogue,
|
||||||
|
//! full_name: String,
|
||||||
|
//! ) -> HandlerResult {
|
||||||
|
//! todo!()
|
||||||
|
//! }
|
||||||
//! ```
|
//! ```
|
||||||
//!
|
//!
|
||||||
//! 1. First, we create the bot: `let bot = Bot::from_env().auto_send()`.
|
//! The above code shows how to dispatch on different combinations of a state
|
||||||
//! 2. Then we construct an update handler. While it is possible to handle all
|
//! and command _elegantly_. We give a top-bottom explanation of the function
|
||||||
//! kinds of [`crate::types::Update`], here we are only interested in
|
//! `schema`, which constructs the main update handler:
|
||||||
//! [`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<Bot>`. There are
|
|
||||||
//! called dependencies: `msg` is supplied by
|
|
||||||
//! [`UpdateFilterExt::filter_message`], while `bot` is supplied by
|
|
||||||
//! [`Dispatcher`].
|
|
||||||
//!
|
//!
|
||||||
//! That being said, if we receive a message, the dispatcher will call our
|
//! - We call the [`dialogue::enter`] function to initiate dialogue
|
||||||
//! handler, but if we receive something other than a message (e.g., a channel
|
//! interaction. Then we call [`dptree::Handler::branch`] two times to form a
|
||||||
//! post), you will see an unhandled update notice in your terminal.
|
//! tree of responsibility of `message_handler` and `callback_query_handler`.
|
||||||
|
//! - Inside `message_handler`, we use [`Update::filter_message`] as a filter
|
||||||
|
//! for incoming messages. Then we create a tree of responsibility again,
|
||||||
|
//! consisting of three branches with a similar structure.
|
||||||
|
//! - Inside `callback_query_handler`, we use
|
||||||
|
//! [`Update::filter_callback_query`] as a filter and create one branch for
|
||||||
|
//! handling product selection.
|
||||||
//!
|
//!
|
||||||
//! This is a very limited example of update pipelining facilities. In more
|
//! `a.branch(b)` roughly means "try to handle an update with `a`, then, if it
|
||||||
//! involved scenarios, there are multiple branches and chains; if one element
|
//! fails, try `b`". We use branching multiple times here, which is a natural
|
||||||
//! of a chain fails to handle an update, the update will be passed forwards; if
|
//! pattern for describing dispatching logic. We also use the [`dptree::case!`]
|
||||||
//! no handler succeeds at handling the update, [`Dispatcher`] will invoke a
|
//! macro extensively, which acts as a filter on an enumeration: if it is of a
|
||||||
//! default handler set up via [`DispatcherBuilder::default_handler`].
|
//! certain variant, it passes the variant's payload down the handler chain;
|
||||||
|
//! otherwise, it neglects an update. Note how we utilise this macro both for
|
||||||
|
//! `State` and `Command` in the same way!
|
||||||
//!
|
//!
|
||||||
//! Update pipelining provides several advantages over the typical `match
|
//! Finally, we plug the schema into [`Dispatcher`] like this:
|
||||||
//! (update.kind) { ... }` approach:
|
|
||||||
//!
|
//!
|
||||||
//! 1. It supports _extension_: e.g., you
|
//! ```no_run
|
||||||
//! can define extension filters or some other handlers and then combine them in
|
//! # #[tokio::main]
|
||||||
//! a single place, thus facilitating loose coupling.
|
//! # async fn main() {
|
||||||
//! 2. Pipelining exhibits a natural syntax for expressing message processing.
|
//! let bot = Bot::from_env().auto_send();
|
||||||
//! 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.
|
|
||||||
//!
|
//!
|
||||||
//! For a more involved example, see [`examples/dispatching_features.rs`](https://github.com/teloxide/teloxide/blob/master/examples/dispatching_features.rs).
|
//! Dispatcher::builder(bot, schema())
|
||||||
|
//! .dependencies(dptree::deps![InMemStorage::<State>::new()])
|
||||||
|
//! .build()
|
||||||
|
//! .setup_ctrlc_handler()
|
||||||
|
//! .dispatch()
|
||||||
|
//! .await;
|
||||||
|
//! # }
|
||||||
|
//! ```
|
||||||
//!
|
//!
|
||||||
//! TODO: explain a more involved example with multiple branches.
|
//! In a call to [`DispatcherBuilder::dependencies`], we specify a list of
|
||||||
|
//! 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.
|
||||||
|
//!
|
||||||
|
//! [`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
|
//! [chain of responsibility]: https://en.wikipedia.org/wiki/Chain-of-responsibility_pattern
|
||||||
//! [dependency injection (DI)]: https://en.wikipedia.org/wiki/Dependency_injection
|
//! [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"))]
|
#[cfg(all(feature = "ctrlc_handler"))]
|
||||||
pub mod repls;
|
pub mod repls;
|
||||||
|
|
|
@ -4,10 +4,11 @@
|
||||||
//! wrapper over [`Storage`] and a chat ID. All it does is provides convenient
|
//! wrapper over [`Storage`] and a chat ID. All it does is provides convenient
|
||||||
//! method for manipulating the dialogue state. [`Storage`] is where all
|
//! method for manipulating the dialogue state. [`Storage`] is where all
|
||||||
//! dialogue states are stored; it can be either [`InMemStorage`], which is a
|
//! dialogue states are stored; it can be either [`InMemStorage`], which is a
|
||||||
//! simple hash map, or database wrappers such as [`SqliteStorage`]. In the
|
//! simple hash map from [`std::collections`], or an advanced database wrapper
|
||||||
//! latter case, your dialogues are _persistent_, meaning that you can safely
|
//! such as [`SqliteStorage`]. In the latter case, your dialogues are
|
||||||
//! restart your bot and all dialogues will remain in the database -- this is a
|
//! _persistent_, meaning that you can safely restart your bot and all ongoing
|
||||||
//! preferred method for production bots.
|
//! dialogues will remain in the database -- this is a preferred method for
|
||||||
|
//! production bots.
|
||||||
//!
|
//!
|
||||||
//! [`examples/dialogue.rs`] clearly demonstrates the typical usage of
|
//! [`examples/dialogue.rs`] clearly demonstrates the typical usage of
|
||||||
//! dialogues. Your dialogue state can be represented as an enumeration:
|
//! dialogues. Your dialogue state can be represented as an enumeration:
|
||||||
|
@ -31,8 +32,8 @@
|
||||||
//! bot: AutoSend<Bot>,
|
//! bot: AutoSend<Bot>,
|
||||||
//! msg: Message,
|
//! msg: Message,
|
||||||
//! dialogue: MyDialogue,
|
//! dialogue: MyDialogue,
|
||||||
//! (full_name,): (String,), // Available from `State::ReceiveAge`.
|
//! full_name: String, // Available from `State::ReceiveAge`.
|
||||||
//! ) -> anyhow::Result<()> {
|
//! ) -> HandlerResult {
|
||||||
//! match msg.text().map(|text| text.parse::<u8>()) {
|
//! match msg.text().map(|text| text.parse::<u8>()) {
|
||||||
//! Some(Ok(age)) => {
|
//! Some(Ok(age)) => {
|
||||||
//! bot.send_message(msg.chat.id, "What's your location?").await?;
|
//! 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,):
|
//! Variant's fields are passed to state handlers as single arguments like
|
||||||
//! (String,)`. Using [`Dialogue::update`], you can update the dialogue with a
|
//! `full_name: String` or tuples in case of two or more variant parameters (see
|
||||||
//! new state, in our case -- `State::ReceiveLocation { full_name, age }`. To
|
//! below). Using [`Dialogue::update`], you can update the dialogue with a new
|
||||||
//! exit the dialogue, just call [`Dialogue::exit`] and it will be removed from
|
//! state, in our case -- `State::ReceiveLocation { full_name, age }`. To exit
|
||||||
//! the inner storage:
|
//! the dialogue, just call [`Dialogue::exit`] and it will be removed from the
|
||||||
|
//! underlying storage:
|
||||||
//!
|
//!
|
||||||
//! ```ignore
|
//! ```ignore
|
||||||
//! async fn receive_location(
|
//! async fn receive_location(
|
||||||
|
|
Loading…
Reference in a new issue