Module teloxide::dispatching

source ·
Expand description

An update dispatching model based on 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.

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:

#[derive(Clone, Default)]
pub enum State {
    #[default]
    Start,
    ReceiveFullName,
    ReceiveProductChoice {
        full_name: String,
    },
}

Then, we define a type Command to represent user commands such as /start or /help:

#[derive(BotCommands, Clone)]
#[command(rename_rule = "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,
}

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:

fn schema() -> UpdateHandler<Box<dyn std::error::Error + Send + Sync + 'static>> {
    use dptree::case;

    let command_handler = teloxide::filter_command::<Command, _>()
        .branch(
            case![State::Start]
                .branch(case![Command::Help].endpoint(help))
                .branch(case![Command::Start].endpoint(start)),
        )
        .branch(case![Command::Cancel].endpoint(cancel));

    let message_handler = Update::filter_message()
        .branch(command_handler)
        .branch(case![State::ReceiveFullName].endpoint(receive_full_name))
        .branch(dptree::endpoint(invalid_state));

    let callback_query_handler = Update::filter_callback_query().branch(
        case![State::ReceiveProductChoice { full_name }].endpoint(receive_product_selection),
    );

    dialogue::enter::<Update, InMemStorage<State>, 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:

type MyDialogue = Dialogue<State, InMemStorage<State>>;
type HandlerResult = Result<(), Box<dyn std::error::Error + Send + Sync>>;

async fn start(bot: Bot, dialogue: MyDialogue, msg: Message) -> HandlerResult {
    todo!()
}
async fn help(bot: Bot, msg: Message) -> HandlerResult {
    todo!()
}
async fn cancel(bot: Bot, dialogue: MyDialogue, msg: Message) -> HandlerResult {
    todo!()
}
async fn invalid_state(bot: Bot, msg: Message) -> HandlerResult {
    todo!()
}
async fn receive_full_name(bot: Bot, dialogue: MyDialogue, msg: Message) -> HandlerResult {
    todo!()
}
async fn receive_product_selection(
    bot: Bot,
    dialogue: MyDialogue,
    full_name: String, // Available from `State::ReceiveProductChoice`.
    q: CallbackQuery,
) -> HandlerResult {
    todo!()
}

Each parameter is supplied as a dependency by teloxide. In particular:

Inside main, we plug the schema into Dispatcher like this:

#[tokio::main]
async fn main() {
    let bot = Bot::from_env();

    Dispatcher::builder(bot, schema())
        .dependencies(dptree::deps![InMemStorage::<State>::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.

Dispatching or REPLs?

The difference between dispatching and the REPLs (crate::repl & co) is that dispatching gives you a greater degree of flexibility at the cost of a bit more complicated setup.

Here are things that dispatching can do, but REPLs can’t:

Thus, REPLs are good for simple bots and rapid prototyping, but for more involved scenarios, we recommend using dispatching over REPLs.

Modules

Support for user dialogues.
replsDeprecatedctrlc_handler
This module was moved to teloxide::repls.
This module was moved to teloxide::update_listeners.

Structs

Default distribution key for dispatching.
The base for update dispatching.
Handler description that is used by Dispatcher.
This error is returned from ShutdownToken::shutdown when trying to shutdown an idle Dispatcher.
A token which used to shutdown Dispatcher.

Traits

Extension methods for working with dptree handlers.
Filter methods for Message.
Filter methods for Update.

Functions

Returns a handler that accepts a parsed command C.

Type Definitions

A handler that processes updates from Telegram.