Module 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§

dialogue
Support for user dialogues.

Structs§

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

Traits§

HandlerExt
Extension methods for working with dptree handlers.
MessageFilterExt
Filter methods for Message.
UpdateFilterExt
Filter methods for Update.
UpdateHandlerTracingExttracing

Functions§

filter_command
Returns a handler that accepts a parsed command C.
filter_mention_command
Returns a handler that accepts a parsed command C if the command contains a bot mention, for example /start@my_bot.

Type Aliases§

UpdateHandler
A handler that processes updates from Telegram.