mirror of
https://github.com/teloxide/teloxide.git
synced 2024-12-22 14:35:36 +01:00
Merge remote-tracking branch 'origin/rework-dispatching' into rework-dispatching
This commit is contained in:
commit
6de0c24ed2
13 changed files with 202 additions and 179 deletions
|
@ -1,12 +1,21 @@
|
||||||
use teloxide::prelude::*;
|
use teloxide::prelude::*;
|
||||||
|
|
||||||
|
use std::env::{set_var, var};
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn 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();
|
pretty_env_logger::init();
|
||||||
log::info!("Starting the ping-pong bot!");
|
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 {
|
.message_handler(&|ctx: DispatcherHandlerCtx<Message>| async move {
|
||||||
ctx.answer("pong").send().await?;
|
ctx.answer("pong").send().await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
@ -10,9 +10,8 @@ edition = "2018"
|
||||||
pretty_env_logger = "0.3.1"
|
pretty_env_logger = "0.3.1"
|
||||||
log = "0.4.8"
|
log = "0.4.8"
|
||||||
tokio = "0.2.9"
|
tokio = "0.2.9"
|
||||||
strum = "0.17.1"
|
|
||||||
smart-default = "0.6.0"
|
smart-default = "0.6.0"
|
||||||
strum_macros = "0.17.1"
|
parse-display = "0.1.1"
|
||||||
teloxide = { path = "../../" }
|
teloxide = { path = "../../" }
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
|
|
|
@ -1,19 +1,22 @@
|
||||||
#[macro_use]
|
#![allow(clippy::trivial_regex)]
|
||||||
extern crate strum_macros;
|
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate smart_default;
|
extern crate smart_default;
|
||||||
|
|
||||||
use std::fmt::{self, Display, Formatter};
|
use std::env::{set_var, var};
|
||||||
|
|
||||||
use teloxide::{
|
use teloxide::{
|
||||||
prelude::*,
|
prelude::*,
|
||||||
types::{KeyboardButton, ReplyKeyboardMarkup},
|
types::{KeyboardButton, ReplyKeyboardMarkup},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use parse_display::{Display, FromStr};
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// [Favourite music kinds]
|
// [Favourite music kinds]
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
#[derive(Copy, Clone, Display, EnumString)]
|
#[derive(Copy, Clone, Display, FromStr)]
|
||||||
enum FavouriteMusic {
|
enum FavouriteMusic {
|
||||||
Rock,
|
Rock,
|
||||||
Metal,
|
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(Clone)]
|
||||||
#[derive(Default)]
|
struct ReceiveAgeState {
|
||||||
struct UserInfo {
|
full_name: String,
|
||||||
full_name: Option<String>,
|
|
||||||
age: Option<u8>,
|
|
||||||
favourite_music: Option<FavouriteMusic>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for UserInfo {
|
#[derive(Clone)]
|
||||||
fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
|
struct ReceiveFavouriteMusicState {
|
||||||
write!(
|
data: ReceiveAgeState,
|
||||||
f,
|
age: u8,
|
||||||
"Your full name: {}, your age: {}, your favourite music: {}",
|
|
||||||
self.full_name.as_ref().unwrap(),
|
|
||||||
self.age.unwrap(),
|
|
||||||
self.favourite_music.unwrap()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
#[derive(Display)]
|
||||||
// [States of a dialogue]
|
#[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)]
|
#[derive(SmartDefault)]
|
||||||
enum State {
|
enum Dialogue {
|
||||||
#[default]
|
#[default]
|
||||||
Start,
|
Start,
|
||||||
FullName,
|
ReceiveFullName,
|
||||||
Age,
|
ReceiveAge(ReceiveAgeState),
|
||||||
FavouriteMusic,
|
ReceiveFavouriteMusic(ReceiveFavouriteMusicState),
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// [Control a dialogue]
|
// [Control a dialogue]
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
type Ctx = DialogueHandlerCtx<Message, State, UserInfo>;
|
type Ctx<State> = DialogueHandlerCtx<Message, State>;
|
||||||
type Res = Result<DialogueStage<State, UserInfo>, RequestError>;
|
type Res = Result<DialogueStage<Dialogue>, RequestError>;
|
||||||
|
|
||||||
async fn send_favourite_music_types(ctx: &Ctx) -> Result<(), RequestError> {
|
async fn start(ctx: Ctx<()>) -> Res {
|
||||||
ctx.answer("Good. Now choose your favourite music:")
|
|
||||||
.reply_markup(FavouriteMusic::markup())
|
|
||||||
.send()
|
|
||||||
.await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn start(mut ctx: Ctx) -> Res {
|
|
||||||
ctx.answer("Let's start! First, what's your full name?")
|
ctx.answer("Let's start! First, what's your full name?")
|
||||||
.send()
|
.send()
|
||||||
.await?;
|
.await?;
|
||||||
state!(ctx, State::FullName);
|
next(Dialogue::ReceiveFullName)
|
||||||
next(ctx.dialogue)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn full_name(mut ctx: Ctx) -> Res {
|
async fn full_name(ctx: Ctx<()>) -> Res {
|
||||||
ctx.answer("What a wonderful name! Your age?")
|
match ctx.update.text() {
|
||||||
.send()
|
None => {
|
||||||
.await?;
|
ctx.answer("Please, send me a text message!").send().await?;
|
||||||
ctx.dialogue.data.full_name = Some(ctx.update.text().unwrap().to_owned());
|
next(Dialogue::ReceiveFullName)
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
Err(_) => ctx
|
Some(full_name) => {
|
||||||
.answer("Oh, please, enter a number!")
|
ctx.answer("What a wonderful name! Your age?")
|
||||||
.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))
|
|
||||||
.send()
|
.send()
|
||||||
.await?;
|
.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()
|
exit()
|
||||||
}
|
}
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
ctx.answer("Oh, please, enter from the keyboard!")
|
ctx.answer("Oh, please, enter from the keyboard!")
|
||||||
.send()
|
.send()
|
||||||
.await?;
|
.await?;
|
||||||
next(ctx.dialogue)
|
next(Dialogue::ReceiveFavouriteMusic(ctx.dialogue))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_message(ctx: Ctx) -> Res {
|
async fn handle_message(ctx: Ctx<Dialogue>) -> Res {
|
||||||
match ctx.dialogue.state {
|
match ctx {
|
||||||
State::Start => start(ctx).await,
|
DialogueHandlerCtx {
|
||||||
State::FullName => full_name(ctx).await,
|
bot,
|
||||||
State::Age => age(ctx).await,
|
update,
|
||||||
State::FavouriteMusic => favourite_music(ctx).await,
|
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]
|
#[tokio::main]
|
||||||
async fn 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();
|
pretty_env_logger::init();
|
||||||
log::info!("Starting the simple_dialogue bot!");
|
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 {
|
.message_handler(&DialogueDispatcher::new(|ctx| async move {
|
||||||
handle_message(ctx)
|
handle_message(ctx)
|
||||||
.await
|
.await
|
||||||
|
|
|
@ -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 }
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,7 +1,6 @@
|
||||||
use crate::dispatching::{
|
use crate::dispatching::{
|
||||||
dialogue::{
|
dialogue::{
|
||||||
Dialogue, DialogueHandlerCtx, DialogueStage, GetChatId, InMemStorage,
|
DialogueHandlerCtx, DialogueStage, GetChatId, InMemStorage, Storage,
|
||||||
Storage,
|
|
||||||
},
|
},
|
||||||
CtxHandler, DispatcherHandlerCtx,
|
CtxHandler, DispatcherHandlerCtx,
|
||||||
};
|
};
|
||||||
|
@ -13,16 +12,14 @@ use std::{future::Future, pin::Pin};
|
||||||
/// 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 DialogueDispatcher<'a, State, T, H> {
|
pub struct DialogueDispatcher<'a, D, H> {
|
||||||
storage: Box<dyn Storage<State, T> + 'a>,
|
storage: Box<dyn Storage<D> + 'a>,
|
||||||
handler: H,
|
handler: H,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, State, T, H> DialogueDispatcher<'a, State, T, H>
|
impl<'a, D, H> DialogueDispatcher<'a, D, H>
|
||||||
where
|
where
|
||||||
Dialogue<State, T>: Default + 'a,
|
D: 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).
|
||||||
|
@ -40,7 +37,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<State, T> + 'a,
|
Stg: Storage<D> + 'a,
|
||||||
{
|
{
|
||||||
Self {
|
Self {
|
||||||
storage: Box::new(storage),
|
storage: Box::new(storage),
|
||||||
|
@ -49,12 +46,12 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, State, T, H, Upd> CtxHandler<DispatcherHandlerCtx<Upd>, Result<(), ()>>
|
impl<'a, D, H, Upd> CtxHandler<DispatcherHandlerCtx<Upd>, Result<(), ()>>
|
||||||
for DialogueDispatcher<'a, State, T, H>
|
for DialogueDispatcher<'a, D, H>
|
||||||
where
|
where
|
||||||
H: CtxHandler<DialogueHandlerCtx<Upd, State, T>, DialogueStage<State, T>>,
|
H: CtxHandler<DialogueHandlerCtx<Upd, D>, DialogueStage<D>>,
|
||||||
Upd: GetChatId,
|
Upd: GetChatId,
|
||||||
Dialogue<State, T>: Default,
|
D: Default,
|
||||||
{
|
{
|
||||||
fn handle_ctx<'b>(
|
fn handle_ctx<'b>(
|
||||||
&'b self,
|
&'b self,
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
dispatching::dialogue::{Dialogue, GetChatId},
|
dispatching::dialogue::GetChatId,
|
||||||
requests::{
|
requests::{
|
||||||
DeleteMessage, EditMessageCaption, EditMessageText, ForwardMessage,
|
DeleteMessage, EditMessageCaption, EditMessageText, ForwardMessage,
|
||||||
PinChatMessage, SendAnimation, SendAudio, SendContact, SendDocument,
|
PinChatMessage, SendAnimation, SendAudio, SendContact, SendDocument,
|
||||||
|
@ -14,28 +14,37 @@ use std::sync::Arc;
|
||||||
/// A context of a [`DialogueDispatcher`]'s message handler.
|
/// A context of a [`DialogueDispatcher`]'s message handler.
|
||||||
///
|
///
|
||||||
/// [`DialogueDispatcher`]: crate::dispatching::dialogue::DialogueDispatcher
|
/// [`DialogueDispatcher`]: crate::dispatching::dialogue::DialogueDispatcher
|
||||||
pub struct DialogueHandlerCtx<Upd, State, T> {
|
pub struct DialogueHandlerCtx<Upd, D> {
|
||||||
pub bot: Arc<Bot>,
|
pub bot: Arc<Bot>,
|
||||||
pub update: Upd,
|
pub update: Upd,
|
||||||
pub dialogue: Dialogue<State, T>,
|
pub dialogue: D,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets a new state.
|
impl<Upd, D> DialogueHandlerCtx<Upd, D> {
|
||||||
///
|
/// Creates a new instance with the provided fields.
|
||||||
/// Use it like this: `state!(ctx, State::RequestAge)`, where `ctx` is
|
pub fn new(bot: Arc<Bot>, update: Upd, dialogue: D) -> Self {
|
||||||
/// [`DialogueHandlerCtx<Upd, State, T>`] and `State::RequestAge` is of type
|
Self {
|
||||||
/// `State`.
|
bot,
|
||||||
///
|
update,
|
||||||
/// [`DialogueHandlerCtx<Upd, State, T>`]:
|
dialogue,
|
||||||
/// crate::dispatching::dialogue::DialogueHandlerCtx
|
}
|
||||||
#[macro_export]
|
}
|
||||||
macro_rules! state {
|
|
||||||
($ctx:ident, $state:expr) => {
|
/// Creates a new instance by substituting a dialogue and preserving
|
||||||
$ctx.dialogue.state = $state;
|
/// `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
|
where
|
||||||
Upd: GetChatId,
|
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
|
pub fn answer<T>(&self, text: T) -> SendMessage
|
||||||
where
|
where
|
||||||
T: Into<String>,
|
T: Into<String>,
|
||||||
|
|
|
@ -1,20 +1,16 @@
|
||||||
use crate::dispatching::dialogue::Dialogue;
|
|
||||||
|
|
||||||
/// Continue or terminate a dialogue.
|
/// Continue or terminate a dialogue.
|
||||||
#[derive(Debug, Copy, Clone, Eq, Hash, PartialEq)]
|
#[derive(Debug, Copy, Clone, Eq, Hash, PartialEq)]
|
||||||
pub enum DialogueStage<State, T> {
|
pub enum DialogueStage<D> {
|
||||||
Next(Dialogue<State, T>),
|
Next(D),
|
||||||
Exit,
|
Exit,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A shortcut for `Ok(DialogueStage::Next(dialogue))`.
|
/// A shortcut for `Ok(DialogueStage::Next(dialogue))`.
|
||||||
pub fn next<E, State, T>(
|
pub fn next<E, D>(dialogue: D) -> Result<DialogueStage<D>, E> {
|
||||||
dialogue: Dialogue<State, T>,
|
|
||||||
) -> Result<DialogueStage<State, T>, E> {
|
|
||||||
Ok(DialogueStage::Next(dialogue))
|
Ok(DialogueStage::Next(dialogue))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A shortcut for `Ok(DialogueStage::Exit)`.
|
/// 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)
|
Ok(DialogueStage::Exit)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,22 +2,20 @@
|
||||||
//!
|
//!
|
||||||
//! There are four main components:
|
//! 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.
|
//! moment.
|
||||||
//! 2. Your type `T`, which represents dialogue data.
|
//! 2. [`Storage`], which encapsulates all the dialogues.
|
||||||
//! 3. [`Dialogue`], which encapsulates the two types, described above.
|
//! 3. Your handler, which receives an update and turns your dialogue into the
|
||||||
//! 4. [`Storage`], which encapsulates all the sessions.
|
|
||||||
//! 5. Your handler, which receives an update and turns your session into the
|
|
||||||
//! next state.
|
//! next state.
|
||||||
//! 6. [`DialogueDispatcher`], which encapsulates your handler, [`Storage`], and
|
//! 4. [`DialogueDispatcher`], which encapsulates your handler, [`Storage`],
|
||||||
//! implements [`CtxHandler`].
|
//! and implements [`CtxHandler`].
|
||||||
//!
|
//!
|
||||||
//! You supply [`DialogueDispatcher`] into [`Dispatcher`]. Every time
|
//! You supply [`DialogueDispatcher`] into [`Dispatcher`]. Every time
|
||||||
//! [`Dispatcher`] calls `DialogueDispatcher::handle_ctx(...)`, the following
|
//! [`Dispatcher`] calls `DialogueDispatcher::handle_ctx(...)`, the following
|
||||||
//! steps are executed:
|
//! steps are executed:
|
||||||
//!
|
//!
|
||||||
//! 1. If a storage doesn't contain a dialogue from this chat, supply
|
//! 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.
|
//! from this chat.
|
||||||
//! 3. If a handler has returned [`DialogueStage::Exit`], remove the session
|
//! 3. If a handler has returned [`DialogueStage::Exit`], remove the session
|
||||||
//! from the storage, otherwise ([`DialogueStage::Next`]) force the storage to
|
//! from the storage, otherwise ([`DialogueStage::Next`]) force the storage to
|
||||||
|
@ -37,14 +35,12 @@
|
||||||
#![allow(clippy::module_inception)]
|
#![allow(clippy::module_inception)]
|
||||||
#![allow(clippy::type_complexity)]
|
#![allow(clippy::type_complexity)]
|
||||||
|
|
||||||
mod dialogue;
|
|
||||||
mod dialogue_dispatcher;
|
mod dialogue_dispatcher;
|
||||||
mod dialogue_handler_ctx;
|
mod dialogue_handler_ctx;
|
||||||
mod dialogue_stage;
|
mod dialogue_stage;
|
||||||
mod get_chat_id;
|
mod get_chat_id;
|
||||||
mod storage;
|
mod storage;
|
||||||
|
|
||||||
pub use dialogue::Dialogue;
|
|
||||||
pub use dialogue_dispatcher::DialogueDispatcher;
|
pub use dialogue_dispatcher::DialogueDispatcher;
|
||||||
pub use dialogue_handler_ctx::DialogueHandlerCtx;
|
pub use dialogue_handler_ctx::DialogueHandlerCtx;
|
||||||
pub use dialogue_stage::{exit, next, DialogueStage};
|
pub use dialogue_stage::{exit, next, DialogueStage};
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
|
||||||
use super::Storage;
|
use super::Storage;
|
||||||
use crate::dispatching::dialogue::Dialogue;
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
|
@ -13,25 +12,18 @@ use tokio::sync::Mutex;
|
||||||
/// store them somewhere on a drive, you need to implement a storage
|
/// store them somewhere on a drive, you need to implement a storage
|
||||||
/// communicating with a DB.
|
/// communicating with a DB.
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
pub struct InMemStorage<State, T> {
|
pub struct InMemStorage<D> {
|
||||||
map: Mutex<HashMap<i64, Dialogue<State, T>>>,
|
map: Mutex<HashMap<i64, D>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait(?Send)]
|
#[async_trait(?Send)]
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl<State, T> Storage<State, T> for InMemStorage<State, T> {
|
impl<D> Storage<D> for InMemStorage<D> {
|
||||||
async fn remove_dialogue(
|
async fn remove_dialogue(&self, chat_id: i64) -> Option<D> {
|
||||||
&self,
|
|
||||||
chat_id: i64,
|
|
||||||
) -> Option<Dialogue<State, T>> {
|
|
||||||
self.map.lock().await.remove(&chat_id)
|
self.map.lock().await.remove(&chat_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn update_dialogue(
|
async fn update_dialogue(&self, chat_id: i64, dialogue: D) -> Option<D> {
|
||||||
&self,
|
|
||||||
chat_id: i64,
|
|
||||||
dialogue: Dialogue<State, T>,
|
|
||||||
) -> Option<Dialogue<State, T>> {
|
|
||||||
self.map.lock().await.insert(chat_id, dialogue)
|
self.map.lock().await.insert(chat_id, dialogue)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
mod in_mem_storage;
|
mod in_mem_storage;
|
||||||
|
|
||||||
use crate::dispatching::dialogue::Dialogue;
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
pub use in_mem_storage::InMemStorage;
|
pub use in_mem_storage::InMemStorage;
|
||||||
|
|
||||||
|
@ -14,21 +13,16 @@ pub use in_mem_storage::InMemStorage;
|
||||||
/// [`InMemStorage`]: crate::dispatching::dialogue::InMemStorage
|
/// [`InMemStorage`]: crate::dispatching::dialogue::InMemStorage
|
||||||
#[async_trait(?Send)]
|
#[async_trait(?Send)]
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait Storage<State, T> {
|
pub trait Storage<D> {
|
||||||
/// Removes a dialogue with the specified `chat_id`.
|
/// Removes a dialogue with the specified `chat_id`.
|
||||||
///
|
///
|
||||||
/// Returns `None` if there wasn't such a dialogue, `Some(dialogue)` if a
|
/// Returns `None` if there wasn't such a dialogue, `Some(dialogue)` if a
|
||||||
/// `dialogue` was deleted.
|
/// `dialogue` was deleted.
|
||||||
async fn remove_dialogue(&self, chat_id: i64)
|
async fn remove_dialogue(&self, chat_id: i64) -> Option<D>;
|
||||||
-> Option<Dialogue<State, T>>;
|
|
||||||
|
|
||||||
/// Updates a dialogue with the specified `chat_id`.
|
/// Updates a dialogue with the specified `chat_id`.
|
||||||
///
|
///
|
||||||
/// Returns `None` if there wasn't such a dialogue, `Some(dialogue)` if a
|
/// Returns `None` if there wasn't such a dialogue, `Some(dialogue)` if a
|
||||||
/// `dialogue` was updated.
|
/// `dialogue` was updated.
|
||||||
async fn update_dialogue(
|
async fn update_dialogue(&self, chat_id: i64, dialogue: D) -> Option<D>;
|
||||||
&self,
|
|
||||||
chat_id: i64,
|
|
||||||
dialogue: Dialogue<State, T>,
|
|
||||||
) -> Option<Dialogue<State, T>>;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -155,13 +155,30 @@ pub fn polling(
|
||||||
let updates = match req.send().await {
|
let updates = match req.send().await {
|
||||||
Err(err) => vec![Err(err)],
|
Err(err) => vec![Err(err)],
|
||||||
Ok(updates) => {
|
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
|
let updates = updates
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|update| match update {
|
.filter(|update| match update {
|
||||||
Err(error) => {
|
Err((value, error)) => {
|
||||||
log::error!("Cannot parse an update: {:?}! \
|
log::error!("Cannot parse an update.\nError: {:?}\nValue: {}\n\
|
||||||
This is a bug in teloxide, please open an issue here: \
|
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
|
false
|
||||||
}
|
}
|
||||||
Ok(_) => true,
|
Ok(_) => true,
|
||||||
|
@ -171,9 +188,6 @@ pub fn polling(
|
||||||
})
|
})
|
||||||
.collect::<Vec<Update>>();
|
.collect::<Vec<Update>>();
|
||||||
|
|
||||||
if let Some(upd) = updates.last() {
|
|
||||||
offset = upd.id + 1;
|
|
||||||
}
|
|
||||||
updates.into_iter().map(Ok).collect::<Vec<_>>()
|
updates.into_iter().map(Ok).collect::<Vec<_>>()
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -9,7 +9,6 @@ pub use crate::{
|
||||||
Dispatcher, DispatcherHandlerCtx,
|
Dispatcher, DispatcherHandlerCtx,
|
||||||
},
|
},
|
||||||
requests::{Request, ResponseResult},
|
requests::{Request, ResponseResult},
|
||||||
state,
|
|
||||||
types::Message,
|
types::Message,
|
||||||
Bot, RequestError,
|
Bot, RequestError,
|
||||||
};
|
};
|
||||||
|
|
|
@ -32,12 +32,14 @@ pub struct GetUpdates {
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
impl Request for GetUpdates {
|
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
|
/// Deserialize to `Vec<serde_json::Result<Update>>` instead of
|
||||||
/// `Vec<Update>`, because we want to parse the rest of updates even if our
|
/// `Vec<Update>`, because we want to parse the rest of updates even if our
|
||||||
/// library hasn't parsed one.
|
/// 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(
|
let value: Value = net::request_json(
|
||||||
self.bot.client(),
|
self.bot.client(),
|
||||||
self.bot.token(),
|
self.bot.token(),
|
||||||
|
@ -49,7 +51,10 @@ impl Request for GetUpdates {
|
||||||
match value {
|
match value {
|
||||||
Value::Array(array) => Ok(array
|
Value::Array(array) => Ok(array
|
||||||
.into_iter()
|
.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()),
|
.collect()),
|
||||||
_ => Err(RequestError::InvalidJson(
|
_ => Err(RequestError::InvalidJson(
|
||||||
serde_json::from_value::<Vec<Update>>(value)
|
serde_json::from_value::<Vec<Update>>(value)
|
||||||
|
|
Loading…
Reference in a new issue