diff --git a/.gitignore b/.gitignore index 7d7050cf..f1ae605b 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ Cargo.lock .idea/ .vscode/ examples/target -examples/ping_pong_bot/target \ No newline at end of file +examples/ping_pong_bot/target +examples/simple_fsm/target \ No newline at end of file diff --git a/examples/simple_fsm/Cargo.toml b/examples/simple_fsm/Cargo.toml new file mode 100644 index 00000000..bd41efe1 --- /dev/null +++ b/examples/simple_fsm/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "simple_fsm" +version = "0.1.0" +authors = ["Temirkhan Myrzamadi "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +pretty_env_logger = "0.3.1" +log = "0.4.8" +tokio = "0.2.9" +strum = "0.17.1" +strum_macros = "0.17.1" +teloxide = { path = "../../" } \ No newline at end of file diff --git a/examples/simple_fsm/src/main.rs b/examples/simple_fsm/src/main.rs new file mode 100644 index 00000000..1a0f83fc --- /dev/null +++ b/examples/simple_fsm/src/main.rs @@ -0,0 +1,174 @@ +#[macro_use] +extern crate strum_macros; + +use std::fmt::{self, Display, Formatter}; +use teloxide::{ + prelude::*, + types::{KeyboardButton, ReplyKeyboardMarkup}, +}; + +// ============================================================================ +// [Favourite music kinds] +// ============================================================================ + +#[derive(Copy, Clone, Display, EnumString)] +enum FavouriteMusic { + Rock, + Metal, + Pop, + Other, +} + +impl FavouriteMusic { + fn markup() -> ReplyKeyboardMarkup { + ReplyKeyboardMarkup { + keyboard: vec![vec![ + KeyboardButton { + text: "Rock".to_owned(), + request: None, + }, + KeyboardButton { + text: "Metal".to_owned(), + request: None, + }, + KeyboardButton { + text: "Pop".to_owned(), + request: None, + }, + KeyboardButton { + text: "Other".to_owned(), + request: None, + }, + ]], + resize_keyboard: None, + one_time_keyboard: None, + selective: None, + } + } +} + +// ============================================================================ +// [A user's data] +// ============================================================================ + +#[derive(Default)] +struct User { + full_name: Option, + age: Option, + favourite_music: Option, +} + +impl Display for User { + fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> { + write!( + f, + "Your full name: {}, your age: {}, your favourite music: {}", + self.full_name.as_ref().unwrap(), + self.age.unwrap(), + self.favourite_music.unwrap() + ) + } +} + +// ============================================================================ +// [Some macros] +// ============================================================================ + +#[macro_export] +macro_rules! reply { + ($ctx:ident, $text:expr) => { + $ctx.reply($text).await?; + }; +} + +// ============================================================================ +// [Control our FSM] +// ============================================================================ + +async fn send_favourite_music_types(ctx: &Ctx) -> Result<(), RequestError> { + ctx.bot + .send_message(ctx.chat_id(), "Good. Now choose your favourite music:") + .reply_markup(FavouriteMusic::markup()) + .send() + .await?; + Ok(()) +} + +type Ctx = SessionHandlerCtx; +type Res = Result, RequestError>; + +async fn start(ctx: Ctx) -> Res { + reply!(ctx, "Let's start! First, what's your full name?"); + Ok(SessionState::Continue(ctx.session)) +} + +async fn full_name(mut ctx: Ctx) -> Res { + reply!(ctx, "What a wonderful name! Your age?"); + ctx.session.full_name = Some(ctx.update.text().unwrap().to_owned()); + Ok(SessionState::Continue(ctx.session)) +} + +async fn age(mut ctx: Ctx) -> Res { + match ctx.update.text().unwrap().parse() { + Ok(ok) => { + send_favourite_music_types(&ctx).await?; + ctx.session.age = Some(ok); + } + Err(_) => reply!(ctx, "Oh, please, enter a number!"), + } + + Ok(SessionState::Continue(ctx.session)) +} + +async fn favourite_music(mut ctx: Ctx) -> Res { + match ctx.update.text().unwrap().parse() { + Ok(ok) => { + ctx.session.favourite_music = Some(ok); + reply!(ctx, format!("Fine. {}", ctx.session)); + Ok(SessionState::Terminate) + } + Err(_) => { + reply!(ctx, "Oh, please, enter from the keyboard!"); + Ok(SessionState::Continue(ctx.session)) + } + } +} + +async fn handle_message(ctx: Ctx) -> Res { + if ctx.session.full_name.is_none() { + return full_name(ctx).await; + } + + if ctx.session.age.is_none() { + return age(ctx).await; + } + + if ctx.session.favourite_music.is_none() { + return favourite_music(ctx).await; + } + + Ok(SessionState::Terminate) +} + +// ============================================================================ +// [Run!] +// ============================================================================ + +#[tokio::main] +async fn main() { + std::env::set_var("RUST_LOG", "simple_fsm=trace"); + pretty_env_logger::init(); + log::info!("Starting the simple_fsm bot!"); + + Dispatcher::new(Bot::new("YourAwesomeToken")) + .message_handler(SessionDispatcher::new(|ctx| async move { + match handle_message(ctx).await { + Ok(ok) => ok, + Err(error) => { + panic!("Something wrong with the bot: {}!", error) + } + } + })) + .dispatch() + .await; +} diff --git a/src/dispatching/session/mod.rs b/src/dispatching/session/mod.rs index 4d209572..4fe65333 100644 --- a/src/dispatching/session/mod.rs +++ b/src/dispatching/session/mod.rs @@ -32,7 +32,12 @@ mod get_chat_id; mod storage; -use crate::{dispatching::AsyncHandler, Bot}; +use crate::{ + dispatching::{AsyncHandler, HandlerCtx}, + requests::{Request, ResponseResult}, + types::Message, + Bot, +}; pub use get_chat_id::*; use std::{future::Future, pin::Pin, sync::Arc}; pub use storage::*; @@ -44,10 +49,21 @@ pub struct SessionHandlerCtx { pub session: Session, } -/// A context of a session dispatcher. -pub struct SessionDispatcherCtx { - pub bot: Arc, - pub update: Upd, +impl SessionHandlerCtx { + pub fn chat_id(&self) -> i64 { + self.update.chat_id() + } + + pub async fn reply(&self, text: T) -> ResponseResult<()> + where + T: Into, + { + self.bot + .send_message(self.chat_id(), text) + .send() + .await + .map(|_| ()) + } } /// Continue or terminate a user session. @@ -92,7 +108,7 @@ where } } -impl<'a, Session, H, Upd> AsyncHandler, ()> +impl<'a, Session, H, Upd> AsyncHandler, Result<(), ()>> for SessionDispatcher<'a, Session, H> where H: AsyncHandler, SessionState>, @@ -102,8 +118,8 @@ where /// Dispatches a single `message` from a private chat. fn handle<'b>( &'b self, - ctx: SessionDispatcherCtx, - ) -> Pin + 'b>> + ctx: HandlerCtx, + ) -> Pin> + 'b>> where Upd: 'b, { @@ -137,6 +153,8 @@ where ); } } + + Ok(()) }) } } diff --git a/src/prelude.rs b/src/prelude.rs index 8882fb4c..c5dcda36 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -1,7 +1,11 @@ //! Commonly used items. pub use crate::{ - dispatching::{Dispatcher, HandlerCtx}, + dispatching::{ + session::{SessionDispatcher, SessionHandlerCtx, SessionState}, + Dispatcher, HandlerCtx, + }, + requests::{Request, ResponseResult}, types::Message, - Bot, + Bot, RequestError, };