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/
examples/target
examples/ping_pong_bot/target
examples/simple_fsm/target
examples/simple_dialogue/target

View file

@ -1,5 +1,5 @@
[package]
name = "simple_fsm"
name = "simple_dialogue"
version = "0.1.0"
authors = ["Temirkhan Myrzamadi <hirrolot@gmail.com>"]
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,
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<Message, Session>;
type Res = Result<SessionState<Session>, RequestError>;
type Ctx = DialogueHandlerCtx<Message, State, User>;
type Res = Result<DialogueStage<State, User>, 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!")

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::{
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<dyn Storage<Session> + 'a>,
pub struct DialogueDispatcher<'a, State, T, H> {
storage: Box<dyn Storage<State, T> + '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<State, T>: 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<Stg>(handler: H, storage: Stg) -> Self
where
Stg: Storage<Session> + 'a,
Stg: Storage<State, T> + 'a,
{
Self {
storage: Box::new(storage),
@ -46,14 +49,13 @@ where
}
}
impl<'a, Session, H, Upd> CtxHandler<DispatcherHandlerCtx<Upd>, Result<(), ()>>
for SessionDispatcher<'a, Session, H>
impl<'a, State, T, H, Upd> CtxHandler<DispatcherHandlerCtx<Upd>, Result<(), ()>>
for DialogueDispatcher<'a, State, T, H>
where
H: CtxHandler<SessionHandlerCtx<Upd, Session>, SessionState<Session>>,
H: CtxHandler<DialogueHandlerCtx<Upd, State, T>, DialogueStage<State, T>>,
Upd: GetChatId,
Session: Default,
Dialogue<State, T>: Default,
{
/// Dispatches a single `message` from a private chat.
fn handle_ctx<'b>(
&'b self,
ctx: DispatcherHandlerCtx<Upd>,
@ -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"
);
}
}

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
#![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};

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::{
dispatching::session::GetChatId,
dispatching::dialogue::GetChatId,
requests::{Request, ResponseResult},
types::Message,
Bot,

View file

@ -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;

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::{
dispatching::{
session::{
GetChatId, SessionDispatcher, SessionHandlerCtx, SessionState,
dialogue::{
DialogueDispatcher, DialogueHandlerCtx, DialogueStage, GetChatId,
},
Dispatcher, DispatcherHandlerCtx,
},