Merge remote-tracking branch 'origin/rework-dispatching' into rework-dispatching

This commit is contained in:
p0lunin 2020-02-12 16:08:39 +02:00
commit 6de0c24ed2
13 changed files with 202 additions and 179 deletions

View file

@ -1,12 +1,21 @@
use teloxide::prelude::*;
use std::env::{set_var, var};
#[tokio::main]
async fn main() {
std::env::set_var("RUST_LOG", "ping_pong_bot=trace");
// Configure a fancy logger. Let this bot print everything, but restrict
// teloxide to only log errors.
set_var("RUST_LOG", "ping_pong_bot=trace");
set_var("RUST_LOG", "teloxide=error");
pretty_env_logger::init();
log::info!("Starting the ping-pong bot!");
Dispatcher::<RequestError>::new(Bot::new("MyAwesomeToken"))
let bot = Bot::new(var("TELOXIDE_TOKEN").unwrap());
// Create a dispatcher with a single message handler that answers "pong" to
// each incoming message.
Dispatcher::<RequestError>::new(bot)
.message_handler(&|ctx: DispatcherHandlerCtx<Message>| async move {
ctx.answer("pong").send().await?;
Ok(())

View file

@ -10,9 +10,8 @@ edition = "2018"
pretty_env_logger = "0.3.1"
log = "0.4.8"
tokio = "0.2.9"
strum = "0.17.1"
smart-default = "0.6.0"
strum_macros = "0.17.1"
parse-display = "0.1.1"
teloxide = { path = "../../" }
[profile.release]

View file

@ -1,19 +1,22 @@
#[macro_use]
extern crate strum_macros;
#![allow(clippy::trivial_regex)]
#[macro_use]
extern crate smart_default;
use std::fmt::{self, Display, Formatter};
use std::env::{set_var, var};
use teloxide::{
prelude::*,
types::{KeyboardButton, ReplyKeyboardMarkup},
};
use parse_display::{Display, FromStr};
// ============================================================================
// [Favourite music kinds]
// ============================================================================
#[derive(Copy, Clone, Display, EnumString)]
#[derive(Copy, Clone, Display, FromStr)]
enum FavouriteMusic {
Rock,
Metal,
@ -33,115 +36,136 @@ impl FavouriteMusic {
}
// ============================================================================
// [A UserInfo's data]
// [A type-safe finite automaton]
// ============================================================================
// TODO: implement a type-safe UserInfo without lots of .unwrap
#[derive(Default)]
struct UserInfo {
full_name: Option<String>,
age: Option<u8>,
favourite_music: Option<FavouriteMusic>,
#[derive(Clone)]
struct ReceiveAgeState {
full_name: String,
}
impl Display for UserInfo {
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()
)
}
#[derive(Clone)]
struct ReceiveFavouriteMusicState {
data: ReceiveAgeState,
age: u8,
}
// ============================================================================
// [States of a dialogue]
// ============================================================================
#[derive(Display)]
#[display(
"Your full name: {data.data.full_name}, your age: {data.age}, your \
favourite music: {favourite_music}"
)]
struct ExitState {
data: ReceiveFavouriteMusicState,
favourite_music: FavouriteMusic,
}
#[derive(SmartDefault)]
enum State {
enum Dialogue {
#[default]
Start,
FullName,
Age,
FavouriteMusic,
ReceiveFullName,
ReceiveAge(ReceiveAgeState),
ReceiveFavouriteMusic(ReceiveFavouriteMusicState),
}
// ============================================================================
// [Control a dialogue]
// ============================================================================
type Ctx = DialogueHandlerCtx<Message, State, UserInfo>;
type Res = Result<DialogueStage<State, UserInfo>, RequestError>;
type Ctx<State> = DialogueHandlerCtx<Message, State>;
type Res = Result<DialogueStage<Dialogue>, RequestError>;
async fn send_favourite_music_types(ctx: &Ctx) -> Result<(), RequestError> {
ctx.answer("Good. Now choose your favourite music:")
.reply_markup(FavouriteMusic::markup())
.send()
.await?;
Ok(())
}
async fn start(mut ctx: Ctx) -> Res {
async fn start(ctx: Ctx<()>) -> Res {
ctx.answer("Let's start! First, what's your full name?")
.send()
.await?;
state!(ctx, State::FullName);
next(ctx.dialogue)
next(Dialogue::ReceiveFullName)
}
async fn full_name(mut ctx: Ctx) -> Res {
ctx.answer("What a wonderful name! Your age?")
.send()
.await?;
ctx.dialogue.data.full_name = Some(ctx.update.text().unwrap().to_owned());
state!(ctx, State::Age);
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.dialogue.data.age = Some(ok);
state!(ctx, State::FavouriteMusic);
async fn full_name(ctx: Ctx<()>) -> Res {
match ctx.update.text() {
None => {
ctx.answer("Please, send me a text message!").send().await?;
next(Dialogue::ReceiveFullName)
}
Err(_) => ctx
.answer("Oh, please, enter a number!")
.send()
.await
.map(|_| ())?,
}
next(ctx.dialogue)
}
async fn favourite_music(mut ctx: Ctx) -> Res {
match ctx.update.text().unwrap().parse() {
Ok(ok) => {
ctx.dialogue.data.favourite_music = Some(ok);
ctx.answer(format!("Fine. {}", ctx.dialogue.data))
Some(full_name) => {
ctx.answer("What a wonderful name! Your age?")
.send()
.await?;
next(Dialogue::ReceiveAge(ReceiveAgeState {
full_name: full_name.to_owned(),
}))
}
}
}
async fn age(ctx: Ctx<ReceiveAgeState>) -> Res {
match ctx.update.text().unwrap().parse() {
Ok(age) => {
ctx.answer("Good. Now choose your favourite music:")
.reply_markup(FavouriteMusic::markup())
.send()
.await?;
next(Dialogue::ReceiveFavouriteMusic(
ReceiveFavouriteMusicState {
data: ctx.dialogue,
age,
},
))
}
Err(_) => {
ctx.answer("Oh, please, enter a number!").send().await?;
next(Dialogue::ReceiveAge(ctx.dialogue))
}
}
}
async fn favourite_music(ctx: Ctx<ReceiveFavouriteMusicState>) -> Res {
match ctx.update.text().unwrap().parse() {
Ok(favourite_music) => {
ctx.answer(format!(
"Fine. {}",
ExitState {
data: ctx.dialogue.clone(),
favourite_music
}
))
.send()
.await?;
exit()
}
Err(_) => {
ctx.answer("Oh, please, enter from the keyboard!")
.send()
.await?;
next(ctx.dialogue)
next(Dialogue::ReceiveFavouriteMusic(ctx.dialogue))
}
}
}
async fn handle_message(ctx: Ctx) -> Res {
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,
async fn handle_message(ctx: Ctx<Dialogue>) -> Res {
match ctx {
DialogueHandlerCtx {
bot,
update,
dialogue: Dialogue::Start,
} => start(DialogueHandlerCtx::new(bot, update, ())).await,
DialogueHandlerCtx {
bot,
update,
dialogue: Dialogue::ReceiveFullName,
} => full_name(DialogueHandlerCtx::new(bot, update, ())).await,
DialogueHandlerCtx {
bot,
update,
dialogue: Dialogue::ReceiveAge(s),
} => age(DialogueHandlerCtx::new(bot, update, s)).await,
DialogueHandlerCtx {
bot,
update,
dialogue: Dialogue::ReceiveFavouriteMusic(s),
} => favourite_music(DialogueHandlerCtx::new(bot, update, s)).await,
}
}
@ -151,11 +175,14 @@ async fn handle_message(ctx: Ctx) -> Res {
#[tokio::main]
async fn main() {
std::env::set_var("RUST_LOG", "simple_dialogue=trace");
set_var("RUST_LOG", "simple_dialogue=trace");
set_var("RUST_LOG", "teloxide=error");
pretty_env_logger::init();
log::info!("Starting the simple_dialogue bot!");
Dispatcher::new(Bot::new("YourAwesomeToken"))
let bot = Bot::new(var("TELOXIDE_TOKEN").unwrap());
Dispatcher::new(bot)
.message_handler(&DialogueDispatcher::new(|ctx| async move {
handle_message(ctx)
.await

View file

@ -1,14 +0,0 @@
/// A type, encapsulating a dialogue state and arbitrary data.
#[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,7 +1,6 @@
use crate::dispatching::{
dialogue::{
Dialogue, DialogueHandlerCtx, DialogueStage, GetChatId, InMemStorage,
Storage,
DialogueHandlerCtx, DialogueStage, GetChatId, InMemStorage, Storage,
},
CtxHandler, DispatcherHandlerCtx,
};
@ -13,16 +12,14 @@ use std::{future::Future, pin::Pin};
/// an instance of this dispatcher into the [`Dispatcher`]'s methods.
///
/// [`Dispatcher`]: crate::dispatching::Dispatcher
pub struct DialogueDispatcher<'a, State, T, H> {
storage: Box<dyn Storage<State, T> + 'a>,
pub struct DialogueDispatcher<'a, D, H> {
storage: Box<dyn Storage<D> + 'a>,
handler: H,
}
impl<'a, State, T, H> DialogueDispatcher<'a, State, T, H>
impl<'a, D, H> DialogueDispatcher<'a, D, H>
where
Dialogue<State, T>: Default + 'a,
T: Default + 'a,
State: Default + 'a,
D: Default + 'a,
{
/// Creates a dispatcher with the specified `handler` and [`InMemStorage`]
/// (a default storage).
@ -40,7 +37,7 @@ where
#[must_use]
pub fn with_storage<Stg>(handler: H, storage: Stg) -> Self
where
Stg: Storage<State, T> + 'a,
Stg: Storage<D> + 'a,
{
Self {
storage: Box::new(storage),
@ -49,12 +46,12 @@ where
}
}
impl<'a, State, T, H, Upd> CtxHandler<DispatcherHandlerCtx<Upd>, Result<(), ()>>
for DialogueDispatcher<'a, State, T, H>
impl<'a, D, H, Upd> CtxHandler<DispatcherHandlerCtx<Upd>, Result<(), ()>>
for DialogueDispatcher<'a, D, H>
where
H: CtxHandler<DialogueHandlerCtx<Upd, State, T>, DialogueStage<State, T>>,
H: CtxHandler<DialogueHandlerCtx<Upd, D>, DialogueStage<D>>,
Upd: GetChatId,
Dialogue<State, T>: Default,
D: Default,
{
fn handle_ctx<'b>(
&'b self,

View file

@ -1,5 +1,5 @@
use crate::{
dispatching::dialogue::{Dialogue, GetChatId},
dispatching::dialogue::GetChatId,
requests::{
DeleteMessage, EditMessageCaption, EditMessageText, ForwardMessage,
PinChatMessage, SendAnimation, SendAudio, SendContact, SendDocument,
@ -14,28 +14,37 @@ use std::sync::Arc;
/// A context of a [`DialogueDispatcher`]'s message handler.
///
/// [`DialogueDispatcher`]: crate::dispatching::dialogue::DialogueDispatcher
pub struct DialogueHandlerCtx<Upd, State, T> {
pub struct DialogueHandlerCtx<Upd, D> {
pub bot: Arc<Bot>,
pub update: Upd,
pub dialogue: Dialogue<State, T>,
pub dialogue: D,
}
/// Sets a new state.
///
/// Use it like this: `state!(ctx, State::RequestAge)`, where `ctx` is
/// [`DialogueHandlerCtx<Upd, State, T>`] and `State::RequestAge` is of type
/// `State`.
///
/// [`DialogueHandlerCtx<Upd, State, T>`]:
/// crate::dispatching::dialogue::DialogueHandlerCtx
#[macro_export]
macro_rules! state {
($ctx:ident, $state:expr) => {
$ctx.dialogue.state = $state;
};
impl<Upd, D> DialogueHandlerCtx<Upd, D> {
/// Creates a new instance with the provided fields.
pub fn new(bot: Arc<Bot>, update: Upd, dialogue: D) -> Self {
Self {
bot,
update,
dialogue,
}
}
/// Creates a new instance by substituting a dialogue and preserving
/// `self.bot` and `self.update`.
pub fn with_new_dialogue<Nd>(
self,
new_dialogue: Nd,
) -> DialogueHandlerCtx<Upd, Nd> {
DialogueHandlerCtx {
bot: self.bot,
update: self.update,
dialogue: new_dialogue,
}
}
}
impl<Upd, State, T> GetChatId for DialogueHandlerCtx<Upd, State, T>
impl<Upd, D> GetChatId for DialogueHandlerCtx<Upd, D>
where
Upd: GetChatId,
{
@ -44,7 +53,7 @@ where
}
}
impl<State, Data> DialogueHandlerCtx<Message, State, Data> {
impl<D> DialogueHandlerCtx<Message, D> {
pub fn answer<T>(&self, text: T) -> SendMessage
where
T: Into<String>,

View file

@ -1,20 +1,16 @@
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>),
pub enum DialogueStage<D> {
Next(D),
Exit,
}
/// A shortcut for `Ok(DialogueStage::Next(dialogue))`.
pub fn next<E, State, T>(
dialogue: Dialogue<State, T>,
) -> Result<DialogueStage<State, T>, E> {
pub fn next<E, D>(dialogue: D) -> Result<DialogueStage<D>, E> {
Ok(DialogueStage::Next(dialogue))
}
/// A shortcut for `Ok(DialogueStage::Exit)`.
pub fn exit<E, State, T>() -> Result<DialogueStage<State, T>, E> {
pub fn exit<E, D>() -> Result<DialogueStage<D>, E> {
Ok(DialogueStage::Exit)
}

View file

@ -2,22 +2,20 @@
//!
//! There are four main components:
//!
//! 1. Your type `State`, which designates a dialogue state at the current
//! 1. Your type `D`, which designates a dialogue state at the current
//! moment.
//! 2. Your type `T`, which represents dialogue data.
//! 3. [`Dialogue`], which encapsulates the two types, described above.
//! 4. [`Storage`], which encapsulates all the sessions.
//! 5. Your handler, which receives an update and turns your session into the
//! 2. [`Storage`], which encapsulates all the dialogues.
//! 3. Your handler, which receives an update and turns your dialogue into the
//! next state.
//! 6. [`DialogueDispatcher`], which encapsulates your handler, [`Storage`], and
//! implements [`CtxHandler`].
//! 4. [`DialogueDispatcher`], which encapsulates your handler, [`Storage`],
//! and implements [`CtxHandler`].
//!
//! You supply [`DialogueDispatcher`] into [`Dispatcher`]. Every time
//! [`Dispatcher`] calls `DialogueDispatcher::handle_ctx(...)`, the following
//! steps are executed:
//!
//! 1. If a storage doesn't contain a dialogue from this chat, supply
//! `Dialogue::default()` into you handler, otherwise, supply the saved session
//! `D::default()` into you handler, otherwise, supply the saved session
//! from this chat.
//! 3. If a handler has returned [`DialogueStage::Exit`], remove the session
//! from the storage, otherwise ([`DialogueStage::Next`]) force the storage to
@ -37,14 +35,12 @@
#![allow(clippy::module_inception)]
#![allow(clippy::type_complexity)]
mod dialogue;
mod dialogue_dispatcher;
mod dialogue_handler_ctx;
mod dialogue_stage;
mod get_chat_id;
mod storage;
pub use dialogue::Dialogue;
pub use dialogue_dispatcher::DialogueDispatcher;
pub use dialogue_handler_ctx::DialogueHandlerCtx;
pub use dialogue_stage::{exit, next, DialogueStage};

View file

@ -1,7 +1,6 @@
use async_trait::async_trait;
use super::Storage;
use crate::dispatching::dialogue::Dialogue;
use std::collections::HashMap;
use tokio::sync::Mutex;
@ -13,25 +12,18 @@ use tokio::sync::Mutex;
/// 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>>>,
pub struct InMemStorage<D> {
map: Mutex<HashMap<i64, D>>,
}
#[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>> {
impl<D> Storage<D> for InMemStorage<D> {
async fn remove_dialogue(&self, chat_id: i64) -> Option<D> {
self.map.lock().await.remove(&chat_id)
}
async fn update_dialogue(
&self,
chat_id: i64,
dialogue: Dialogue<State, T>,
) -> Option<Dialogue<State, T>> {
async fn update_dialogue(&self, chat_id: i64, dialogue: D) -> Option<D> {
self.map.lock().await.insert(chat_id, dialogue)
}
}

View file

@ -1,6 +1,5 @@
mod in_mem_storage;
use crate::dispatching::dialogue::Dialogue;
use async_trait::async_trait;
pub use in_mem_storage::InMemStorage;
@ -14,21 +13,16 @@ pub use in_mem_storage::InMemStorage;
/// [`InMemStorage`]: crate::dispatching::dialogue::InMemStorage
#[async_trait(?Send)]
#[async_trait]
pub trait Storage<State, T> {
pub trait Storage<D> {
/// 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>>;
async fn remove_dialogue(&self, chat_id: i64) -> Option<D>;
/// 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>>;
async fn update_dialogue(&self, chat_id: i64, dialogue: D) -> Option<D>;
}

View file

@ -155,13 +155,30 @@ pub fn polling(
let updates = match req.send().await {
Err(err) => vec![Err(err)],
Ok(updates) => {
// Set offset to the last update's id + 1
if let Some(upd) = updates.last() {
let id: i32 = match upd {
Ok(ok) => ok.id,
Err((value, _)) => value["update_id"]
.as_i64()
.expect(
"The 'update_id' field must always exist in \
Update",
)
.try_into()
.expect("update_id must be i32"),
};
offset = id + 1;
}
let updates = updates
.into_iter()
.filter(|update| match update {
Err(error) => {
log::error!("Cannot parse an update: {:?}! \
Err((value, error)) => {
log::error!("Cannot parse an update.\nError: {:?}\nValue: {}\n\
This is a bug in teloxide, please open an issue here: \
https://github.com/teloxide/teloxide/issues.", error);
https://github.com/teloxide/teloxide/issues.", error, value);
false
}
Ok(_) => true,
@ -171,9 +188,6 @@ pub fn polling(
})
.collect::<Vec<Update>>();
if let Some(upd) = updates.last() {
offset = upd.id + 1;
}
updates.into_iter().map(Ok).collect::<Vec<_>>()
}
};

View file

@ -9,7 +9,6 @@ pub use crate::{
Dispatcher, DispatcherHandlerCtx,
},
requests::{Request, ResponseResult},
state,
types::Message,
Bot, RequestError,
};

View file

@ -32,12 +32,14 @@ pub struct GetUpdates {
#[async_trait::async_trait]
impl Request for GetUpdates {
type Output = Vec<serde_json::Result<Update>>;
type Output = Vec<Result<Update, (Value, serde_json::Error)>>;
/// Deserialize to `Vec<serde_json::Result<Update>>` instead of
/// `Vec<Update>`, because we want to parse the rest of updates even if our
/// library hasn't parsed one.
async fn send(&self) -> ResponseResult<Vec<serde_json::Result<Update>>> {
async fn send(
&self,
) -> ResponseResult<Vec<Result<Update, (Value, serde_json::Error)>>> {
let value: Value = net::request_json(
self.bot.client(),
self.bot.token(),
@ -49,7 +51,10 @@ impl Request for GetUpdates {
match value {
Value::Array(array) => Ok(array
.into_iter()
.map(|value| serde_json::from_str(&value.to_string()))
.map(|value| {
serde_json::from_str(&value.to_string())
.map_err(|error| (value, error))
})
.collect()),
_ => Err(RequestError::InvalidJson(
serde_json::from_value::<Vec<Update>>(value)