diff --git a/.gitignore b/.gitignore index f1ae605b..dc7a5786 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,4 @@ Cargo.lock .vscode/ examples/target examples/ping_pong_bot/target -examples/simple_fsm/target \ No newline at end of file +examples/simple_dialogue/target \ No newline at end of file diff --git a/examples/simple_fsm/Cargo.toml b/examples/simple_dialogue/Cargo.toml similarity index 93% rename from examples/simple_fsm/Cargo.toml rename to examples/simple_dialogue/Cargo.toml index 925430c6..42d2efd7 100644 --- a/examples/simple_fsm/Cargo.toml +++ b/examples/simple_dialogue/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "simple_fsm" +name = "simple_dialogue" version = "0.1.0" authors = ["Temirkhan Myrzamadi "] edition = "2018" diff --git a/examples/simple_fsm/src/main.rs b/examples/simple_dialogue/src/main.rs similarity index 69% rename from examples/simple_fsm/src/main.rs rename to examples/simple_dialogue/src/main.rs index a27f6e4a..a99dfac0 100644 --- a/examples/simple_fsm/src/main.rs +++ b/examples/simple_dialogue/src/main.rs @@ -54,38 +54,28 @@ impl Display for User { } // ============================================================================ -// [FSM - Finite-State Machine] +// [States of a dialogue] // ============================================================================ -enum Fsm { +enum State { Start, FullName, Age, FavouriteMusic, } -impl Default for Fsm { +impl Default for State { fn default() -> Self { Self::Start } } // ============================================================================ -// [Our Session type] +// [Control a dialogue] // ============================================================================ -#[derive(Default)] -struct Session { - user: User, - fsm: Fsm, -} - -// ============================================================================ -// [Control our FSM] -// ============================================================================ - -type Ctx = SessionHandlerCtx; -type Res = Result, RequestError>; +type Ctx = DialogueHandlerCtx; +type Res = Result, RequestError>; async fn send_favourite_music_types(ctx: &Ctx) -> Result<(), RequestError> { ctx.bot @@ -96,53 +86,53 @@ async fn send_favourite_music_types(ctx: &Ctx) -> Result<(), RequestError> { Ok(()) } -async fn start(ctx: Ctx) -> Res { +async fn start(mut ctx: Ctx) -> Res { ctx.reply("Let's start! First, what's your full name?") .await?; - ctx.session.state = Fsm::FullName; - Ok(SessionState::Next(ctx.session)) + ctx.dialogue.state = State::FullName; + Ok(DialogueStage::Next(ctx.dialogue)) } async fn full_name(mut ctx: Ctx) -> Res { ctx.reply("What a wonderful name! Your age?").await?; - ctx.session.user.full_name = Some(ctx.update.text().unwrap().to_owned()); - ctx.session.fsm = Fsm::Age; - Ok(SessionState::Next(ctx.session)) + ctx.dialogue.data.full_name = Some(ctx.update.text().unwrap().to_owned()); + ctx.dialogue.state = State::Age; + Ok(DialogueStage::Next(ctx.dialogue)) } async fn age(mut ctx: Ctx) -> Res { match ctx.update.text().unwrap().parse() { Ok(ok) => { send_favourite_music_types(&ctx).await?; - ctx.session.user.age = Some(ok); - ctx.session.fsm = Fsm::FavouriteMusic; + ctx.dialogue.data.age = Some(ok); + ctx.dialogue.state = State::FavouriteMusic; } Err(_) => ctx.reply("Oh, please, enter a number!").await?, } - Ok(SessionState::Next(ctx.session)) + Ok(DialogueStage::Next(ctx.dialogue)) } async fn favourite_music(mut ctx: Ctx) -> Res { match ctx.update.text().unwrap().parse() { Ok(ok) => { - ctx.session.user.favourite_music = Some(ok); - ctx.reply(format!("Fine. {}", ctx.session.user)).await?; - Ok(SessionState::Exit) + ctx.dialogue.data.favourite_music = Some(ok); + ctx.reply(format!("Fine. {}", ctx.dialogue.data)).await?; + Ok(DialogueStage::Exit) } Err(_) => { ctx.reply("Oh, please, enter from the keyboard!").await?; - Ok(SessionState::Next(ctx.session)) + Ok(DialogueStage::Next(ctx.dialogue)) } } } async fn handle_message(ctx: Ctx) -> Res { - match ctx.session.fsm { - Fsm::Start => start(ctx).await, - Fsm::FullName => full_name(ctx).await, - Fsm::Age => age(ctx).await, - Fsm::FavouriteMusic => favourite_music(ctx).await, + match ctx.dialogue.state { + State::Start => start(ctx).await, + State::FullName => full_name(ctx).await, + State::Age => age(ctx).await, + State::FavouriteMusic => favourite_music(ctx).await, } } @@ -152,12 +142,12 @@ async fn handle_message(ctx: Ctx) -> Res { #[tokio::main] async fn main() { - std::env::set_var("RUST_LOG", "simple_fsm=trace"); + std::env::set_var("RUST_LOG", "simple_dialogue=trace"); pretty_env_logger::init(); - log::info!("Starting the simple_fsm bot!"); + log::info!("Starting the simple_dialogue bot!"); Dispatcher::new(Bot::new("YourAwesomeToken")) - .message_handler(SessionDispatcher::new(|ctx| async move { + .message_handler(DialogueDispatcher::new(|ctx| async move { handle_message(ctx) .await .expect("Something wrong with the bot!") diff --git a/src/dispatching/dialogue/dialogue.rs b/src/dispatching/dialogue/dialogue.rs new file mode 100644 index 00000000..9245aca3 --- /dev/null +++ b/src/dispatching/dialogue/dialogue.rs @@ -0,0 +1,34 @@ +/// A type, encapsulating a dialogue state and arbitrary data. +/// +/// ## Example +/// ``` +/// use teloxide::dispatching::dialogue::Dialogue; +/// +/// enum MyState { +/// FullName, +/// Age, +/// FavouriteMusic, +/// } +/// +/// #[derive(Default)] +/// struct User { +/// full_name: Option, +/// age: Option, +/// favourite_music: Option, +/// } +/// +/// let _dialogue = Dialogue::new(MyState::FullName, User::default()); +/// ``` +#[derive(Default, Debug, Copy, Clone, Eq, Hash, PartialEq)] +pub struct Dialogue { + pub state: State, + pub data: T, +} + +impl Dialogue { + /// Creates new `Dialogue` with the provided fields. + #[must_use] + pub fn new(state: State, data: T) -> Self { + Self { state, data } + } +} diff --git a/src/dispatching/session/session_dispatcher.rs b/src/dispatching/dialogue/dialogue_dispatcher.rs similarity index 56% rename from src/dispatching/session/session_dispatcher.rs rename to src/dispatching/dialogue/dialogue_dispatcher.rs index a30d7839..bb1e9a79 100644 --- a/src/dispatching/session/session_dispatcher.rs +++ b/src/dispatching/dialogue/dialogue_dispatcher.rs @@ -1,30 +1,33 @@ use crate::dispatching::{ - session::{ - GetChatId, InMemStorage, SessionHandlerCtx, SessionState, Storage, + dialogue::{ + Dialogue, DialogueHandlerCtx, DialogueStage, GetChatId, InMemStorage, + Storage, }, CtxHandler, DispatcherHandlerCtx, }; use std::{future::Future, pin::Pin}; -/// A dispatcher of user sessions. +/// A dispatcher of dialogues. /// -/// Note that `SessionDispatcher` implements `AsyncHandler`, so you can just put +/// Note that `DialogueDispatcher` implements `CtxHandler`, so you can just put /// an instance of this dispatcher into the [`Dispatcher`]'s methods. /// /// [`Dispatcher`]: crate::dispatching::Dispatcher -pub struct SessionDispatcher<'a, Session, H> { - storage: Box + 'a>, +pub struct DialogueDispatcher<'a, State, T, H> { + storage: Box + 'a>, handler: H, } -impl<'a, Session, H> SessionDispatcher<'a, Session, H> +impl<'a, State, T, H> DialogueDispatcher<'a, State, T, H> where - Session: Default + 'a, + Dialogue: Default + 'a, + T: Default + 'a, + State: Default + 'a, { /// Creates a dispatcher with the specified `handler` and [`InMemStorage`] /// (a default storage). /// - /// [`InMemStorage`]: crate::dispatching::session::InMemStorage + /// [`InMemStorage`]: crate::dispatching::dialogue::InMemStorage #[must_use] pub fn new(handler: H) -> Self { Self { @@ -37,7 +40,7 @@ where #[must_use] pub fn with_storage(handler: H, storage: Stg) -> Self where - Stg: Storage + 'a, + Stg: Storage + 'a, { Self { storage: Box::new(storage), @@ -46,14 +49,13 @@ where } } -impl<'a, Session, H, Upd> CtxHandler, Result<(), ()>> - for SessionDispatcher<'a, Session, H> +impl<'a, State, T, H, Upd> CtxHandler, Result<(), ()>> + for DialogueDispatcher<'a, State, T, H> where - H: CtxHandler, SessionState>, + H: CtxHandler, DialogueStage>, Upd: GetChatId, - Session: Default, + Dialogue: Default, { - /// Dispatches a single `message` from a private chat. fn handle_ctx<'b>( &'b self, ctx: DispatcherHandlerCtx, @@ -64,30 +66,30 @@ where Box::pin(async move { let chat_id = ctx.update.chat_id(); - let session = self + let dialogue = self .storage - .remove_session(chat_id) + .remove_dialogue(chat_id) .await .unwrap_or_default(); - if let SessionState::Next(new_session) = self + if let DialogueStage::Next(new_dialogue) = self .handler - .handle_ctx(SessionHandlerCtx { + .handle_ctx(DialogueHandlerCtx { bot: ctx.bot, update: ctx.update, - session, + dialogue, }) .await { if self .storage - .update_session(chat_id, new_session) + .update_dialogue(chat_id, new_dialogue) .await .is_some() { panic!( - "We previously storage.remove_session() so \ - storage.update_session() must return None" + "We previously storage.remove_dialogue() so \ + storage.update_dialogue() must return None" ); } } diff --git a/src/dispatching/dialogue/dialogue_handler_ctx.rs b/src/dispatching/dialogue/dialogue_handler_ctx.rs new file mode 100644 index 00000000..f238df6f --- /dev/null +++ b/src/dispatching/dialogue/dialogue_handler_ctx.rs @@ -0,0 +1,38 @@ +use crate::{ + dispatching::dialogue::{Dialogue, GetChatId}, + requests::{Request, ResponseResult}, + types::Message, + Bot, +}; +use std::sync::Arc; + +/// A context of a [`DialogueDispatcher`]'s message handler. +/// +/// [`DialogueDispatcher`]: crate::dispatching::dialogue::DialogueDispatcher +pub struct DialogueHandlerCtx { + pub bot: Arc, + pub update: Upd, + pub dialogue: Dialogue, +} + +impl GetChatId for DialogueHandlerCtx +where + Upd: GetChatId, +{ + fn chat_id(&self) -> i64 { + self.update.chat_id() + } +} + +impl DialogueHandlerCtx { + pub async fn reply(&self, text: S) -> ResponseResult<()> + where + S: Into, + { + self.bot + .send_message(self.chat_id(), text) + .send() + .await + .map(|_| ()) + } +} diff --git a/src/dispatching/dialogue/dialogue_state.rs b/src/dispatching/dialogue/dialogue_state.rs new file mode 100644 index 00000000..24a0413e --- /dev/null +++ b/src/dispatching/dialogue/dialogue_state.rs @@ -0,0 +1,8 @@ +use crate::dispatching::dialogue::Dialogue; + +/// Continue or terminate a dialogue. +#[derive(Debug, Copy, Clone, Eq, Hash, PartialEq)] +pub enum DialogueStage { + Next(Dialogue), + Exit, +} diff --git a/src/dispatching/session/get_chat_id.rs b/src/dispatching/dialogue/get_chat_id.rs similarity index 100% rename from src/dispatching/session/get_chat_id.rs rename to src/dispatching/dialogue/get_chat_id.rs diff --git a/src/dispatching/session/mod.rs b/src/dispatching/dialogue/mod.rs similarity index 82% rename from src/dispatching/session/mod.rs rename to src/dispatching/dialogue/mod.rs index b693fba9..7d97a6a8 100644 --- a/src/dispatching/session/mod.rs +++ b/src/dispatching/dialogue/mod.rs @@ -31,14 +31,19 @@ // TODO: examples +#![allow(clippy::module_inception)] +#![allow(clippy::type_complexity)] + +mod dialogue; +mod dialogue_dispatcher; +mod dialogue_handler_ctx; +mod dialogue_state; mod get_chat_id; -mod session_dispatcher; -mod session_handler_ctx; -mod session_state; mod storage; +pub use dialogue::Dialogue; +pub use dialogue_dispatcher::DialogueDispatcher; +pub use dialogue_handler_ctx::DialogueHandlerCtx; +pub use dialogue_state::DialogueStage; pub use get_chat_id::GetChatId; -pub use session_dispatcher::SessionDispatcher; -pub use session_handler_ctx::SessionHandlerCtx; -pub use session_state::SessionState; pub use storage::{InMemStorage, Storage}; diff --git a/src/dispatching/dialogue/storage/in_mem_storage.rs b/src/dispatching/dialogue/storage/in_mem_storage.rs new file mode 100644 index 00000000..9db2c5fd --- /dev/null +++ b/src/dispatching/dialogue/storage/in_mem_storage.rs @@ -0,0 +1,37 @@ +use async_trait::async_trait; + +use super::Storage; +use crate::dispatching::dialogue::Dialogue; +use std::collections::HashMap; +use tokio::sync::Mutex; + +/// A memory storage based on a hash map. Stores all the dialogues directly in +/// RAM. +/// +/// ## Note +/// All the dialogues will be lost after you restart your bot. If you need to +/// store them somewhere on a drive, you need to implement a storage +/// communicating with a DB. +#[derive(Debug, Default)] +pub struct InMemStorage { + map: Mutex>>, +} + +#[async_trait(?Send)] +#[async_trait] +impl Storage for InMemStorage { + async fn remove_dialogue( + &self, + chat_id: i64, + ) -> Option> { + self.map.lock().await.remove(&chat_id) + } + + async fn update_dialogue( + &self, + chat_id: i64, + dialogue: Dialogue, + ) -> Option> { + self.map.lock().await.insert(chat_id, dialogue) + } +} diff --git a/src/dispatching/dialogue/storage/mod.rs b/src/dispatching/dialogue/storage/mod.rs new file mode 100644 index 00000000..1fe4641e --- /dev/null +++ b/src/dispatching/dialogue/storage/mod.rs @@ -0,0 +1,34 @@ +mod in_mem_storage; + +use crate::dispatching::dialogue::Dialogue; +use async_trait::async_trait; +pub use in_mem_storage::InMemStorage; + +/// A storage of dialogues. +/// +/// You can implement this trait for a structure that communicates with a DB and +/// be sure that after you restart your bot, all the dialogues won't be lost. +/// +/// For a storage based on a simple hash map, see [`InMemStorage`]. +/// +/// [`InMemStorage`]: crate::dispatching::dialogue::InMemStorage +#[async_trait(?Send)] +#[async_trait] +pub trait Storage { + /// Removes a dialogue with the specified `chat_id`. + /// + /// Returns `None` if there wasn't such a dialogue, `Some(dialogue)` if a + /// `dialogue` was deleted. + async fn remove_dialogue(&self, chat_id: i64) + -> Option>; + + /// Updates a dialogue with the specified `chat_id`. + /// + /// Returns `None` if there wasn't such a dialogue, `Some(dialogue)` if a + /// `dialogue` was updated. + async fn update_dialogue( + &self, + chat_id: i64, + dialogue: Dialogue, + ) -> Option>; +} diff --git a/src/dispatching/dispatcher_handler_ctx.rs b/src/dispatching/dispatcher_handler_ctx.rs index c48e53fc..3eae25fb 100644 --- a/src/dispatching/dispatcher_handler_ctx.rs +++ b/src/dispatching/dispatcher_handler_ctx.rs @@ -1,5 +1,5 @@ use crate::{ - dispatching::session::GetChatId, + dispatching::dialogue::GetChatId, requests::{Request, ResponseResult}, types::Message, Bot, diff --git a/src/dispatching/mod.rs b/src/dispatching/mod.rs index 2f2f5e9f..75030f8f 100644 --- a/src/dispatching/mod.rs +++ b/src/dispatching/mod.rs @@ -42,11 +42,11 @@ //! [`SessionDispatcher`]: crate::dispatching::SessionDispatcher mod ctx_handlers; +pub mod dialogue; mod dispatcher; mod dispatcher_handler_ctx; mod error_handlers; mod middleware; -pub mod session; pub mod update_listeners; pub use ctx_handlers::CtxHandler; diff --git a/src/dispatching/session/session_handler_ctx.rs b/src/dispatching/session/session_handler_ctx.rs deleted file mode 100644 index 44ef8294..00000000 --- a/src/dispatching/session/session_handler_ctx.rs +++ /dev/null @@ -1,38 +0,0 @@ -use crate::{ - dispatching::session::GetChatId, - requests::{Request, ResponseResult}, - types::Message, - Bot, -}; -use std::sync::Arc; - -/// A context of a [`SessionDispatcher`]'s message handler. -/// -/// [`SessionDispatcher`]: crate::dispatching::session::SessionDispatcher -pub struct SessionHandlerCtx { - pub bot: Arc, - pub update: Upd, - pub session: Session, -} - -impl GetChatId for SessionHandlerCtx -where - Upd: GetChatId, -{ - fn chat_id(&self) -> i64 { - self.update.chat_id() - } -} - -impl SessionHandlerCtx { - pub async fn reply(&self, text: T) -> ResponseResult<()> - where - T: Into, - { - self.bot - .send_message(self.chat_id(), text) - .send() - .await - .map(|_| ()) - } -} diff --git a/src/dispatching/session/session_state.rs b/src/dispatching/session/session_state.rs deleted file mode 100644 index 636999cc..00000000 --- a/src/dispatching/session/session_state.rs +++ /dev/null @@ -1,6 +0,0 @@ -/// Continue or terminate a user session. -#[derive(Debug, Copy, Clone, Eq, Hash, PartialEq)] -pub enum SessionState { - Next(Session), - Exit, -} diff --git a/src/dispatching/session/storage/in_mem_storage.rs b/src/dispatching/session/storage/in_mem_storage.rs deleted file mode 100644 index 3203093c..00000000 --- a/src/dispatching/session/storage/in_mem_storage.rs +++ /dev/null @@ -1,33 +0,0 @@ -use async_trait::async_trait; - -use super::Storage; -use std::collections::HashMap; -use tokio::sync::Mutex; - -/// A memory storage based on a hash map. Stores all the sessions directly in -/// RAM. -/// -/// ## Note -/// All the sessions will be lost after you restart your bot. If you need to -/// store them somewhere on a drive, you need to implement a storage -/// communicating with a DB. -#[derive(Debug, Default)] -pub struct InMemStorage { - map: Mutex>, -} - -#[async_trait(?Send)] -#[async_trait] -impl Storage for InMemStorage { - async fn remove_session(&self, chat_id: i64) -> Option { - self.map.lock().await.remove(&chat_id) - } - - async fn update_session( - &self, - chat_id: i64, - state: Session, - ) -> Option { - self.map.lock().await.insert(chat_id, state) - } -} diff --git a/src/dispatching/session/storage/mod.rs b/src/dispatching/session/storage/mod.rs deleted file mode 100644 index 521cd934..00000000 --- a/src/dispatching/session/storage/mod.rs +++ /dev/null @@ -1,32 +0,0 @@ -mod in_mem_storage; - -use async_trait::async_trait; -pub use in_mem_storage::InMemStorage; - -/// A storage of sessions. -/// -/// You can implement this trait for a structure that communicates with a DB and -/// be sure that after you restart your bot, all the sessions won't be lost. -/// -/// For a storage based on a simple hash map, see [`InMemStorage`]. -/// -/// [`InMemStorage`]: crate::dispatching::session::InMemStorage -#[async_trait(?Send)] -#[async_trait] -pub trait Storage { - /// Removes a session with the specified `chat_id`. - /// - /// Returns `None` if there wasn't such a session, `Some(session)` if a - /// `session` was deleted. - async fn remove_session(&self, chat_id: i64) -> Option; - - /// Updates a session with the specified `chat_id`. - /// - /// Returns `None` if there wasn't such a session, `Some(session)` if a - /// `session` was updated. - async fn update_session( - &self, - chat_id: i64, - session: Session, - ) -> Option; -} diff --git a/src/prelude.rs b/src/prelude.rs index f753e3bd..dabc41d7 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -2,8 +2,8 @@ pub use crate::{ dispatching::{ - session::{ - GetChatId, SessionDispatcher, SessionHandlerCtx, SessionState, + dialogue::{ + DialogueDispatcher, DialogueHandlerCtx, DialogueStage, GetChatId, }, Dispatcher, DispatcherHandlerCtx, },