diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e6c2d69..0087853c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [0.3.0] - ??? ### Added - `BotBuilder`, which allows setting a default `ParseMode`. + - The `BotDialogue` trait. + - Automatic `dispatch` function generation via `#[derive(BotDialogue)]` + `#[transition(transition_fn)]`. ### Deprecated - `Bot::{from_env_with_client, new, with_client}`. diff --git a/Cargo.toml b/Cargo.toml index 8f2a86cc..079b5d0c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -53,7 +53,7 @@ serde_cbor = { version = "0.11.1", optional = true } bincode = { version = "1.3.1", optional = true } #teloxide-macros = "0.3.1" -teloxide-macros = { git = "http://github.com/teloxide/teloxide-macros", branch = "dev" } +teloxide-macros = { git = "http://github.com/teloxide/teloxide-macros", branch = "master" } [dev-dependencies] smart-default = "0.6.0" diff --git a/README.md b/README.md index 23960a3d..d928011f 100644 --- a/README.md +++ b/README.md @@ -185,48 +185,50 @@ States and transition functions are placed into separated modules. For example: ```rust // Imports are omitted... +#[derive(BotDialogue, SmartDefault, From)] +pub enum Dialogue { + #[default] + #[transition(start)] + Start(StartState), + + #[transition(receive_days_of_week)] + ReceiveDaysOfWeek(ReceiveDaysOfWeekState), + + #[transition(receive_10x5_answer)] + Receive10x5Answer(Receive10x5AnswerState), + + #[transition(receive_gandalf_alternative_name)] + ReceiveGandalfAlternativeName(ReceiveGandalfAlternativeNameState), +} + #[derive(Default)] pub struct StartState; -pub struct ReceiveFullNameState { +pub struct ReceiveDaysOfWeekState { rest: StartState, } -pub struct ReceiveAgeState { - rest: ReceiveFullNameState, - full_name: String, +pub struct Receive10x5AnswerState { + rest: ReceiveDaysOfWeekState, + days_of_week: u8, } -pub struct ReceiveFavouriteMusicState { - rest: ReceiveAgeState, - age: u8, +pub struct ReceiveGandalfAlternativeNameState { + rest: Receive10x5AnswerState, + _10x5_answer: 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, + rest: ReceiveGandalfAlternativeNameState, + gandalf_alternative_name: String, } up!( - StartState -> ReceiveFullNameState, - ReceiveFullNameState + [full_name: String] -> ReceiveAgeState, - ReceiveAgeState + [age: u8] -> ReceiveFavouriteMusicState, - ReceiveFavouriteMusicState + [favourite_music: FavouriteMusic] -> ExitState, + StartState -> ReceiveDaysOfWeekState, + ReceiveDaysOfWeekState + [days_of_week: u8] -> Receive10x5AnswerState, + Receive10x5AnswerState + [_10x5_answer: u8] -> ReceiveGandalfAlternativeNameState, + ReceiveGandalfAlternativeNameState + [gandalf_alternative_name: String] -> ExitState, ); - -#[derive(SmartDefault, From)] -pub enum Dialogue { - #[default] - Start(StartState), - ReceiveFullName(ReceiveFullNameState), - ReceiveAge(ReceiveAgeState), - ReceiveFavouriteMusic(ReceiveFavouriteMusicState), -} ``` The handy `up!` macro automatically generates functions that complete one state to another by appending a field. Here are the transition functions: @@ -235,98 +237,66 @@ The handy `up!` macro automatically generates functions that complete one state ```rust // Imports are omitted... -pub type Cx = UpdateWithCx; pub type Out = TransitionOut; -async fn start(cx: Cx, state: StartState) -> Out { - cx.answer_str("Let's start! First, what's your full name?").await?; +pub async fn start(cx: TransitionIn, state: StartState) -> Out { + cx.answer_str("Let's start our test! How many days per week are there?") + .await?; next(state.up()) } -async fn receive_full_name(cx: Cx, state: ReceiveFullNameState) -> Out { - match cx.update.text_owned() { - Some(full_name) => { - cx.answer_str("What a wonderful name! Your age?").await?; - next(state.up(full_name)) - } - _ => { - cx.answer_str("Please, enter a text message!").await?; - next(state) - } - } -} - -async fn receive_age(cx: Cx, state: ReceiveAgeState) -> Out { - 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(state.up(age)) - } - _ => { - cx.answer_str("Please, enter a number!").await?; - next(state) - } - } -} - -async fn receive_favourite_music( - cx: Cx, - state: ReceiveFavouriteMusicState, +pub async fn receive_days_of_week( + cx: TransitionIn, + state: ReceiveDaysOfWeekState, ) -> Out { match cx.update.text().map(str::parse) { - Some(Ok(favourite_music)) => { - cx.answer_str(format!("Fine. {}", state.up(favourite_music))) - .await?; + Some(Ok(ans)) if ans == 7 => { + cx.answer_str("10*5 = ?").await?; + next(state.up(ans)) + } + _ => { + cx.answer_str("Try again.").await?; + next(state) + } + } +} + +pub async fn receive_10x5_answer( + cx: TransitionIn, + state: Receive10x5AnswerState, +) -> Out { + match cx.update.text().map(str::parse) { + Some(Ok(ans)) if ans == 50 => { + cx.answer_str("What's an alternative name of Gandalf?").await?; + next(state.up(ans)) + } + _ => { + cx.answer_str("Try again.").await?; + next(state) + } + } +} + +pub async fn receive_gandalf_alternative_name( + cx: TransitionIn, + state: ReceiveGandalfAlternativeNameState, +) -> Out { + match cx.update.text() { + Some(ans) if ans == "Mithrandir" => { + cx.answer_str( + "Congratulations! You've successfully passed the test!", + ) + .await?; exit() } _ => { - cx.answer_str("Please, enter from the keyboard!").await?; + cx.answer_str("Try again.").await?; next(state) } } } - -pub async fn dispatch(cx: Cx, dialogue: Dialogue) -> Out { - match dialogue { - Dialogue::Start(state) => start(cx, state).await, - Dialogue::ReceiveFullName(state) => receive_full_name(cx, state).await, - Dialogue::ReceiveAge(state) => receive_age(cx, state).await, - Dialogue::ReceiveFavouriteMusic(state) => { - receive_favourite_music(cx, state).await - } - } -} ``` -([dialogue_bot/src/favourite_music.rs](https://github.com/teloxide/teloxide/blob/master/examples/dialogue_bot/src/favourite_music.rs)) -```rust -// Imports are omitted... - -#[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"), - ]) - } -} -``` - - - ([dialogue_bot/src/main.rs](https://github.com/teloxide/teloxide/blob/master/examples/dialogue_bot/src/main.rs)) ```rust // Imports are omitted... @@ -340,9 +310,12 @@ async fn main() { Dispatcher::new(bot) .messages_handler(DialogueDispatcher::new( - |input: TransitionIn| async move { + |input: DialogueWithCx| async move { // Unwrap without panic because of std::convert::Infallible. - dispatch(input.cx, input.dialogue.unwrap()) + input + .dialogue + .unwrap() + .dispatch(input.cx) .await .expect("Something wrong with the bot!") }, diff --git a/examples/dialogue_bot/Cargo.toml b/examples/dialogue_bot/Cargo.toml index 6bf24e5d..eac96fa6 100644 --- a/examples/dialogue_bot/Cargo.toml +++ b/examples/dialogue_bot/Cargo.toml @@ -10,12 +10,11 @@ edition = "2018" log = "0.4.8" tokio = "0.2.9" pretty_env_logger = "0.4.0" - +futures = "0.3.5" smart-default = "0.6.0" derive_more = "0.99.9" -parse-display = "0.1.1" - teloxide = { path = "../../" } +teloxide-macros = { git = "http://github.com/teloxide/teloxide-macros", branch = "master" } [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 deleted file mode 100644 index 5b102332..00000000 --- a/examples/dialogue_bot/src/favourite_music.rs +++ /dev/null @@ -1,22 +0,0 @@ -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 f9b9683d..6fb15ace 100644 --- a/examples/dialogue_bot/src/main.rs +++ b/examples/dialogue_bot/src/main.rs @@ -21,13 +21,13 @@ extern crate smart_default; #[macro_use] extern crate derive_more; +#[macro_use] +extern crate teloxide_macros; -mod favourite_music; mod states; mod transitions; use states::*; -use transitions::*; use std::convert::Infallible; use teloxide::prelude::*; @@ -45,9 +45,12 @@ async fn run() { Dispatcher::new(bot) .messages_handler(DialogueDispatcher::new( - |input: TransitionIn| async move { + |input: DialogueWithCx| async move { // Unwrap without panic because of std::convert::Infallible. - dispatch(input.cx, input.dialogue.unwrap()) + input + .dialogue + .unwrap() + .dispatch(input.cx) .await .expect("Something wrong with the bot!") }, diff --git a/examples/dialogue_bot/src/states.rs b/examples/dialogue_bot/src/states.rs index 705226bd..a27ee3cc 100644 --- a/examples/dialogue_bot/src/states.rs +++ b/examples/dialogue_bot/src/states.rs @@ -1,47 +1,51 @@ use teloxide::prelude::*; -use super::favourite_music::FavouriteMusic; -use parse_display::Display; +use super::transitions::{ + receive_10x5_answer, receive_days_of_week, + receive_gandalf_alternative_name, start, +}; + +#[derive(BotDialogue, SmartDefault, From)] +pub enum Dialogue { + #[default] + #[transition(start)] + Start(StartState), + + #[transition(receive_days_of_week)] + ReceiveDaysOfWeek(ReceiveDaysOfWeekState), + + #[transition(receive_10x5_answer)] + Receive10x5Answer(Receive10x5AnswerState), + + #[transition(receive_gandalf_alternative_name)] + ReceiveGandalfAlternativeName(ReceiveGandalfAlternativeNameState), +} #[derive(Default)] pub struct StartState; -pub struct ReceiveFullNameState { +pub struct ReceiveDaysOfWeekState { rest: StartState, } -pub struct ReceiveAgeState { - rest: ReceiveFullNameState, - full_name: String, +pub struct Receive10x5AnswerState { + rest: ReceiveDaysOfWeekState, + days_of_week: u8, } -pub struct ReceiveFavouriteMusicState { - rest: ReceiveAgeState, - age: u8, +pub struct ReceiveGandalfAlternativeNameState { + rest: Receive10x5AnswerState, + _10x5_answer: 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, + rest: ReceiveGandalfAlternativeNameState, + gandalf_alternative_name: String, } up!( - StartState -> ReceiveFullNameState, - ReceiveFullNameState + [full_name: String] -> ReceiveAgeState, - ReceiveAgeState + [age: u8] -> ReceiveFavouriteMusicState, - ReceiveFavouriteMusicState + [favourite_music: FavouriteMusic] -> ExitState, + StartState -> ReceiveDaysOfWeekState, + ReceiveDaysOfWeekState + [days_of_week: u8] -> Receive10x5AnswerState, + Receive10x5AnswerState + [_10x5_answer: u8] -> ReceiveGandalfAlternativeNameState, + ReceiveGandalfAlternativeNameState + [gandalf_alternative_name: String] -> ExitState, ); - -#[derive(SmartDefault, From)] -pub enum Dialogue { - #[default] - Start(StartState), - ReceiveFullName(ReceiveFullNameState), - ReceiveAge(ReceiveAgeState), - ReceiveFavouriteMusic(ReceiveFavouriteMusicState), -} diff --git a/examples/dialogue_bot/src/transitions.rs b/examples/dialogue_bot/src/transitions.rs index 6f6a5747..a3265652 100644 --- a/examples/dialogue_bot/src/transitions.rs +++ b/examples/dialogue_bot/src/transitions.rs @@ -1,68 +1,64 @@ +use crate::states::{ + Dialogue, Receive10x5AnswerState, ReceiveDaysOfWeekState, + ReceiveGandalfAlternativeNameState, StartState, +}; use teloxide::prelude::*; -use super::{favourite_music::FavouriteMusic, states::*}; - -pub type Cx = UpdateWithCx; pub type Out = TransitionOut; -async fn start(cx: Cx, state: StartState) -> Out { - cx.answer_str("Let's start! First, what's your full name?").await?; +pub async fn start(cx: TransitionIn, state: StartState) -> Out { + cx.answer_str("Let's start our test! How many days per week are there?") + .await?; next(state.up()) } -async fn receive_full_name(cx: Cx, state: ReceiveFullNameState) -> Out { - match cx.update.text_owned() { - Some(full_name) => { - cx.answer_str("What a wonderful name! Your age?").await?; - next(state.up(full_name)) - } - _ => { - cx.answer_str("Please, enter a text message!").await?; - next(state) - } - } -} - -async fn receive_age(cx: Cx, state: ReceiveAgeState) -> Out { - 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(state.up(age)) - } - _ => { - cx.answer_str("Please, enter a number!").await?; - next(state) - } - } -} - -async fn receive_favourite_music( - cx: Cx, - state: ReceiveFavouriteMusicState, +pub async fn receive_days_of_week( + cx: TransitionIn, + state: ReceiveDaysOfWeekState, ) -> Out { match cx.update.text().map(str::parse) { - Some(Ok(favourite_music)) => { - cx.answer_str(format!("Fine. {}", state.up(favourite_music))) - .await?; + Some(Ok(ans)) if ans == 7 => { + cx.answer_str("10*5 = ?").await?; + next(state.up(ans)) + } + _ => { + cx.answer_str("Try again.").await?; + next(state) + } + } +} + +pub async fn receive_10x5_answer( + cx: TransitionIn, + state: Receive10x5AnswerState, +) -> Out { + match cx.update.text().map(str::parse) { + Some(Ok(ans)) if ans == 50 => { + cx.answer_str("What's an alternative name of Gandalf?").await?; + next(state.up(ans)) + } + _ => { + cx.answer_str("Try again.").await?; + next(state) + } + } +} + +pub async fn receive_gandalf_alternative_name( + cx: TransitionIn, + state: ReceiveGandalfAlternativeNameState, +) -> Out { + match cx.update.text() { + Some(ans) if ans == "Mithrandir" => { + cx.answer_str( + "Congratulations! You've successfully passed the test!", + ) + .await?; exit() } _ => { - cx.answer_str("Please, enter from the keyboard!").await?; + cx.answer_str("Try again.").await?; next(state) } } } - -pub async fn dispatch(cx: Cx, dialogue: Dialogue) -> Out { - match dialogue { - Dialogue::Start(state) => start(cx, state).await, - Dialogue::ReceiveFullName(state) => receive_full_name(cx, state).await, - Dialogue::ReceiveAge(state) => receive_age(cx, state).await, - Dialogue::ReceiveFavouriteMusic(state) => { - receive_favourite_music(cx, state).await - } - } -} diff --git a/examples/redis_remember_bot/src/main.rs b/examples/redis_remember_bot/src/main.rs index bffaa8aa..2b86b295 100644 --- a/examples/redis_remember_bot/src/main.rs +++ b/examples/redis_remember_bot/src/main.rs @@ -25,7 +25,7 @@ enum Error { StorageError(#[from] StorageError), } -type In = TransitionIn; +type In = DialogueWithCx; async fn handle_message(input: In) -> Out { let (cx, dialogue) = input.unpack(); diff --git a/examples/redis_remember_bot/src/transitions.rs b/examples/redis_remember_bot/src/transitions.rs index 594471e1..c294e9e4 100644 --- a/examples/redis_remember_bot/src/transitions.rs +++ b/examples/redis_remember_bot/src/transitions.rs @@ -2,10 +2,9 @@ use teloxide::prelude::*; use super::states::*; -pub type Cx = UpdateWithCx; pub type Out = TransitionOut; -async fn start(cx: Cx, state: StartState, text: &str) -> Out { +async fn start(cx: TransitionIn, state: StartState, text: &str) -> Out { if let Ok(number) = text.parse() { cx.answer_str(format!( "Remembered number {}. Now use /get or /reset", @@ -19,7 +18,11 @@ async fn start(cx: Cx, state: StartState, text: &str) -> Out { } } -async fn have_number(cx: Cx, state: HaveNumberState, text: &str) -> Out { +async fn have_number( + cx: TransitionIn, + state: HaveNumberState, + text: &str, +) -> Out { let num = state.number; if text.starts_with("/get") { @@ -34,7 +37,7 @@ async fn have_number(cx: Cx, state: HaveNumberState, text: &str) -> Out { } } -pub async fn dispatch(cx: Cx, dialogue: Dialogue, text: &str) -> Out { +pub async fn dispatch(cx: TransitionIn, dialogue: Dialogue, text: &str) -> Out { match dialogue { Dialogue::Start(state) => start(cx, state, text).await, Dialogue::HaveNumber(state) => have_number(cx, state, text).await, diff --git a/src/dispatching/dialogue/bot_dialogue.rs b/src/dispatching/dialogue/bot_dialogue.rs new file mode 100644 index 00000000..6fc7b918 --- /dev/null +++ b/src/dispatching/dialogue/bot_dialogue.rs @@ -0,0 +1,14 @@ +use crate::{ + dispatching::{dialogue::TransitionOut, UpdateWithCx}, + types::Message, +}; +use futures::future::BoxFuture; + +/// Represents a dialogue FSM. +pub trait BotDialogue: Default { + /// Turns itself into another state, depending on the input message. + fn dispatch( + self, + cx: UpdateWithCx, + ) -> BoxFuture<'static, TransitionOut>; +} diff --git a/src/dispatching/dialogue/mod.rs b/src/dispatching/dialogue/mod.rs index 09d753fd..2687d489 100644 --- a/src/dispatching/dialogue/mod.rs +++ b/src/dispatching/dialogue/mod.rs @@ -42,6 +42,7 @@ #![allow(clippy::type_complexity)] +mod bot_dialogue; mod dialogue_dispatcher; mod dialogue_dispatcher_handler; mod dialogue_stage; @@ -50,6 +51,7 @@ mod get_chat_id; mod storage; use crate::{requests::ResponseResult, types::Message}; +pub use bot_dialogue::BotDialogue; pub use dialogue_dispatcher::DialogueDispatcher; pub use dialogue_dispatcher_handler::DialogueDispatcherHandler; pub use dialogue_stage::{exit, next, DialogueStage}; @@ -59,6 +61,7 @@ pub use get_chat_id::GetChatId; #[cfg(feature = "redis-storage")] pub use storage::{RedisStorage, RedisStorageError}; +use crate::dispatching::UpdateWithCx; pub use storage::{serializer, InMemStorage, Serializer, Storage}; /// Generates `.up(field)` methods for dialogue states. @@ -110,8 +113,8 @@ macro_rules! up { }; } -/// A type passed into a FSM transition function. -pub type TransitionIn = DialogueWithCx; +/// An input passed into a FSM transition function. +pub type TransitionIn = UpdateWithCx; // A type returned from a FSM transition function. pub type TransitionOut = ResponseResult>; diff --git a/src/prelude.rs b/src/prelude.rs index 7ea96bdf..7ebec1f1 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -3,8 +3,8 @@ pub use crate::{ dispatching::{ dialogue::{ - exit, next, DialogueDispatcher, DialogueStage, DialogueWithCx, - GetChatId, TransitionIn, TransitionOut, + exit, next, BotDialogue, DialogueDispatcher, DialogueStage, + DialogueWithCx, GetChatId, TransitionIn, TransitionOut, }, Dispatcher, DispatcherHandlerRx, DispatcherHandlerRxExt, UpdateWithCx, },