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 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(())

View file

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

View file

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

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::{ 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,

View file

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

View file

@ -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)
} }

View file

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

View file

@ -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)
} }
} }

View file

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

View file

@ -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<_>>()
} }
}; };

View file

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

View file

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