This commit is contained in:
Temirkhan Myrzamadi 2020-02-04 21:38:25 +06:00
parent 4002d8fbbc
commit 17de4840d7
18 changed files with 220 additions and 181 deletions

2
.gitignore vendored
View file

@ -5,4 +5,4 @@ Cargo.lock
.vscode/ .vscode/
examples/target examples/target
examples/ping_pong_bot/target examples/ping_pong_bot/target
examples/simple_fsm/target examples/simple_dialogue/target

View file

@ -1,5 +1,5 @@
[package] [package]
name = "simple_fsm" name = "simple_dialogue"
version = "0.1.0" version = "0.1.0"
authors = ["Temirkhan Myrzamadi <hirrolot@gmail.com>"] authors = ["Temirkhan Myrzamadi <hirrolot@gmail.com>"]
edition = "2018" edition = "2018"

View file

@ -54,38 +54,28 @@ impl Display for User {
} }
// ============================================================================ // ============================================================================
// [FSM - Finite-State Machine] // [States of a dialogue]
// ============================================================================ // ============================================================================
enum Fsm { enum State {
Start, Start,
FullName, FullName,
Age, Age,
FavouriteMusic, FavouriteMusic,
} }
impl Default for Fsm { impl Default for State {
fn default() -> Self { fn default() -> Self {
Self::Start Self::Start
} }
} }
// ============================================================================ // ============================================================================
// [Our Session type] // [Control a dialogue]
// ============================================================================ // ============================================================================
#[derive(Default)] type Ctx = DialogueHandlerCtx<Message, State, User>;
struct Session { type Res = Result<DialogueStage<State, User>, RequestError>;
user: User,
fsm: Fsm,
}
// ============================================================================
// [Control our FSM]
// ============================================================================
type Ctx = SessionHandlerCtx<Message, Session>;
type Res = Result<SessionState<Session>, RequestError>;
async fn send_favourite_music_types(ctx: &Ctx) -> Result<(), RequestError> { async fn send_favourite_music_types(ctx: &Ctx) -> Result<(), RequestError> {
ctx.bot ctx.bot
@ -96,53 +86,53 @@ async fn send_favourite_music_types(ctx: &Ctx) -> Result<(), RequestError> {
Ok(()) 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?") ctx.reply("Let's start! First, what's your full name?")
.await?; .await?;
ctx.session.state = Fsm::FullName; ctx.dialogue.state = State::FullName;
Ok(SessionState::Next(ctx.session)) Ok(DialogueStage::Next(ctx.dialogue))
} }
async fn full_name(mut ctx: Ctx) -> Res { async fn full_name(mut ctx: Ctx) -> Res {
ctx.reply("What a wonderful name! Your age?").await?; ctx.reply("What a wonderful name! Your age?").await?;
ctx.session.user.full_name = Some(ctx.update.text().unwrap().to_owned()); ctx.dialogue.data.full_name = Some(ctx.update.text().unwrap().to_owned());
ctx.session.fsm = Fsm::Age; ctx.dialogue.state = State::Age;
Ok(SessionState::Next(ctx.session)) Ok(DialogueStage::Next(ctx.dialogue))
} }
async fn age(mut ctx: Ctx) -> Res { async fn age(mut ctx: Ctx) -> Res {
match ctx.update.text().unwrap().parse() { match ctx.update.text().unwrap().parse() {
Ok(ok) => { Ok(ok) => {
send_favourite_music_types(&ctx).await?; send_favourite_music_types(&ctx).await?;
ctx.session.user.age = Some(ok); ctx.dialogue.data.age = Some(ok);
ctx.session.fsm = Fsm::FavouriteMusic; ctx.dialogue.state = State::FavouriteMusic;
} }
Err(_) => ctx.reply("Oh, please, enter a number!").await?, 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 { async fn favourite_music(mut ctx: Ctx) -> Res {
match ctx.update.text().unwrap().parse() { match ctx.update.text().unwrap().parse() {
Ok(ok) => { Ok(ok) => {
ctx.session.user.favourite_music = Some(ok); ctx.dialogue.data.favourite_music = Some(ok);
ctx.reply(format!("Fine. {}", ctx.session.user)).await?; ctx.reply(format!("Fine. {}", ctx.dialogue.data)).await?;
Ok(SessionState::Exit) Ok(DialogueStage::Exit)
} }
Err(_) => { Err(_) => {
ctx.reply("Oh, please, enter from the keyboard!").await?; 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 { async fn handle_message(ctx: Ctx) -> Res {
match ctx.session.fsm { match ctx.dialogue.state {
Fsm::Start => start(ctx).await, State::Start => start(ctx).await,
Fsm::FullName => full_name(ctx).await, State::FullName => full_name(ctx).await,
Fsm::Age => age(ctx).await, State::Age => age(ctx).await,
Fsm::FavouriteMusic => favourite_music(ctx).await, State::FavouriteMusic => favourite_music(ctx).await,
} }
} }
@ -152,12 +142,12 @@ async fn handle_message(ctx: Ctx) -> Res {
#[tokio::main] #[tokio::main]
async fn 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(); pretty_env_logger::init();
log::info!("Starting the simple_fsm bot!"); log::info!("Starting the simple_dialogue bot!");
Dispatcher::new(Bot::new("YourAwesomeToken")) Dispatcher::new(Bot::new("YourAwesomeToken"))
.message_handler(SessionDispatcher::new(|ctx| async move { .message_handler(DialogueDispatcher::new(|ctx| async move {
handle_message(ctx) handle_message(ctx)
.await .await
.expect("Something wrong with the bot!") .expect("Something wrong with the bot!")

View file

@ -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<String>,
/// age: Option<u8>,
/// favourite_music: Option<String>,
/// }
///
/// let _dialogue = Dialogue::new(MyState::FullName, User::default());
/// ```
#[derive(Default, Debug, Copy, Clone, Eq, Hash, PartialEq)]
pub struct Dialogue<State, T> {
pub state: State,
pub data: T,
}
impl<State, T> Dialogue<State, T> {
/// Creates new `Dialogue` with the provided fields.
#[must_use]
pub fn new(state: State, data: T) -> Self {
Self { state, data }
}
}

View file

@ -1,30 +1,33 @@
use crate::dispatching::{ use crate::dispatching::{
session::{ dialogue::{
GetChatId, InMemStorage, SessionHandlerCtx, SessionState, Storage, Dialogue, DialogueHandlerCtx, DialogueStage, GetChatId, InMemStorage,
Storage,
}, },
CtxHandler, DispatcherHandlerCtx, CtxHandler, DispatcherHandlerCtx,
}; };
use std::{future::Future, pin::Pin}; 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. /// an instance of this dispatcher into the [`Dispatcher`]'s methods.
/// ///
/// [`Dispatcher`]: crate::dispatching::Dispatcher /// [`Dispatcher`]: crate::dispatching::Dispatcher
pub struct SessionDispatcher<'a, Session, H> { pub struct DialogueDispatcher<'a, State, T, H> {
storage: Box<dyn Storage<Session> + 'a>, storage: Box<dyn Storage<State, T> + 'a>,
handler: H, handler: H,
} }
impl<'a, Session, H> SessionDispatcher<'a, Session, H> impl<'a, State, T, H> DialogueDispatcher<'a, State, T, H>
where where
Session: Default + 'a, Dialogue<State, T>: Default + 'a,
T: Default + 'a,
State: Default + 'a,
{ {
/// Creates a dispatcher with the specified `handler` and [`InMemStorage`] /// Creates a dispatcher with the specified `handler` and [`InMemStorage`]
/// (a default storage). /// (a default storage).
/// ///
/// [`InMemStorage`]: crate::dispatching::session::InMemStorage /// [`InMemStorage`]: crate::dispatching::dialogue::InMemStorage
#[must_use] #[must_use]
pub fn new(handler: H) -> Self { pub fn new(handler: H) -> Self {
Self { Self {
@ -37,7 +40,7 @@ where
#[must_use] #[must_use]
pub fn with_storage<Stg>(handler: H, storage: Stg) -> Self pub fn with_storage<Stg>(handler: H, storage: Stg) -> Self
where where
Stg: Storage<Session> + 'a, Stg: Storage<State, T> + 'a,
{ {
Self { Self {
storage: Box::new(storage), storage: Box::new(storage),
@ -46,14 +49,13 @@ where
} }
} }
impl<'a, Session, H, Upd> CtxHandler<DispatcherHandlerCtx<Upd>, Result<(), ()>> impl<'a, State, T, H, Upd> CtxHandler<DispatcherHandlerCtx<Upd>, Result<(), ()>>
for SessionDispatcher<'a, Session, H> for DialogueDispatcher<'a, State, T, H>
where where
H: CtxHandler<SessionHandlerCtx<Upd, Session>, SessionState<Session>>, H: CtxHandler<DialogueHandlerCtx<Upd, State, T>, DialogueStage<State, T>>,
Upd: GetChatId, Upd: GetChatId,
Session: Default, Dialogue<State, T>: Default,
{ {
/// Dispatches a single `message` from a private chat.
fn handle_ctx<'b>( fn handle_ctx<'b>(
&'b self, &'b self,
ctx: DispatcherHandlerCtx<Upd>, ctx: DispatcherHandlerCtx<Upd>,
@ -64,30 +66,30 @@ where
Box::pin(async move { Box::pin(async move {
let chat_id = ctx.update.chat_id(); let chat_id = ctx.update.chat_id();
let session = self let dialogue = self
.storage .storage
.remove_session(chat_id) .remove_dialogue(chat_id)
.await .await
.unwrap_or_default(); .unwrap_or_default();
if let SessionState::Next(new_session) = self if let DialogueStage::Next(new_dialogue) = self
.handler .handler
.handle_ctx(SessionHandlerCtx { .handle_ctx(DialogueHandlerCtx {
bot: ctx.bot, bot: ctx.bot,
update: ctx.update, update: ctx.update,
session, dialogue,
}) })
.await .await
{ {
if self if self
.storage .storage
.update_session(chat_id, new_session) .update_dialogue(chat_id, new_dialogue)
.await .await
.is_some() .is_some()
{ {
panic!( panic!(
"We previously storage.remove_session() so \ "We previously storage.remove_dialogue() so \
storage.update_session() must return None" storage.update_dialogue() must return None"
); );
} }
} }

View file

@ -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<Upd, State, T> {
pub bot: Arc<Bot>,
pub update: Upd,
pub dialogue: Dialogue<State, T>,
}
impl<Upd, State, T> GetChatId for DialogueHandlerCtx<Upd, State, T>
where
Upd: GetChatId,
{
fn chat_id(&self) -> i64 {
self.update.chat_id()
}
}
impl<State, T> DialogueHandlerCtx<Message, State, T> {
pub async fn reply<S>(&self, text: S) -> ResponseResult<()>
where
S: Into<String>,
{
self.bot
.send_message(self.chat_id(), text)
.send()
.await
.map(|_| ())
}
}

View file

@ -0,0 +1,8 @@
use crate::dispatching::dialogue::Dialogue;
/// Continue or terminate a dialogue.
#[derive(Debug, Copy, Clone, Eq, Hash, PartialEq)]
pub enum DialogueStage<State, T> {
Next(Dialogue<State, T>),
Exit,
}

View file

@ -31,14 +31,19 @@
// TODO: examples // 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 get_chat_id;
mod session_dispatcher;
mod session_handler_ctx;
mod session_state;
mod storage; 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 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}; pub use storage::{InMemStorage, Storage};

View file

@ -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<State, T> {
map: Mutex<HashMap<i64, Dialogue<State, T>>>,
}
#[async_trait(?Send)]
#[async_trait]
impl<State, T> Storage<State, T> for InMemStorage<State, T> {
async fn remove_dialogue(
&self,
chat_id: i64,
) -> Option<Dialogue<State, T>> {
self.map.lock().await.remove(&chat_id)
}
async fn update_dialogue(
&self,
chat_id: i64,
dialogue: Dialogue<State, T>,
) -> Option<Dialogue<State, T>> {
self.map.lock().await.insert(chat_id, dialogue)
}
}

View file

@ -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<State, T> {
/// 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<Dialogue<State, T>>;
/// 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<State, T>,
) -> Option<Dialogue<State, T>>;
}

View file

@ -1,5 +1,5 @@
use crate::{ use crate::{
dispatching::session::GetChatId, dispatching::dialogue::GetChatId,
requests::{Request, ResponseResult}, requests::{Request, ResponseResult},
types::Message, types::Message,
Bot, Bot,

View file

@ -42,11 +42,11 @@
//! [`SessionDispatcher`]: crate::dispatching::SessionDispatcher //! [`SessionDispatcher`]: crate::dispatching::SessionDispatcher
mod ctx_handlers; mod ctx_handlers;
pub mod dialogue;
mod dispatcher; mod dispatcher;
mod dispatcher_handler_ctx; mod dispatcher_handler_ctx;
mod error_handlers; mod error_handlers;
mod middleware; mod middleware;
pub mod session;
pub mod update_listeners; pub mod update_listeners;
pub use ctx_handlers::CtxHandler; pub use ctx_handlers::CtxHandler;

View file

@ -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<Upd, Session> {
pub bot: Arc<Bot>,
pub update: Upd,
pub session: Session,
}
impl<Upd, Session> GetChatId for SessionHandlerCtx<Upd, Session>
where
Upd: GetChatId,
{
fn chat_id(&self) -> i64 {
self.update.chat_id()
}
}
impl<Session> SessionHandlerCtx<Message, Session> {
pub async fn reply<T>(&self, text: T) -> ResponseResult<()>
where
T: Into<String>,
{
self.bot
.send_message(self.chat_id(), text)
.send()
.await
.map(|_| ())
}
}

View file

@ -1,6 +0,0 @@
/// Continue or terminate a user session.
#[derive(Debug, Copy, Clone, Eq, Hash, PartialEq)]
pub enum SessionState<Session> {
Next(Session),
Exit,
}

View file

@ -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<Session> {
map: Mutex<HashMap<i64, Session>>,
}
#[async_trait(?Send)]
#[async_trait]
impl<Session> Storage<Session> for InMemStorage<Session> {
async fn remove_session(&self, chat_id: i64) -> Option<Session> {
self.map.lock().await.remove(&chat_id)
}
async fn update_session(
&self,
chat_id: i64,
state: Session,
) -> Option<Session> {
self.map.lock().await.insert(chat_id, state)
}
}

View file

@ -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<Session> {
/// 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<Session>;
/// 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<Session>;
}

View file

@ -2,8 +2,8 @@
pub use crate::{ pub use crate::{
dispatching::{ dispatching::{
session::{ dialogue::{
GetChatId, SessionDispatcher, SessionHandlerCtx, SessionState, DialogueDispatcher, DialogueHandlerCtx, DialogueStage, GetChatId,
}, },
Dispatcher, DispatcherHandlerCtx, Dispatcher, DispatcherHandlerCtx,
}, },