mirror of
https://github.com/teloxide/teloxide.git
synced 2024-12-22 22:46:39 +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::{
|
||||
dispatching::{
|
||||
dialogue::{self, InMemStorage},
|
||||
UpdateHandler,
|
||||
},
|
||||
dispatching::{dialogue::InMemStorage, UpdateHandler},
|
||||
prelude::*,
|
||||
types::{InlineKeyboardButton, InlineKeyboardMarkup},
|
||||
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::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 }]
|
||||
.endpoint(receive_product_selection),
|
||||
);
|
||||
|
||||
dialogue::enter::<Update, InMemStorage<State>, State, _>()
|
||||
teloxide::dispatching::dialogue::enter::<Update, InMemStorage<State>, State, _>()
|
||||
.branch(message_handler)
|
||||
.branch(callback_query_handler)
|
||||
}
|
||||
|
|
|
@ -1,76 +1,175 @@
|
|||
//! 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))
|
||||
//! Let us look at this simple example:
|
||||
//!
|
||||
//! [[`examples/purchase.rs`](https://github.com/teloxide/teloxide/blob/master/examples/purchase.rs)]
|
||||
//! ```no_run
|
||||
//! // TODO: examples/purchase.rs
|
||||
//! fn main() {}
|
||||
//! // Imports omitted...
|
||||
//! # 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()`.
|
||||
//! 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<Bot>`. There are
|
||||
//! called dependencies: `msg` is supplied by
|
||||
//! [`UpdateFilterExt::filter_message`], while `bot` is supplied by
|
||||
//! [`Dispatcher`].
|
||||
//! The above code shows how to dispatch on different combinations of a state
|
||||
//! and command _elegantly_. We give a top-bottom explanation of the function
|
||||
//! `schema`, which constructs the main update handler:
|
||||
//!
|
||||
//! 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.
|
||||
//! - We call the [`dialogue::enter`] function to initiate dialogue
|
||||
//! interaction. Then we call [`dptree::Handler::branch`] two times to form a
|
||||
//! 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
|
||||
//! 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`].
|
||||
//! `a.branch(b)` roughly means "try to handle an update with `a`, then, if it
|
||||
//! fails, try `b`". We use branching multiple times here, which is a natural
|
||||
//! pattern for describing dispatching logic. 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. Note how we utilise this macro both for
|
||||
//! `State` and `Command` in the same way!
|
||||
//!
|
||||
//! Update pipelining provides several advantages over the typical `match
|
||||
//! (update.kind) { ... }` approach:
|
||||
//! Finally, we plug the schema into [`Dispatcher`] like this:
|
||||
//!
|
||||
//! 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.
|
||||
//! ```no_run
|
||||
//! # #[tokio::main]
|
||||
//! # async fn main() {
|
||||
//! let bot = Bot::from_env().auto_send();
|
||||
//!
|
||||
//! 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
|
||||
//! [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;
|
||||
|
|
|
@ -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<Bot>,
|
||||
//! 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::<u8>()) {
|
||||
//! 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(
|
||||
|
|
Loading…
Reference in a new issue