Skip to main content

teloxide_ng/
dispatching.rs

1//! An update dispatching model based on [`dptree`].
2//!
3//! In `teloxide`, update dispatching is declarative: it takes the form of a
4//! [chain of responsibility] pattern enriched with a number of combinator
5//! functions, which together form an instance of the [`dptree::Handler`] type.
6//!
7//! Take [`examples/purchase.rs`] as an example of dispatching logic. First, we
8//! define a type named `State` to represent the current state of a dialogue:
9//!
10//! ```no_run
11//! #[derive(Clone, Default)]
12//! pub enum State {
13//!     #[default]
14//!     Start,
15//!     ReceiveFullName,
16//!     ReceiveProductChoice {
17//!         full_name: String,
18//!     },
19//! }
20//! ```
21//!
22//! Then, we define a type `Command` to represent user commands such as
23//! `/start` or `/help`:
24//!
25//! ```no_run
26//! # #[cfg(feature = "macros")] {
27//! # use teloxide_ng::utils::command::BotCommands;
28//! #[derive(BotCommands, Clone)]
29//! #[command(rename_rule = "lowercase", description = "These commands are supported:")]
30//! enum Command {
31//!     #[command(description = "display this text.")]
32//!     Help,
33//!     #[command(description = "start the purchase procedure.")]
34//!     Start,
35//!     #[command(description = "cancel the purchase procedure.")]
36//!     Cancel,
37//! }
38//! # }
39//! ```
40//!
41//! Now the key question: how to elegantly dispatch on different combinations of
42//! `State`, `Command`, and Telegram updates? -- i.e., we may want to execute
43//! specific endpoints only in response to specific user commands and while we
44//! are in a given dialogue state (and possibly under other circumstances!). The
45//! solution is to use [`dptree`]:
46//!
47//! ```no_run
48//! # #[cfg(feature = "macros")] {
49//! # // That's a lot of context needed to compile this, oof
50//! # use teloxide_ng::dispatching::{UpdateHandler, UpdateFilterExt, dialogue, dialogue::InMemStorage};
51//! # use teloxide_ng::utils::command::BotCommands;
52//! # use teloxide_ng::types::Update;
53//! # #[derive(Clone, Default)] pub enum State { #[default] Start, ReceiveFullName, ReceiveProductChoice { full_name: String } }
54//! # #[derive(BotCommands, Clone)] enum Command { Help, Start, Cancel }
55//! # type HandlerResult = Result<(), Box<dyn std::error::Error + Send + Sync>>;
56//! # async fn help() -> HandlerResult { Ok(()) }
57//! # async fn start() -> HandlerResult { Ok(()) }
58//! # async fn cancel() -> HandlerResult { Ok(()) }
59//! # async fn receive_full_name() -> HandlerResult { Ok(()) }
60//! # async fn invalid_state() -> HandlerResult { Ok(()) }
61//! # async fn receive_product_selection() -> HandlerResult { Ok(()) }
62//! #
63//! fn schema() -> UpdateHandler<Box<dyn std::error::Error + Send + Sync + 'static>> {
64//!     use dptree::case;
65//!
66//!     let command_handler = teloxide_ng::filter_command::<Command, _>()
67//!         .branch(
68//!             case![State::Start]
69//!                 .branch(case![Command::Help].endpoint(help))
70//!                 .branch(case![Command::Start].endpoint(start)),
71//!         )
72//!         .branch(case![Command::Cancel].endpoint(cancel));
73//!
74//!     let message_handler = Update::filter_message()
75//!         .branch(command_handler)
76//!         .branch(case![State::ReceiveFullName].endpoint(receive_full_name))
77//!         .branch(dptree::endpoint(invalid_state));
78//!
79//!     let callback_query_handler = Update::filter_callback_query().branch(
80//!         case![State::ReceiveProductChoice { full_name }].endpoint(receive_product_selection),
81//!     );
82//!
83//!     dialogue::enter::<Update, InMemStorage<State>, State, _>()
84//!         .branch(message_handler)
85//!         .branch(callback_query_handler)
86//! }
87//! # }
88//! ```
89//!
90//! The overall logic should be clear. Throughout the above example, we use
91//! several techniques:
92//!
93//!  - **Branching:** `a.branch(b)` roughly means "try to handle an update with
94//!    `a`, then, if it neglects the update, try `b`".
95//!  - **Pattern matching:** We also use the [`dptree::case!`] macro
96//!    extensively, which acts as a filter on an enumeration: if it is of a
97//!    certain variant, it passes the variant's payload down the handler chain;
98//!    otherwise, it neglects an update.
99//!  - **Endpoints:** To specify the final function to handle an update, we use
100//!    [`dptree::Handler::endpoint`].
101//!
102//! Notice the clear and uniform code structure: regardless of the dispatch
103//! criteria, we use the same program constructions. In future, you may want to
104//! introduce your application-specific filters or data structures to match upon
105//! -- no problem, reuse [`dptree::Handler::filter`], [`dptree::case!`], and
106//! other combinators in the same way!
107//!
108//! Finally, we define our endpoints:
109//!
110//! ```no_run
111//! # use teloxide_ng::Bot;
112//! # use teloxide_ng::types::{Message, CallbackQuery};
113//! # use teloxide_ng::dispatching::dialogue::{InMemStorage, Dialogue};
114//! # enum State{}
115//! #
116//! type MyDialogue = Dialogue<State, InMemStorage<State>>;
117//! type HandlerResult = Result<(), Box<dyn std::error::Error + Send + Sync>>;
118//!
119//! async fn start(bot: Bot, dialogue: MyDialogue, msg: Message) -> HandlerResult {
120//!     todo!()
121//! }
122//! async fn help(bot: Bot, msg: Message) -> HandlerResult {
123//!     todo!()
124//! }
125//! async fn cancel(bot: Bot, dialogue: MyDialogue, msg: Message) -> HandlerResult {
126//!     todo!()
127//! }
128//! async fn invalid_state(bot: Bot, msg: Message) -> HandlerResult {
129//!     todo!()
130//! }
131//! async fn receive_full_name(bot: Bot, dialogue: MyDialogue, msg: Message) -> HandlerResult {
132//!     todo!()
133//! }
134//! async fn receive_product_selection(
135//!     bot: Bot,
136//!     dialogue: MyDialogue,
137//!     full_name: String, // Available from `State::ReceiveProductChoice`.
138//!     q: CallbackQuery,
139//! ) -> HandlerResult {
140//!     todo!()
141//! }
142//! ```
143//!
144//! Each parameter is supplied as a dependency by `teloxide`. In particular:
145//!  - `bot: Bot` comes from the dispatcher (see below)
146//!  - `msg: Message` comes from [`Update::filter_message`]
147//!  - `q: CallbackQuery` comes from [`Update::filter_callback_query`]
148//!  - `dialogue: MyDialogue` comes from [`dialogue::enter`]
149//!  - `full_name: String` comes from `dptree::case![State::ReceiveProductChoice
150//!    { full_name }]`
151//!
152//! Inside `main`, we plug the schema into [`Dispatcher`] like this:
153//!
154//! ```no_run
155//! # #[cfg(feature = "ctrlc_handler")] {
156//! # use teloxide_ng::Bot;
157//! # use teloxide_ng::requests::RequesterExt;
158//! # use teloxide_ng::dispatching::{Dispatcher, dialogue::InMemStorage};
159//! # enum State {}
160//! # fn schema() -> teloxide_ng::dispatching::UpdateHandler<Box<dyn std::error::Error + Send + Sync + 'static>> { teloxide_ng::dptree::entry() }
161//! #[tokio::main]
162//! async fn main() {
163//!     let bot = Bot::from_env();
164//!
165//!     Dispatcher::builder(bot, schema())
166//!         .dependencies(dptree::deps![InMemStorage::<State>::new()])
167//!         .enable_ctrlc_handler()
168//!         .build()
169//!         .dispatch()
170//!         .await;
171//! }
172//! # }
173//! ```
174//!
175//! In a call to [`DispatcherBuilder::dependencies`], we specify a list of
176//! additional dependencies that all handlers will receive as parameters. Here,
177//! we only specify an in-memory storage of dialogues needed for
178//! [`dialogue::enter`]. However, in production bots, you normally also pass a
179//! database connection, configuration, and other stuff.
180//!
181//! All in all, [`dptree`] can be seen as an extensible alternative to pattern
182//! matching, with support for [dependency injection (DI)] and a few other
183//! useful features. See [`examples/dispatching_features.rs`] as a more involved
184//! example.
185//!
186//! ## Dispatching or REPLs?
187//!
188//! The difference between dispatching and the REPLs ([`crate::repl`] & co) is
189//! that dispatching gives you a greater degree of flexibility at the cost of a
190//! bit more complicated setup.
191//!
192//! Here are things that dispatching can do, but REPLs can't:
193//!  - Handle different kinds of [`Update`]
194//!  - [Pass dependencies] to handlers
195//!  - Disable a [default Ctrl-C handling]
196//!  - Control your [default] and [error] handlers
197//!  - Use [dialogues]
198//!  - Use [`dptree`]-related functionality
199//!  - Probably more
200//!
201//! Thus, REPLs are good for simple bots and rapid prototyping, but for more
202//! involved scenarios, we recommend using dispatching over REPLs.
203//!
204//! [Pass dependencies]: DispatcherBuilder#method.dependencies
205//! [default Ctrl-C handling]: DispatcherBuilder#method.enable_ctrlc_handler
206//! [default]: DispatcherBuilder#method.default_handler
207//! [error]: DispatcherBuilder#method.error_handler
208//! [dialogues]: dialogue
209//! [`examples/purchase.rs`]: https://github.com/teloxide/teloxide/blob/master/crates/teloxide-ng/examples/purchase.rs
210//! [`Update::filter_message`]: crate::types::Update::filter_message
211//! [`Update::filter_callback_query`]: crate::types::Update::filter_callback_query
212//! [chain of responsibility]: https://en.wikipedia.org/wiki/Chain-of-responsibility_pattern
213//! [dependency injection (DI)]: https://en.wikipedia.org/wiki/Dependency_injection
214//! [`examples/dispatching_features.rs`]: https://github.com/teloxide/teloxide/blob/master/crates/teloxide-ng/examples/dispatching_features.rs
215//! [`Update`]: crate::types::Update
216
217pub mod dialogue;
218
219mod dispatcher;
220mod distribution;
221mod filter_ext;
222mod handler_description;
223mod handler_ext;
224
225#[cfg(feature = "tracing")]
226mod tracing;
227
228pub use crate::utils::shutdown_token::{IdleShutdownError, ShutdownToken};
229pub use dispatcher::{Dispatcher, DispatcherBuilder, UpdateHandler};
230pub use distribution::DefaultKey;
231pub use filter_ext::{MessageFilterExt, UpdateFilterExt};
232pub use handler_description::DpHandlerDescription;
233pub use handler_ext::{HandlerExt, filter_command, filter_mention_command};
234
235#[cfg(feature = "tracing")]
236pub use self::tracing::UpdateHandlerTracingExt;