diff --git a/Cargo.toml b/Cargo.toml index b106d02f..f9ecce06 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,6 +44,7 @@ async-trait = "0.1.22" futures = "0.3.1" pin-project = "0.4.6" serde_with_macros = "1.0.1" +frunk = "0.3.1" teloxide-macros = "0.2.1" diff --git a/README.md b/README.md index e9ee31ec..819c28e3 100644 --- a/README.md +++ b/README.md @@ -23,12 +23,11 @@ ## Table of contents - [Features](https://github.com/teloxide/teloxide#features) - - [Getting started](https://github.com/teloxide/teloxide#getting-started) - - [Examples](https://github.com/teloxide/teloxide#examples) + - [Setting up your environment](https://github.com/teloxide/teloxide#setting-up-your-environment) + - [API overview](https://github.com/teloxide/teloxide#api-overview) - [The ping-pong bot](https://github.com/teloxide/teloxide#the-ping-pong-bot) - [Commands](https://github.com/teloxide/teloxide#commands) - - [Guess a number](https://github.com/teloxide/teloxide#guess-a-number) - - [More examples!](https://github.com/teloxide/teloxide#more-examples) + - [Dialogues](https://github.com/teloxide/teloxide#dialogues) - [Recommendations](https://github.com/teloxide/teloxide#recommendations) - [FAQ](https://github.com/teloxide/teloxide#faq) - [Where I can ask questions?](https://github.com/teloxide/teloxide#where-i-can-ask-questions) @@ -57,9 +56,10 @@ All the API <a href="https://docs.rs/teloxide/latest/teloxide/types/index.html"> Dialogues management is independent of how/where they are stored: just replace one line and make them <a href="https://en.wikipedia.org/wiki/Persistence_(computer_science)">persistent</a> (for example, store on a disk, transmit through a network), without affecting the actual <a href="https://en.wikipedia.org/wiki/Finite-state_machine">FSM</a> algorithm. By default, teloxide stores all user dialogues in RAM. Default database implementations <a href="https://github.com/teloxide/teloxide/issues/183">are coming</a>! </p> -## Getting started - 1. Create a new bot using [@Botfather](https://t.me/botfather) to get a token in the format `123456789:blablabla`. - 2. Initialise the `TELOXIDE_TOKEN` environmental variable to your token: +## Setting up your environment + 1. [Download Rust](http://rustup.rs/). + 2. Create a new bot using [@Botfather](https://t.me/botfather) to get a token in the format `123456789:blablabla`. + 3. Initialise the `TELOXIDE_TOKEN` environmental variable to your token: ```bash # Unix-like $ export TELOXIDE_TOKEN=<Your token here> @@ -67,7 +67,7 @@ $ export TELOXIDE_TOKEN=<Your token here> # Windows $ set TELOXIDE_TOKEN=<Your token here> ``` - 3. Be sure that you are up to date: + 4. Be sure that you are up to date: ```bash # If you're using stable $ rustup update stable @@ -78,7 +78,7 @@ $ rustup update nightly $ rustup override set nightly ``` - 4. Execute `cargo new my_bot`, enter the directory and put these lines into your `Cargo.toml`: + 5. Execute `cargo new my_bot`, enter the directory and put these lines into your `Cargo.toml`: ```toml [dependencies] teloxide = "0.2.0" @@ -87,7 +87,9 @@ tokio = "0.2.11" pretty_env_logger = "0.4.0" ``` -## The ping-pong bot +## API overview + +### The ping-pong bot This bot has a single message handler, which answers "pong" to each incoming message: ([Full](https://github.com/teloxide/teloxide/blob/master/examples/ping_pong_bot/src/main.rs)) @@ -119,7 +121,7 @@ async fn main() { </kbd> </div> -## Commands +### Commands Commands are defined similar to how we define CLI using [structopt](https://docs.rs/structopt/0.3.9/structopt/). This bot says "I am a cat! Meow!" on `/meow`, generates a random number within [0; 1) on `/generate`, and shows the usage guide on `/help`: ([Full](https://github.com/teloxide/teloxide/blob/master/examples/simple_commands_bot/src/main.rs)) @@ -185,84 +187,182 @@ See? The dispatcher gives us a stream of messages, so we can handle it as we wan - ... And lots of [others](https://docs.rs/futures/0.3.4/futures/stream/trait.StreamExt.html) and [others](https://docs.rs/teloxide/latest/teloxide/dispatching/trait.DispatcherHandlerRxExt.html) and [others](https://docs.rs/tokio/0.2.13/tokio/sync/index.html)! -## Guess a number -Wanna see more? This is a bot, which starts a game on each incoming message. You must guess a number from 1 to 10 (inclusively): +### Dialogues +Wanna see more? This is how dialogues management is made in teloxide. -([Full](https://github.com/teloxide/teloxide/blob/master/examples/guess_a_number_bot/src/main.rs)) +([dialogue_bot/src/states.rs](https://github.com/teloxide/teloxide/blob/master/examples/dialogue_bot/src/states.rs)) ```rust // Imports are omitted... -#[derive(SmartDefault)] -enum Dialogue { - #[default] - Start, - ReceiveAttempt(u8), +pub struct StartState; + +pub struct ReceiveFullNameState { + rest: StartState, } -type Cx<State> = DialogueDispatcherHandlerCx<Message, State>; -type Res = ResponseResult<DialogueStage<Dialogue>>; - -async fn start(cx: Cx<()>) -> Res { - cx.answer("Let's play a game! Guess a number from 1 to 10 (inclusively).") - .send() - .await?; - next(Dialogue::ReceiveAttempt(thread_rng().gen_range(1, 11))) +pub struct ReceiveAgeState { + rest: ReceiveFullNameState, + full_name: String, } -async fn receive_attempt(cx: Cx<u8>) -> Res { - let secret = cx.dialogue; +pub struct ReceiveFavouriteMusicState { + rest: ReceiveAgeState, + age: u8, +} - match cx.update.text() { - None => { - cx.answer("Oh, please, send me a text message!").send().await?; - next(Dialogue::ReceiveAttempt(secret)) +#[derive(Display)] +#[display( + "Your full name: {rest.rest.full_name}, your age: {rest.age}, your \ + favourite music: {favourite_music}" +)] +pub struct ExitState { + rest: ReceiveFavouriteMusicState, + favourite_music: FavouriteMusic, +} + +up!( + StartState -> ReceiveFullNameState, + ReceiveFullNameState + [full_name: String] -> ReceiveAgeState, + ReceiveAgeState + [age: u8] -> ReceiveFavouriteMusicState, + ReceiveFavouriteMusicState + [favourite_music: FavouriteMusic] -> ExitState, +); + +pub type Dialogue = Coprod!( + StartState, + ReceiveFullNameState, + ReceiveAgeState, + ReceiveFavouriteMusicState, +); + +wrap_dialogue!( + Wrapper(Dialogue), + default Self(Dialogue::inject(StartState)), +); +``` + +The [`wrap_dialogue!`](https://docs.rs/teloxide/latest/teloxide/macro.wrap_dialogue.html) macro generates a new-type of `Dialogue` with a default implementation. + +([dialogue_bot/src/transitions.rs](https://github.com/teloxide/teloxide/blob/master/examples/dialogue_bot/src/transitions.rs)) +```rust +// Imports are omitted... + +pub type In<State> = TransitionIn<State, std::convert::Infallible>; +pub type Out = TransitionOut<Wrapper>; + +pub async fn start(cx: In<StartState>) -> Out { + let (cx, dialogue) = cx.unpack(); + + cx.answer_str("Let's start! First, what's your full name?").await?; + next(dialogue.up()) +} + +pub async fn receive_full_name(cx: In<ReceiveFullNameState>) -> Out { + let (cx, dialogue) = cx.unpack(); + + match cx.update.text_owned() { + Some(full_name) => { + cx.answer_str("What a wonderful name! Your age?").await?; + next(dialogue.up(full_name)) + } + _ => { + cx.answer_str("Please, enter a text message!").await?; + next(dialogue) } - Some(text) => match text.parse::<u8>() { - Ok(attempt) => { - if attempt == secret { - cx.answer("Congratulations! You won!").send().await?; - exit() - } else { - cx.answer("No.").send().await?; - next(Dialogue::ReceiveAttempt(secret)) - } - } - Err(_) => { - cx.answer("Oh, please, send me a number in the range [1; 10]!") - .send() - .await?; - next(Dialogue::ReceiveAttempt(secret)) - } - }, } } -async fn handle_message( - cx: DialogueDispatcherHandlerCx<Message, Dialogue>, -) -> Res { - // Match is omitted... +pub async fn receive_age(cx: In<ReceiveAgeState>) -> Out { + let (cx, dialogue) = cx.unpack(); + + match cx.update.text().map(str::parse) { + Some(Ok(age)) => { + cx.answer("Good. Now choose your favourite music:") + .reply_markup(FavouriteMusic::markup()) + .send() + .await?; + next(dialogue.up(age)) + } + _ => { + cx.answer_str("Please, enter a number!").await?; + next(dialogue) + } + } } -#[tokio::main] -async fn main() { - // Setup is omitted... +pub async fn receive_favourite_music( + cx: In<ReceiveFavouriteMusicState>, +) -> Out { + let (cx, dialogue) = cx.unpack(); + + match cx.update.text().map(str::parse) { + Some(Ok(favourite_music)) => { + cx.answer_str(format!("Fine. {}", dialogue.up(favourite_music))) + .await?; + exit() + } + _ => { + cx.answer_str("Please, enter from the keyboard!").await?; + next(dialogue) + } + } } ``` -<div align="center"> - <kbd> - <img src=https://github.com/teloxide/teloxide/raw/master/media/GUESS_A_NUMBER_BOT.png width="600" /> - </kbd> - <br/><br/> -</div> +([dialogue_bot/src/favourite_music.rs](https://github.com/teloxide/teloxide/blob/master/examples/dialogue_bot/src/favourite_music.rs)) +```rust +// Imports are omitted... -Our [finite automaton](https://en.wikipedia.org/wiki/Finite-state_machine), designating a user dialogue, cannot be in an invalid state, and this is why it is called "type-safe". We could use `enum` + `Option`s instead, but it would lead us to lots of unpleasant `.unwrap()`s. +#[derive(Copy, Clone, Display, FromStr)] +pub enum FavouriteMusic { + Rock, + Metal, + Pop, + Other, +} -Remember that a classical [finite automaton](https://en.wikipedia.org/wiki/Finite-state_machine) is defined by its initial state, a list of its possible states and a transition function? We can think that `Dialogue` is a finite automaton with a context type at each state (`Dialogue::Start` has `()`, `Dialogue::ReceiveAttempt` has `u8`). +impl FavouriteMusic { + pub fn markup() -> ReplyKeyboardMarkup { + ReplyKeyboardMarkup::default().append_row(vec![ + KeyboardButton::new("Rock"), + KeyboardButton::new("Metal"), + KeyboardButton::new("Pop"), + KeyboardButton::new("Other"), + ]) + } +} +``` -See [examples/dialogue_bot](https://github.com/teloxide/teloxide/blob/master/examples/dialogue_bot/src/main.rs) to see a bit more complicated bot with dialogues. -## [More examples!](https://github.com/teloxide/teloxide/tree/master/examples) +([dialogue_bot/src/main.rs](https://github.com/teloxide/teloxide/blob/master/examples/dialogue_bot/src/main.rs)) +```rust +// Imports are omitted... + +#[tokio::main] +async fn main() { + teloxide::enable_logging!(); + log::info!("Starting dialogue_bot!"); + + let bot = Bot::from_env(); + + Dispatcher::new(bot) + .messages_handler(DialogueDispatcher::new(|cx| async move { + let DialogueWithCx { cx, dialogue } = cx; + + // Unwrap without panic because of std::convert::Infallible. + let Wrapper(dialogue) = dialogue.unwrap(); + + dispatch!( + [cx, dialogue] -> + [start, receive_full_name, receive_age, receive_favourite_music] + ) + .expect("Something wrong with the bot!") + })) + .dispatch() + .await; +} +``` + +[More examples!](https://github.com/teloxide/teloxide/tree/master/examples) ## Recommendations - Use this pattern: diff --git a/examples/dialogue_bot/Cargo.toml b/examples/dialogue_bot/Cargo.toml index 947470f0..eac16046 100644 --- a/examples/dialogue_bot/Cargo.toml +++ b/examples/dialogue_bot/Cargo.toml @@ -10,9 +10,9 @@ edition = "2018" log = "0.4.8" tokio = "0.2.9" pretty_env_logger = "0.4.0" -smart-default = "0.6.0" parse-display = "0.1.1" teloxide = { path = "../../" } +frunk = "0.3.1" [profile.release] lto = true \ No newline at end of file diff --git a/examples/dialogue_bot/src/favourite_music.rs b/examples/dialogue_bot/src/favourite_music.rs new file mode 100644 index 00000000..5b102332 --- /dev/null +++ b/examples/dialogue_bot/src/favourite_music.rs @@ -0,0 +1,22 @@ +use parse_display::{Display, FromStr}; + +use teloxide::types::{KeyboardButton, ReplyKeyboardMarkup}; + +#[derive(Copy, Clone, Display, FromStr)] +pub enum FavouriteMusic { + Rock, + Metal, + Pop, + Other, +} + +impl FavouriteMusic { + pub fn markup() -> ReplyKeyboardMarkup { + ReplyKeyboardMarkup::default().append_row(vec![ + KeyboardButton::new("Rock"), + KeyboardButton::new("Metal"), + KeyboardButton::new("Pop"), + KeyboardButton::new("Other"), + ]) + } +} diff --git a/examples/dialogue_bot/src/main.rs b/examples/dialogue_bot/src/main.rs index 9d610c2f..fac687fb 100644 --- a/examples/dialogue_bot/src/main.rs +++ b/examples/dialogue_bot/src/main.rs @@ -15,167 +15,16 @@ // ``` #![allow(clippy::trivial_regex)] +#![allow(dead_code)] -#[macro_use] -extern crate smart_default; +mod favourite_music; +mod states; +mod transitions; -use std::convert::Infallible; -use teloxide::{ - prelude::*, - types::{KeyboardButton, ReplyKeyboardMarkup}, -}; +use states::*; +use transitions::*; -use parse_display::{Display, FromStr}; - -// ============================================================================ -// [Favourite music kinds] -// ============================================================================ - -#[derive(Copy, Clone, Display, FromStr)] -enum FavouriteMusic { - Rock, - Metal, - Pop, - Other, -} - -impl FavouriteMusic { - fn markup() -> ReplyKeyboardMarkup { - ReplyKeyboardMarkup::default().append_row(vec![ - KeyboardButton::new("Rock"), - KeyboardButton::new("Metal"), - KeyboardButton::new("Pop"), - KeyboardButton::new("Other"), - ]) - } -} - -// ============================================================================ -// [A type-safe finite automaton] -// ============================================================================ - -#[derive(Clone)] -struct ReceiveAgeState { - full_name: String, -} - -#[derive(Clone)] -struct ReceiveFavouriteMusicState { - data: ReceiveAgeState, - age: u8, -} - -#[derive(Display)] -#[display( - "Your full name: {data.data.full_name}, your age: {data.age}, your \ - favourite music: {favourite_music}" -)] -struct ExitState { - data: ReceiveFavouriteMusicState, - favourite_music: FavouriteMusic, -} - -#[derive(SmartDefault)] -enum Dialogue { - #[default] - Start, - ReceiveFullName, - ReceiveAge(ReceiveAgeState), - ReceiveFavouriteMusic(ReceiveFavouriteMusicState), -} - -// ============================================================================ -// [Control a dialogue] -// ============================================================================ - -type Cx<State> = DialogueDispatcherHandlerCx<Message, State, Infallible>; -type Res = ResponseResult<DialogueStage<Dialogue>>; - -async fn start(cx: Cx<()>) -> Res { - cx.answer("Let's start! First, what's your full name?").send().await?; - next(Dialogue::ReceiveFullName) -} - -async fn full_name(cx: Cx<()>) -> Res { - match cx.update.text() { - None => { - cx.answer("Please, send me a text message!").send().await?; - next(Dialogue::ReceiveFullName) - } - Some(full_name) => { - cx.answer("What a wonderful name! Your age?").send().await?; - next(Dialogue::ReceiveAge(ReceiveAgeState { - full_name: full_name.to_owned(), - })) - } - } -} - -async fn age(cx: Cx<ReceiveAgeState>) -> Res { - match cx.update.text().unwrap().parse() { - Ok(age) => { - cx.answer("Good. Now choose your favourite music:") - .reply_markup(FavouriteMusic::markup()) - .send() - .await?; - next(Dialogue::ReceiveFavouriteMusic(ReceiveFavouriteMusicState { - data: cx.dialogue.unwrap(), - age, - })) - } - Err(_) => { - cx.answer("Oh, please, enter a number!").send().await?; - next(Dialogue::ReceiveAge(cx.dialogue.unwrap())) - } - } -} - -async fn favourite_music(cx: Cx<ReceiveFavouriteMusicState>) -> Res { - match cx.update.text().unwrap().parse() { - Ok(favourite_music) => { - cx.answer(format!( - "Fine. {}", - ExitState { - data: cx.dialogue.clone().unwrap(), - favourite_music - } - )) - .send() - .await?; - exit() - } - Err(_) => { - cx.answer("Oh, please, enter from the keyboard!").send().await?; - next(Dialogue::ReceiveFavouriteMusic(cx.dialogue.unwrap())) - } - } -} - -async fn handle_message(cx: Cx<Dialogue>) -> Res { - let DialogueDispatcherHandlerCx { bot, update, dialogue } = cx; - - // You need handle the error instead of panicking in real-world code, maybe - // send diagnostics to a development chat. - match dialogue.expect("Failed to get dialogue info from storage") { - Dialogue::Start => { - start(DialogueDispatcherHandlerCx::new(bot, update, ())).await - } - Dialogue::ReceiveFullName => { - full_name(DialogueDispatcherHandlerCx::new(bot, update, ())).await - } - Dialogue::ReceiveAge(s) => { - age(DialogueDispatcherHandlerCx::new(bot, update, s)).await - } - Dialogue::ReceiveFavouriteMusic(s) => { - favourite_music(DialogueDispatcherHandlerCx::new(bot, update, s)) - .await - } - } -} - -// ============================================================================ -// [Run!] -// ============================================================================ +use teloxide::prelude::*; #[tokio::main] async fn main() { @@ -190,7 +39,16 @@ async fn run() { Dispatcher::new(bot) .messages_handler(DialogueDispatcher::new(|cx| async move { - handle_message(cx).await.expect("Something wrong with the bot!") + let DialogueWithCx { cx, dialogue } = cx; + + // Unwrap without panic because of std::convert::Infallible. + let Wrapper(dialogue) = dialogue.unwrap(); + + dispatch!( + [cx, dialogue] -> + [start, receive_full_name, receive_age, receive_favourite_music] + ) + .expect("Something wrong with the bot!") })) .dispatch() .await; diff --git a/examples/dialogue_bot/src/states.rs b/examples/dialogue_bot/src/states.rs new file mode 100644 index 00000000..c920392a --- /dev/null +++ b/examples/dialogue_bot/src/states.rs @@ -0,0 +1,49 @@ +use teloxide::prelude::*; + +use super::favourite_music::FavouriteMusic; +use parse_display::Display; + +pub struct StartState; + +pub struct ReceiveFullNameState { + rest: StartState, +} + +pub struct ReceiveAgeState { + rest: ReceiveFullNameState, + full_name: String, +} + +pub struct ReceiveFavouriteMusicState { + rest: ReceiveAgeState, + age: u8, +} + +#[derive(Display)] +#[display( + "Your full name: {rest.rest.full_name}, your age: {rest.age}, your \ + favourite music: {favourite_music}" +)] +pub struct ExitState { + rest: ReceiveFavouriteMusicState, + favourite_music: FavouriteMusic, +} + +up!( + StartState -> ReceiveFullNameState, + ReceiveFullNameState + [full_name: String] -> ReceiveAgeState, + ReceiveAgeState + [age: u8] -> ReceiveFavouriteMusicState, + ReceiveFavouriteMusicState + [favourite_music: FavouriteMusic] -> ExitState, +); + +pub type Dialogue = Coprod!( + StartState, + ReceiveFullNameState, + ReceiveAgeState, + ReceiveFavouriteMusicState, +); + +wrap_dialogue!( + Wrapper(Dialogue), + default Self(Dialogue::inject(StartState)), +); diff --git a/examples/dialogue_bot/src/transitions.rs b/examples/dialogue_bot/src/transitions.rs new file mode 100644 index 00000000..4a20fafb --- /dev/null +++ b/examples/dialogue_bot/src/transitions.rs @@ -0,0 +1,64 @@ +use teloxide::prelude::*; + +use super::{favourite_music::FavouriteMusic, states::*}; + +pub type In<State> = TransitionIn<State, std::convert::Infallible>; +pub type Out = TransitionOut<Wrapper>; + +pub async fn start(cx: In<StartState>) -> Out { + let (cx, dialogue) = cx.unpack(); + + cx.answer_str("Let's start! First, what's your full name?").await?; + next(dialogue.up()) +} + +pub async fn receive_full_name(cx: In<ReceiveFullNameState>) -> Out { + let (cx, dialogue) = cx.unpack(); + + match cx.update.text_owned() { + Some(full_name) => { + cx.answer_str("What a wonderful name! Your age?").await?; + next(dialogue.up(full_name)) + } + _ => { + cx.answer_str("Please, enter a text message!").await?; + next(dialogue) + } + } +} + +pub async fn receive_age(cx: In<ReceiveAgeState>) -> Out { + let (cx, dialogue) = cx.unpack(); + + match cx.update.text().map(str::parse) { + Some(Ok(age)) => { + cx.answer("Good. Now choose your favourite music:") + .reply_markup(FavouriteMusic::markup()) + .send() + .await?; + next(dialogue.up(age)) + } + _ => { + cx.answer_str("Please, enter a number!").await?; + next(dialogue) + } + } +} + +pub async fn receive_favourite_music( + cx: In<ReceiveFavouriteMusicState>, +) -> Out { + let (cx, dialogue) = cx.unpack(); + + match cx.update.text().map(str::parse) { + Some(Ok(favourite_music)) => { + cx.answer_str(format!("Fine. {}", dialogue.up(favourite_music))) + .await?; + exit() + } + _ => { + cx.answer_str("Please, enter from the keyboard!").await?; + next(dialogue) + } + } +} diff --git a/examples/guess_a_number_bot/Cargo.toml b/examples/guess_a_number_bot/Cargo.toml deleted file mode 100644 index c81ef20f..00000000 --- a/examples/guess_a_number_bot/Cargo.toml +++ /dev/null @@ -1,15 +0,0 @@ -[package] -name = "guess_a_number_bot" -version = "0.1.0" -authors = ["Temirkhan Myrzamadi <hirrolot@gmail.com>"] -edition = "2018" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -log = "0.4.8" -tokio = "0.2.9" -smart-default = "0.6.0" -rand = "0.7.3" -pretty_env_logger = "0.4.0" -teloxide = { path = "../../" } \ No newline at end of file diff --git a/examples/guess_a_number_bot/src/main.rs b/examples/guess_a_number_bot/src/main.rs deleted file mode 100644 index 019f593a..00000000 --- a/examples/guess_a_number_bot/src/main.rs +++ /dev/null @@ -1,121 +0,0 @@ -// This is a guess-a-number game! -// -// # Example -// ``` -// - Hello -// - Let's play a game! Guess a number from 1 to 10 (inclusively). -// - 4 -// - No. -// - 3 -// - No. -// - Blablabla -// - Oh, please, send me a text message! -// - 111 -// - Oh, please, send me a number in the range [1; 10]! -// - 5 -// - Congratulations! You won! -// ``` - -#[macro_use] -extern crate smart_default; - -use teloxide::prelude::*; - -use rand::{thread_rng, Rng}; -use std::convert::Infallible; - -// ============================================================================ -// [A type-safe finite automaton] -// ============================================================================ - -#[derive(SmartDefault)] -enum Dialogue { - #[default] - Start, - ReceiveAttempt(u8), -} - -// ============================================================================ -// [Control a dialogue] -// ============================================================================ - -type Cx<State> = DialogueDispatcherHandlerCx<Message, State, Infallible>; -type Res = ResponseResult<DialogueStage<Dialogue>>; - -async fn start(cx: Cx<()>) -> Res { - cx.answer("Let's play a game! Guess a number from 1 to 10 (inclusively).") - .send() - .await?; - next(Dialogue::ReceiveAttempt(thread_rng().gen_range(1, 11))) -} - -async fn receive_attempt(cx: Cx<u8>) -> Res { - let secret = cx.dialogue.unwrap(); - - match cx.update.text() { - None => { - cx.answer("Oh, please, send me a text message!").send().await?; - next(Dialogue::ReceiveAttempt(secret)) - } - Some(text) => match text.parse::<u8>() { - Ok(attempt) => { - if attempt == secret { - cx.answer("Congratulations! You won!").send().await?; - exit() - } else { - cx.answer("No.").send().await?; - next(Dialogue::ReceiveAttempt(secret)) - } - } - Err(_) => { - cx.answer("Oh, please, send me a number in the range [1; 10]!") - .send() - .await?; - next(Dialogue::ReceiveAttempt(secret)) - } - }, - } -} - -async fn handle_message( - cx: DialogueDispatcherHandlerCx<Message, Dialogue, Infallible>, -) -> Res { - let DialogueDispatcherHandlerCx { bot, update, dialogue } = cx; - - // You need handle the error instead of panicking in real-world code, maybe - // send diagnostics to a development chat. - match dialogue.expect("Failed to get dialogue info from storage") { - Dialogue::Start => { - start(DialogueDispatcherHandlerCx::new(bot, update, ())).await - } - Dialogue::ReceiveAttempt(secret) => { - receive_attempt(DialogueDispatcherHandlerCx::new( - bot, update, secret, - )) - .await - } - } -} - -// ============================================================================ -// [Run!] -// ============================================================================ - -#[tokio::main] -async fn main() { - run().await; -} - -async fn run() { - teloxide::enable_logging!(); - log::info!("Starting guess_a_number_bot!"); - - let bot = Bot::from_env(); - - Dispatcher::new(bot) - .messages_handler(DialogueDispatcher::new(|cx| async move { - handle_message(cx).await.expect("Something wrong with the bot!") - })) - .dispatch() - .await; -} diff --git a/examples/heroku_ping_pong_bot/Cargo.toml b/examples/heroku_ping_pong_bot/Cargo.toml index 984abfb1..d5d8d1f1 100644 --- a/examples/heroku_ping_pong_bot/Cargo.toml +++ b/examples/heroku_ping_pong_bot/Cargo.toml @@ -11,7 +11,7 @@ log = "0.4.8" futures = "0.3.4" tokio = "0.2.9" pretty_env_logger = "0.4.0" -teloxide = "0.2.0" +teloxide = { path = "../../" } # Used to setup a webhook warp = "0.2.2" diff --git a/examples/heroku_ping_pong_bot/src/main.rs b/examples/heroku_ping_pong_bot/src/main.rs index 06d2491c..57d3d105 100644 --- a/examples/heroku_ping_pong_bot/src/main.rs +++ b/examples/heroku_ping_pong_bot/src/main.rs @@ -14,14 +14,19 @@ async fn main() { run().await; } -async fn handle_rejection(error: warp::Rejection) -> Result<impl warp::Reply, Infallible> { +async fn handle_rejection( + error: warp::Rejection, +) -> Result<impl warp::Reply, Infallible> { log::error!("Cannot process the request due to: {:?}", error); Ok(StatusCode::INTERNAL_SERVER_ERROR) } -pub async fn webhook<'a>(bot: Arc<Bot>) -> impl update_listeners::UpdateListener<Infallible> { +pub async fn webhook<'a>( + bot: Arc<Bot>, +) -> impl update_listeners::UpdateListener<Infallible> { // Heroku defines auto defines a port value - let teloxide_token = env::var("TELOXIDE_TOKEN").expect("TELOXIDE_TOKEN env variable missing"); + let teloxide_token = env::var("TELOXIDE_TOKEN") + .expect("TELOXIDE_TOKEN env variable missing"); let port: u16 = env::var("PORT") .expect("PORT env variable missing") .parse() @@ -31,10 +36,7 @@ pub async fn webhook<'a>(bot: Arc<Bot>) -> impl update_listeners::UpdateListener let path = format!("bot{}", teloxide_token); let url = format!("https://{}/{}", host, path); - bot.set_webhook(url) - .send() - .await - .expect("Cannot setup a webhook"); + bot.set_webhook(url).send().await.expect("Cannot setup a webhook"); let (tx, rx) = mpsc::unbounded_channel(); @@ -80,12 +82,14 @@ async fn run() { Dispatcher::new(Arc::clone(&bot)) .messages_handler(|rx: DispatcherHandlerRx<Message>| { rx.for_each(|message| async move { - message.answer("pong").send().await.log_on_error().await; + message.answer_str("pong").await.log_on_error().await; }) }) .dispatch_with_listener( webhook(bot).await, - LoggingErrorHandler::with_custom_text("An error from the update listener"), + LoggingErrorHandler::with_custom_text( + "An error from the update listener", + ), ) .await; } diff --git a/examples/ping_pong_bot/src/main.rs b/examples/ping_pong_bot/src/main.rs index b59e8c0b..1b52e4cc 100644 --- a/examples/ping_pong_bot/src/main.rs +++ b/examples/ping_pong_bot/src/main.rs @@ -16,7 +16,7 @@ async fn run() { Dispatcher::new(bot) .messages_handler(|rx: DispatcherHandlerRx<Message>| { rx.for_each(|message| async move { - message.answer("pong").send().await.log_on_error().await; + message.answer_str("pong").await.log_on_error().await; }) }) .dispatch() diff --git a/examples/shared_state_bot/src/main.rs b/examples/shared_state_bot/src/main.rs index a066849b..edfc1a6c 100644 --- a/examples/shared_state_bot/src/main.rs +++ b/examples/shared_state_bot/src/main.rs @@ -26,11 +26,10 @@ async fn run() { let previous = MESSAGES_TOTAL.fetch_add(1, Ordering::Relaxed); message - .answer(format!( + .answer_str(format!( "I received {} messages in total.", previous )) - .send() .await .log_on_error() .await; diff --git a/examples/simple_commands_bot/src/main.rs b/examples/simple_commands_bot/src/main.rs index 25bdb39a..8c6d71be 100644 --- a/examples/simple_commands_bot/src/main.rs +++ b/examples/simple_commands_bot/src/main.rs @@ -18,7 +18,7 @@ fn generate() -> String { } async fn answer( - cx: DispatcherHandlerCx<Message>, + cx: UpdateWithCx<Message>, command: Command, ) -> ResponseResult<()> { match command { diff --git a/examples/webhook_ping_pong_bot/src/main.rs b/examples/webhook_ping_pong_bot/src/main.rs index 8f41c0e2..2c33d03e 100644 --- a/examples/webhook_ping_pong_bot/src/main.rs +++ b/examples/webhook_ping_pong_bot/src/main.rs @@ -63,7 +63,7 @@ async fn run() { Dispatcher::new(Arc::clone(&bot)) .messages_handler(|rx: DispatcherHandlerRx<Message>| { rx.for_each(|message| async move { - message.answer("pong").send().await.log_on_error().await; + message.answer_str("pong").await.log_on_error().await; }) }) .dispatch_with_listener( diff --git a/src/dispatching/dialogue/dialogue_dispatcher.rs b/src/dispatching/dialogue/dialogue_dispatcher.rs index 0181a564..385545c2 100644 --- a/src/dispatching/dialogue/dialogue_dispatcher.rs +++ b/src/dispatching/dialogue/dialogue_dispatcher.rs @@ -1,9 +1,9 @@ use crate::dispatching::{ dialogue::{ - DialogueDispatcherHandler, DialogueDispatcherHandlerCx, DialogueStage, - GetChatId, InMemStorage, Storage, + DialogueDispatcherHandler, DialogueStage, DialogueWithCx, GetChatId, + InMemStorage, Storage, }, - DispatcherHandler, DispatcherHandlerCx, + DispatcherHandler, UpdateWithCx, }; use std::{convert::Infallible, marker::PhantomData}; @@ -34,7 +34,7 @@ pub struct DialogueDispatcher<D, S, H, Upd> { /// A value is the TX part of an unbounded asynchronous MPSC channel. A /// handler that executes updates from the same chat ID sequentially /// handles the RX part. - senders: Arc<Map<i64, mpsc::UnboundedSender<DispatcherHandlerCx<Upd>>>>, + senders: Arc<Map<i64, mpsc::UnboundedSender<UpdateWithCx<Upd>>>>, } impl<D, H, Upd> DialogueDispatcher<D, InMemStorage<D>, H, Upd> @@ -78,14 +78,14 @@ where } #[must_use] - fn new_tx(&self) -> mpsc::UnboundedSender<DispatcherHandlerCx<Upd>> { + fn new_tx(&self) -> mpsc::UnboundedSender<UpdateWithCx<Upd>> { let (tx, rx) = mpsc::unbounded_channel(); let storage = Arc::clone(&self.storage); let handler = Arc::clone(&self.handler); let senders = Arc::clone(&self.senders); - tokio::spawn(rx.for_each(move |cx: DispatcherHandlerCx<Upd>| { + tokio::spawn(rx.for_each(move |cx: UpdateWithCx<Upd>| { let storage = Arc::clone(&storage); let handler = Arc::clone(&handler); let senders = Arc::clone(&senders); @@ -98,14 +98,7 @@ where .await .map(Option::unwrap_or_default); - match handler - .handle(DialogueDispatcherHandlerCx { - bot: cx.bot, - update: cx.update, - dialogue, - }) - .await - { + match handler.handle(DialogueWithCx { cx, dialogue }).await { DialogueStage::Next(new_dialogue) => { if let Ok(Some(_)) = storage.update_dialogue(chat_id, new_dialogue).await @@ -144,10 +137,10 @@ where { fn handle( self, - updates: mpsc::UnboundedReceiver<DispatcherHandlerCx<Upd>>, + updates: mpsc::UnboundedReceiver<UpdateWithCx<Upd>>, ) -> BoxFuture<'static, ()> where - DispatcherHandlerCx<Upd>: 'static, + UpdateWithCx<Upd>: 'static, { let this = Arc::new(self); @@ -221,10 +214,10 @@ mod tests { } let dispatcher = DialogueDispatcher::new( - |cx: DialogueDispatcherHandlerCx<MyUpdate, (), Infallible>| async move { + |cx: DialogueWithCx<MyUpdate, (), Infallible>| async move { delay_for(Duration::from_millis(300)).await; - match cx.update { + match cx.cx.update { MyUpdate { chat_id: 1, unique_number } => { SEQ1.lock().await.push(unique_number); } @@ -266,11 +259,11 @@ mod tests { MyUpdate::new(3, 1611), ] .into_iter() - .map(|update| DispatcherHandlerCx { + .map(|update| UpdateWithCx { update, bot: Bot::new("Doesn't matter here"), }) - .collect::<Vec<DispatcherHandlerCx<MyUpdate>>>(), + .collect::<Vec<UpdateWithCx<MyUpdate>>>(), ); let (tx, rx) = mpsc::unbounded_channel(); diff --git a/src/dispatching/dialogue/dialogue_dispatcher_handler.rs b/src/dispatching/dialogue/dialogue_dispatcher_handler.rs index 69743621..111febcd 100644 --- a/src/dispatching/dialogue/dialogue_dispatcher_handler.rs +++ b/src/dispatching/dialogue/dialogue_dispatcher_handler.rs @@ -1,4 +1,4 @@ -use crate::prelude::{DialogueDispatcherHandlerCx, DialogueStage}; +use crate::prelude::{DialogueStage, DialogueWithCx}; use futures::future::BoxFuture; use std::{future::Future, sync::Arc}; @@ -12,26 +12,23 @@ pub trait DialogueDispatcherHandler<Upd, D, E> { #[must_use] fn handle( self: Arc<Self>, - cx: DialogueDispatcherHandlerCx<Upd, D, E>, + cx: DialogueWithCx<Upd, D, E>, ) -> BoxFuture<'static, DialogueStage<D>> where - DialogueDispatcherHandlerCx<Upd, D, E>: Send + 'static; + DialogueWithCx<Upd, D, E>: Send + 'static; } impl<Upd, D, E, F, Fut> DialogueDispatcherHandler<Upd, D, E> for F where - F: Fn(DialogueDispatcherHandlerCx<Upd, D, E>) -> Fut - + Send - + Sync - + 'static, + F: Fn(DialogueWithCx<Upd, D, E>) -> Fut + Send + Sync + 'static, Fut: Future<Output = DialogueStage<D>> + Send + 'static, { fn handle( self: Arc<Self>, - cx: DialogueDispatcherHandlerCx<Upd, D, E>, + cx: DialogueWithCx<Upd, D, E>, ) -> BoxFuture<'static, Fut::Output> where - DialogueDispatcherHandlerCx<Upd, D, E>: Send + 'static, + DialogueWithCx<Upd, D, E>: Send + 'static, { Box::pin(async move { self(cx).await }) } diff --git a/src/dispatching/dialogue/dialogue_dispatcher_handler_cx.rs b/src/dispatching/dialogue/dialogue_dispatcher_handler_cx.rs deleted file mode 100644 index 7c76db74..00000000 --- a/src/dispatching/dialogue/dialogue_dispatcher_handler_cx.rs +++ /dev/null @@ -1,186 +0,0 @@ -use crate::{ - dispatching::dialogue::GetChatId, - requests::{ - DeleteMessage, EditMessageCaption, EditMessageText, ForwardMessage, - PinChatMessage, SendAnimation, SendAudio, SendContact, SendDocument, - SendLocation, SendMediaGroup, SendMessage, SendPhoto, SendSticker, - SendVenue, SendVideo, SendVideoNote, SendVoice, - }, - types::{ChatId, ChatOrInlineMessage, InputFile, InputMedia, Message}, - Bot, -}; -use std::sync::Arc; - -/// A context of a [`DialogueDispatcher`]'s message handler. -/// -/// See [the module-level documentation for the design -/// overview](crate::dispatching::dialogue). -/// -/// [`DialogueDispatcher`]: crate::dispatching::dialogue::DialogueDispatcher -#[derive(Debug)] -pub struct DialogueDispatcherHandlerCx<Upd, D, E> { - pub bot: Arc<Bot>, - pub update: Upd, - pub dialogue: Result<D, E>, -} - -impl<Upd, D, E> DialogueDispatcherHandlerCx<Upd, D, E> { - /// Creates a new instance with the provided fields. - pub fn new(bot: Arc<Bot>, update: Upd, dialogue: D) -> Self { - Self { bot, update, dialogue: Ok(dialogue) } - } - - /// Creates a new instance by substituting a dialogue and preserving - /// `self.bot` and `self.update`. - pub fn with_new_dialogue<Nd, Ne>( - self, - new_dialogue: Result<Nd, Ne>, - ) -> DialogueDispatcherHandlerCx<Upd, Nd, Ne> { - DialogueDispatcherHandlerCx { - bot: self.bot, - update: self.update, - dialogue: new_dialogue, - } - } -} - -impl<Upd, D, E> GetChatId for DialogueDispatcherHandlerCx<Upd, D, E> -where - Upd: GetChatId, -{ - fn chat_id(&self) -> i64 { - self.update.chat_id() - } -} - -impl<D, E> DialogueDispatcherHandlerCx<Message, D, E> { - pub fn answer<T>(&self, text: T) -> SendMessage - where - T: Into<String>, - { - self.bot.send_message(self.chat_id(), text) - } - - pub fn reply_to<T>(&self, text: T) -> SendMessage - where - T: Into<String>, - { - self.bot - .send_message(self.chat_id(), text) - .reply_to_message_id(self.update.id) - } - - pub fn answer_photo(&self, photo: InputFile) -> SendPhoto { - self.bot.send_photo(self.update.chat.id, photo) - } - - pub fn answer_audio(&self, audio: InputFile) -> SendAudio { - self.bot.send_audio(self.update.chat.id, audio) - } - - pub fn answer_animation(&self, animation: InputFile) -> SendAnimation { - self.bot.send_animation(self.update.chat.id, animation) - } - - pub fn answer_document(&self, document: InputFile) -> SendDocument { - self.bot.send_document(self.update.chat.id, document) - } - - pub fn answer_video(&self, video: InputFile) -> SendVideo { - self.bot.send_video(self.update.chat.id, video) - } - - pub fn answer_voice(&self, voice: InputFile) -> SendVoice { - self.bot.send_voice(self.update.chat.id, voice) - } - - pub fn answer_media_group<T>(&self, media_group: T) -> SendMediaGroup - where - T: Into<Vec<InputMedia>>, - { - self.bot.send_media_group(self.update.chat.id, media_group) - } - - pub fn answer_location( - &self, - latitude: f32, - longitude: f32, - ) -> SendLocation { - self.bot.send_location(self.update.chat.id, latitude, longitude) - } - - pub fn answer_venue<T, U>( - &self, - latitude: f32, - longitude: f32, - title: T, - address: U, - ) -> SendVenue - where - T: Into<String>, - U: Into<String>, - { - self.bot.send_venue( - self.update.chat.id, - latitude, - longitude, - title, - address, - ) - } - - pub fn answer_video_note(&self, video_note: InputFile) -> SendVideoNote { - self.bot.send_video_note(self.update.chat.id, video_note) - } - - pub fn answer_contact<T, U>( - &self, - phone_number: T, - first_name: U, - ) -> SendContact - where - T: Into<String>, - U: Into<String>, - { - self.bot.send_contact(self.chat_id(), phone_number, first_name) - } - - pub fn answer_sticker<T>(&self, sticker: InputFile) -> SendSticker { - self.bot.send_sticker(self.update.chat.id, sticker) - } - - pub fn forward_to<T>(&self, chat_id: T) -> ForwardMessage - where - T: Into<ChatId>, - { - self.bot.forward_message(chat_id, self.update.chat.id, self.update.id) - } - - pub fn edit_message_text<T>(&self, text: T) -> EditMessageText - where - T: Into<String>, - { - self.bot.edit_message_text( - ChatOrInlineMessage::Chat { - chat_id: self.update.chat.id.into(), - message_id: self.update.id, - }, - text, - ) - } - - pub fn edit_message_caption(&self) -> EditMessageCaption { - self.bot.edit_message_caption(ChatOrInlineMessage::Chat { - chat_id: self.update.chat.id.into(), - message_id: self.update.id, - }) - } - - pub fn delete_message(&self) -> DeleteMessage { - self.bot.delete_message(self.update.chat.id, self.update.id) - } - - pub fn pin_message(&self) -> PinChatMessage { - self.bot.pin_chat_message(self.update.chat.id, self.update.id) - } -} diff --git a/src/dispatching/dialogue/dialogue_stage.rs b/src/dispatching/dialogue/dialogue_stage.rs index 70989bc9..2fd3c8e7 100644 --- a/src/dispatching/dialogue/dialogue_stage.rs +++ b/src/dispatching/dialogue/dialogue_stage.rs @@ -1,3 +1,6 @@ +use crate::dispatching::dialogue::TransitionOut; +use frunk::coproduct::CoprodInjector; + /// Continue or terminate a dialogue. /// /// See [the module-level documentation for the design @@ -8,18 +11,29 @@ pub enum DialogueStage<D> { Exit, } -/// A shortcut for `Ok(DialogueStage::Next(dialogue))`. -/// -/// See [the module-level documentation for the design -/// overview](crate::dispatching::dialogue). -pub fn next<E, D>(dialogue: D) -> Result<DialogueStage<D>, E> { - Ok(DialogueStage::Next(dialogue)) +/// A dialogue wrapper to bypass orphan rules. +pub trait DialogueWrapper<D> { + fn new(dialogue: D) -> Self; } -/// A shortcut for `Ok(DialogueStage::Exit)`. +/// Returns a new dialogue state. /// /// See [the module-level documentation for the design /// overview](crate::dispatching::dialogue). -pub fn exit<E, D>() -> Result<DialogueStage<D>, E> { +pub fn next<Dialogue, State, Index, DWrapper>( + new_state: State, +) -> TransitionOut<DWrapper> +where + Dialogue: CoprodInjector<State, Index>, + DWrapper: DialogueWrapper<Dialogue>, +{ + Ok(DialogueStage::Next(DWrapper::new(Dialogue::inject(new_state)))) +} + +/// Exits a dialogue. +/// +/// See [the module-level documentation for the design +/// overview](crate::dispatching::dialogue). +pub fn exit<DWrapper>() -> TransitionOut<DWrapper> { Ok(DialogueStage::Exit) } diff --git a/src/dispatching/dialogue/dialogue_with_cx.rs b/src/dispatching/dialogue/dialogue_with_cx.rs new file mode 100644 index 00000000..5ff09e88 --- /dev/null +++ b/src/dispatching/dialogue/dialogue_with_cx.rs @@ -0,0 +1,49 @@ +use crate::dispatching::{dialogue::GetChatId, UpdateWithCx}; +use std::fmt::Debug; + +/// A context of a [`DialogueDispatcher`]'s message handler. +/// +/// See [the module-level documentation for the design +/// overview](crate::dispatching::dialogue). +/// +/// [`DialogueDispatcher`]: crate::dispatching::dialogue::DialogueDispatcher +#[derive(Debug)] +pub struct DialogueWithCx<Upd, D, E> { + pub cx: UpdateWithCx<Upd>, + pub dialogue: Result<D, E>, +} + +impl<Upd, D, E> DialogueWithCx<Upd, D, E> { + /// Returns the inner `UpdateWithCx<Upd>` and an unwrapped dialogue. + pub fn unpack(self) -> (UpdateWithCx<Upd>, D) + where + E: Debug, + { + (self.cx, self.dialogue.unwrap()) + } +} + +impl<Upd, D, E> DialogueWithCx<Upd, D, E> { + /// Creates a new instance with the provided fields. + pub fn new(cx: UpdateWithCx<Upd>, dialogue: D) -> Self { + Self { cx, dialogue: Ok(dialogue) } + } + + /// Creates a new instance by substituting a dialogue and preserving + /// `self.bot` and `self.update`. + pub fn with_new_dialogue<Nd, Ne>( + self, + new_dialogue: Result<Nd, Ne>, + ) -> DialogueWithCx<Upd, Nd, Ne> { + DialogueWithCx { cx: self.cx, dialogue: new_dialogue } + } +} + +impl<Upd, D, E> GetChatId for DialogueWithCx<Upd, D, E> +where + Upd: GetChatId, +{ + fn chat_id(&self) -> i64 { + self.cx.update.chat_id() + } +} diff --git a/src/dispatching/dialogue/mod.rs b/src/dispatching/dialogue/mod.rs index 092b8d6a..fd770d5e 100644 --- a/src/dispatching/dialogue/mod.rs +++ b/src/dispatching/dialogue/mod.rs @@ -36,22 +36,195 @@ //! [`Dispatcher::messages_handler`]: //! crate::dispatching::Dispatcher::messages_handler //! [`UpdateKind::Message(message)`]: crate::types::UpdateKind::Message -//! [`DialogueDispatcherHandlerCx<YourUpdate, D>`]: -//! crate::dispatching::dialogue::DialogueDispatcherHandlerCx +//! [`DialogueWithCx<YourUpdate, D>`]: +//! crate::dispatching::dialogue::DialogueWithCx //! [examples/dialogue_bot]: https://github.com/teloxide/teloxide/tree/master/examples/dialogue_bot #![allow(clippy::type_complexity)] mod dialogue_dispatcher; mod dialogue_dispatcher_handler; -mod dialogue_dispatcher_handler_cx; mod dialogue_stage; +mod dialogue_with_cx; mod get_chat_id; mod storage; +use crate::{requests::ResponseResult, types::Message}; pub use dialogue_dispatcher::DialogueDispatcher; pub use dialogue_dispatcher_handler::DialogueDispatcherHandler; -pub use dialogue_dispatcher_handler_cx::DialogueDispatcherHandlerCx; -pub use dialogue_stage::{exit, next, DialogueStage}; +pub use dialogue_stage::{exit, next, DialogueStage, DialogueWrapper}; +pub use dialogue_with_cx::DialogueWithCx; pub use get_chat_id::GetChatId; pub use storage::{InMemStorage, Storage}; + +/// Dispatches a dialogue state into transition functions. +/// +/// # Example +/// ```no_run +/// use teloxide::prelude::*; +/// +/// pub struct StartState; +/// pub struct ReceiveWordState; +/// pub struct ReceiveNumberState; +/// pub struct ExitState; +/// +/// pub type Dialogue = Coprod!( +/// StartState, +/// ReceiveWordState, +/// ReceiveNumberState, +/// ); +/// +/// wrap_dialogue!( +/// Wrapper(Dialogue), +/// default Self(Dialogue::inject(StartState)), +/// ); +/// +/// pub type In<State> = TransitionIn<State, std::convert::Infallible>; +/// pub type Out = TransitionOut<Wrapper>; +/// +/// pub async fn start(cx: In<StartState>) -> Out { todo!() } +/// pub async fn receive_word(cx: In<ReceiveWordState>) -> Out { todo!() } +/// pub async fn receive_number(cx: In<ReceiveNumberState>) -> Out { todo!() } +/// +/// # #[tokio::main] +/// # async fn main() { +/// let cx: In<Dialogue> = todo!(); +/// let (cx, dialogue) = cx.unpack(); +/// +/// // StartState -> start +/// // ReceiveWordState -> receive_word +/// // ReceiveNumberState -> receive_number +/// let stage = dispatch!( +/// [cx, dialogue] -> +/// [start, receive_word, receive_number] +/// ); +/// # } +/// ``` +#[macro_export] +macro_rules! dispatch { + ([$cx:ident, $dialogue:ident] -> [$transition:ident, $($transitions:ident),+]) => { + match $dialogue { + Coproduct::Inl(state) => { + $transition(teloxide::dispatching::dialogue::DialogueWithCx::new($cx, state)).await + } + Coproduct::Inr(another) => { dispatch!([$cx, another] -> [$($transitions),+]) } + } + }; + + ([$cx:ident, $dialogue:ident] -> [$transition:ident]) => { + match $dialogue { + Coproduct::Inl(state) => { + $transition(teloxide::dispatching::dialogue::DialogueWithCx::new($cx, state)).await + } + Coproduct::Inr(_absurd) => unreachable!(), + } + }; +} + +/// Generates a dialogue wrapper and implements `Default` for it. +/// +/// The reason is to bypass orphan rules to be able to pass a user-defined +/// dialogue into [`DialogueDispatcher`]. Since a dialogue is +/// [`frunk::Coproduct`], we cannot directly satisfy the `D: Default` +/// constraint. +/// +/// # Examples +/// ``` +/// use teloxide::prelude::*; +/// +/// struct StartState; +/// struct ReceiveWordState; +/// struct ReceiveNumberState; +/// struct ExitState; +/// +/// type Dialogue = Coprod!( +/// StartState, +/// ReceiveWordState, +/// ReceiveNumberState, +/// ); +/// +/// wrap_dialogue!( +/// Wrapper(Dialogue), +/// default Self(Dialogue::inject(StartState)), +/// ); +/// +/// let start_state = Wrapper::default(); +/// ``` +/// +/// [`DialogueDispatcher`]: crate::dispatching::dialogue::DialogueDispatcher +/// [`frunk::Coproduct`]: https://docs.rs/frunk/0.3.1/frunk/coproduct/enum.Coproduct.html +#[macro_export] +macro_rules! wrap_dialogue { + ($name:ident($dialogue:ident), default $default_block:expr, ) => { + pub struct $name(pub $dialogue); + + impl teloxide::dispatching::dialogue::DialogueWrapper<$dialogue> + for $name + { + fn new(d: $dialogue) -> Wrapper { + $name(d) + } + } + + impl Default for $name { + fn default() -> $name { + $default_block + } + } + }; +} + +/// Generates `.up(field)` methods for dialogue states. +/// +/// Given inductively defined states, this macro generates `.up(field)` methods +/// from `Sn` to `Sn+1`. +/// +/// # Examples +/// ``` +/// use teloxide::prelude::*; +/// +/// struct StartState; +/// +/// struct ReceiveWordState { +/// rest: StartState, +/// } +/// +/// struct ReceiveNumberState { +/// rest: ReceiveWordState, +/// word: String, +/// } +/// +/// struct ExitState { +/// rest: ReceiveNumberState, +/// number: i32, +/// } +/// +/// up!( +/// StartState -> ReceiveWordState, +/// ReceiveWordState + [word: String] -> ReceiveNumberState, +/// ReceiveNumberState + [number: i32] -> ExitState, +/// ); +/// +/// let start_state = StartState; +/// let receive_word_state = start_state.up(); +/// let receive_number_state = receive_word_state.up("Hello".to_owned()); +/// let exit_state = receive_number_state.up(123); +/// ``` +#[macro_export] +macro_rules! up { + ( $( $from:ident $(+ [$field_name:ident : $field_type:ty])? -> $to:ident ),+, ) => { + $( + impl $from { + pub fn up(self, $( $field_name: $field_type )?) -> $to { + $to { rest: self, $($field_name)? } + } + } + )+ + }; +} + +/// A type passed into a FSM transition function. +pub type TransitionIn<State, E> = DialogueWithCx<Message, State, E>; + +// A type returned from a FSM transition function. +pub type TransitionOut<DWrapper> = ResponseResult<DialogueStage<DWrapper>>; diff --git a/src/dispatching/dispatcher.rs b/src/dispatching/dispatcher.rs index 8390a623..1a7fdad4 100644 --- a/src/dispatching/dispatcher.rs +++ b/src/dispatching/dispatcher.rs @@ -1,7 +1,7 @@ use crate::{ dispatching::{ update_listeners, update_listeners::UpdateListener, DispatcherHandler, - DispatcherHandlerCx, + UpdateWithCx, }, error_handlers::{ErrorHandler, LoggingErrorHandler}, types::{ @@ -14,7 +14,7 @@ use futures::StreamExt; use std::{fmt::Debug, sync::Arc}; use tokio::sync::mpsc; -type Tx<Upd> = Option<mpsc::UnboundedSender<DispatcherHandlerCx<Upd>>>; +type Tx<Upd> = Option<mpsc::UnboundedSender<UpdateWithCx<Upd>>>; #[macro_use] mod macros { @@ -36,7 +36,7 @@ fn send<'a, Upd>( { if let Some(tx) = tx { if let Err(error) = - tx.send(DispatcherHandlerCx { bot: Arc::clone(&bot), update }) + tx.send(UpdateWithCx { bot: Arc::clone(&bot), update }) { log::error!( "The RX part of the {} channel is closed, but an update is \ diff --git a/src/dispatching/dispatcher_handler.rs b/src/dispatching/dispatcher_handler.rs index a1ae2c55..a00f2479 100644 --- a/src/dispatching/dispatcher_handler.rs +++ b/src/dispatching/dispatcher_handler.rs @@ -1,6 +1,6 @@ use std::future::Future; -use crate::dispatching::{DispatcherHandlerCx, DispatcherHandlerRx}; +use crate::dispatching::{DispatcherHandlerRx, UpdateWithCx}; use futures::future::BoxFuture; /// An asynchronous handler of a stream of updates used in [`Dispatcher`]. @@ -16,7 +16,7 @@ pub trait DispatcherHandler<Upd> { updates: DispatcherHandlerRx<Upd>, ) -> BoxFuture<'static, ()> where - DispatcherHandlerCx<Upd>: Send + 'static; + UpdateWithCx<Upd>: Send + 'static; } impl<Upd, F, Fut> DispatcherHandler<Upd> for F @@ -26,7 +26,7 @@ where { fn handle(self, updates: DispatcherHandlerRx<Upd>) -> BoxFuture<'static, ()> where - DispatcherHandlerCx<Upd>: Send + 'static, + UpdateWithCx<Upd>: Send + 'static, { Box::pin(async move { self(updates).await }) } diff --git a/src/dispatching/dispatcher_handler_rx_ext.rs b/src/dispatching/dispatcher_handler_rx_ext.rs index e0aa607d..99d567ae 100644 --- a/src/dispatching/dispatcher_handler_rx_ext.rs +++ b/src/dispatching/dispatcher_handler_rx_ext.rs @@ -1,5 +1,5 @@ use crate::{ - prelude::DispatcherHandlerCx, types::Message, utils::command::BotCommand, + prelude::UpdateWithCx, types::Message, utils::command::BotCommand, }; use futures::{stream::BoxStream, Stream, StreamExt}; @@ -10,18 +10,18 @@ pub trait DispatcherHandlerRxExt { /// Extracts only text messages from this stream of arbitrary messages. fn text_messages( self, - ) -> BoxStream<'static, (DispatcherHandlerCx<Message>, String)> + ) -> BoxStream<'static, (UpdateWithCx<Message>, String)> where - Self: Stream<Item = DispatcherHandlerCx<Message>>; + Self: Stream<Item = UpdateWithCx<Message>>; /// Extracts only commands with their arguments from this stream of /// arbitrary messages. fn commands<C, N>( self, bot_name: N, - ) -> BoxStream<'static, (DispatcherHandlerCx<Message>, C, Vec<String>)> + ) -> BoxStream<'static, (UpdateWithCx<Message>, C, Vec<String>)> where - Self: Stream<Item = DispatcherHandlerCx<Message>>, + Self: Stream<Item = UpdateWithCx<Message>>, C: BotCommand, N: Into<String> + Send; } @@ -32,9 +32,9 @@ where { fn text_messages( self, - ) -> BoxStream<'static, (DispatcherHandlerCx<Message>, String)> + ) -> BoxStream<'static, (UpdateWithCx<Message>, String)> where - Self: Stream<Item = DispatcherHandlerCx<Message>>, + Self: Stream<Item = UpdateWithCx<Message>>, { Box::pin(self.filter_map(|cx| async move { cx.update.text_owned().map(|text| (cx, text)) @@ -44,9 +44,9 @@ where fn commands<C, N>( self, bot_name: N, - ) -> BoxStream<'static, (DispatcherHandlerCx<Message>, C, Vec<String>)> + ) -> BoxStream<'static, (UpdateWithCx<Message>, C, Vec<String>)> where - Self: Stream<Item = DispatcherHandlerCx<Message>>, + Self: Stream<Item = UpdateWithCx<Message>>, C: BotCommand, N: Into<String> + Send, { diff --git a/src/dispatching/mod.rs b/src/dispatching/mod.rs index 20bd9485..cb257908 100644 --- a/src/dispatching/mod.rs +++ b/src/dispatching/mod.rs @@ -80,17 +80,17 @@ pub mod dialogue; mod dispatcher; mod dispatcher_handler; -mod dispatcher_handler_cx; mod dispatcher_handler_rx_ext; pub mod update_listeners; +mod update_with_cx; pub use dispatcher::Dispatcher; pub use dispatcher_handler::DispatcherHandler; -pub use dispatcher_handler_cx::DispatcherHandlerCx; pub use dispatcher_handler_rx_ext::DispatcherHandlerRxExt; use tokio::sync::mpsc::UnboundedReceiver; +pub use update_with_cx::UpdateWithCx; /// A type of a stream, consumed by [`Dispatcher`]'s handlers. /// /// [`Dispatcher`]: crate::dispatching::Dispatcher -pub type DispatcherHandlerRx<Upd> = UnboundedReceiver<DispatcherHandlerCx<Upd>>; +pub type DispatcherHandlerRx<Upd> = UnboundedReceiver<UpdateWithCx<Upd>>; diff --git a/src/dispatching/dispatcher_handler_cx.rs b/src/dispatching/update_with_cx.rs similarity index 89% rename from src/dispatching/dispatcher_handler_cx.rs rename to src/dispatching/update_with_cx.rs index 3431892f..2292d4d9 100644 --- a/src/dispatching/dispatcher_handler_cx.rs +++ b/src/dispatching/update_with_cx.rs @@ -2,9 +2,9 @@ use crate::{ dispatching::dialogue::GetChatId, requests::{ DeleteMessage, EditMessageCaption, EditMessageText, ForwardMessage, - PinChatMessage, SendAnimation, SendAudio, SendContact, SendDocument, - SendLocation, SendMediaGroup, SendMessage, SendPhoto, SendSticker, - SendVenue, SendVideo, SendVideoNote, SendVoice, + PinChatMessage, Request, ResponseResult, SendAnimation, SendAudio, + SendContact, SendDocument, SendLocation, SendMediaGroup, SendMessage, + SendPhoto, SendSticker, SendVenue, SendVideo, SendVideoNote, SendVoice, }, types::{ChatId, ChatOrInlineMessage, InputFile, InputMedia, Message}, Bot, @@ -18,12 +18,12 @@ use std::sync::Arc; /// /// [`Dispatcher`]: crate::dispatching::Dispatcher #[derive(Debug)] -pub struct DispatcherHandlerCx<Upd> { +pub struct UpdateWithCx<Upd> { pub bot: Arc<Bot>, pub update: Upd, } -impl<Upd> GetChatId for DispatcherHandlerCx<Upd> +impl<Upd> GetChatId for UpdateWithCx<Upd> where Upd: GetChatId, { @@ -32,7 +32,14 @@ where } } -impl DispatcherHandlerCx<Message> { +impl UpdateWithCx<Message> { + pub async fn answer_str<T>(&self, text: T) -> ResponseResult<Message> + where + T: Into<String>, + { + self.answer(text).send().await + } + pub fn answer<T>(&self, text: T) -> SendMessage where T: Into<String>, diff --git a/src/prelude.rs b/src/prelude.rs index 318234dc..43e29238 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -1,20 +1,21 @@ //! Commonly used items. pub use crate::{ + dispatch, dispatching::{ dialogue::{ - exit, next, DialogueDispatcher, DialogueDispatcherHandlerCx, - DialogueStage, GetChatId, + exit, next, DialogueDispatcher, DialogueStage, DialogueWithCx, + DialogueWrapper, GetChatId, TransitionIn, TransitionOut, }, - Dispatcher, DispatcherHandlerCx, DispatcherHandlerRx, - DispatcherHandlerRxExt, + Dispatcher, DispatcherHandlerRx, DispatcherHandlerRxExt, UpdateWithCx, }, error_handlers::{LoggingErrorHandler, OnError}, requests::{Request, ResponseResult}, types::{Message, Update}, - Bot, RequestError, + up, wrap_dialogue, Bot, RequestError, }; +pub use frunk::{Coprod, Coproduct}; pub use tokio::sync::mpsc::UnboundedReceiver; pub use futures::StreamExt;