Merge branch 'dev' of https://github.com/teloxide/teloxide into useful_functions

This commit is contained in:
p0lunin 2020-01-19 15:50:25 +02:00
commit 50c29227ce
16 changed files with 439 additions and 609 deletions

4
.gitignore vendored
View file

@ -1,4 +1,6 @@
/target
**/*.rs.bk
Cargo.lock
.idea/
.idea/
.vscode/
examples/target

View file

@ -0,0 +1,18 @@
use serde::{Deserialize, Serialize};
use crate::types::{CallbackQuery, Message};
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct ChatUpdate {
pub id: i32,
pub kind: ChatUpdateKind,
}
#[allow(clippy::large_enum_variant)]
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum ChatUpdateKind {
Message(Message),
EditedMessage(Message),
CallbackQuery(CallbackQuery),
}

View file

@ -0,0 +1,106 @@
use super::{
super::DispatchResult,
storage::{InMemStorage, Storage},
};
use crate::{
dispatching::{
chat::{ChatUpdate, ChatUpdateKind},
Handler, SessionState,
},
types::{Update, UpdateKind},
};
/// A dispatcher that dispatches updates from chats.
pub struct Dispatcher<'a, Session, H> {
storage: Box<dyn Storage<Session> + 'a>,
handler: H,
}
impl<'a, Session, H> Dispatcher<'a, Session, H>
where
Session: Default + 'a,
H: Handler<Session, ChatUpdate>,
{
/// Creates a dispatcher with the specified `handler` and [`InMemStorage`]
/// (a default storage).
///
/// [`InMemStorage`]: crate::dispatching::private::InMemStorage
pub fn new(handler: H) -> Self {
Self {
storage: Box::new(InMemStorage::default()),
handler,
}
}
/// Creates a dispatcher with the specified `handler` and `storage`.
pub fn with_storage<Stg>(handler: H, storage: Stg) -> Self
where
Stg: Storage<Session> + 'a,
{
Self {
storage: Box::new(storage),
handler,
}
}
/// Dispatches a single `update`.
///
/// ## Returns
/// Returns [`DispatchResult::Handled`] if `update` was supplied to a
/// handler, and [`DispatchResult::Unhandled`] if it was an update not
/// from a chat.
///
/// [`DispatchResult::Handled`]: crate::dispatching::DispatchResult::Handled
/// [`DispatchResult::Unhandled`]:
/// crate::dispatching::DispatchResult::Unhandled
pub async fn dispatch(&mut self, update: Update) -> DispatchResult {
let chat_update = match update.kind {
UpdateKind::Message(msg) => ChatUpdate {
id: update.id,
kind: ChatUpdateKind::Message(msg),
},
UpdateKind::EditedMessage(msg) => ChatUpdate {
id: update.id,
kind: ChatUpdateKind::EditedMessage(msg),
},
UpdateKind::CallbackQuery(query) => ChatUpdate {
id: update.id,
kind: ChatUpdateKind::CallbackQuery(query),
},
_ => return DispatchResult::Unhandled,
};
let chat_id = match &chat_update.kind {
ChatUpdateKind::Message(msg) => msg.chat.id,
ChatUpdateKind::EditedMessage(msg) => msg.chat.id,
ChatUpdateKind::CallbackQuery(query) => match &query.message {
None => return DispatchResult::Unhandled,
Some(msg) => msg.chat.id,
},
};
let session = self
.storage
.remove_session(chat_id)
.await
.unwrap_or_default();
if let SessionState::Continue(session) =
self.handler.handle(session, chat_update).await
{
if self
.storage
.update_session(chat_id, session)
.await
.is_some()
{
panic!(
"We previously storage.remove_session() so \
storage.update_session() must return None"
);
}
}
DispatchResult::Handled
}
}

View file

@ -0,0 +1,41 @@
//! Dispatching updates from chats.
//!
//! There are four main components:
//!
//! 1. Your session type `Session`, which designates a dialogue state at the
//! current moment.
//! 2. [`Storage`] that encapsulates all the sessions.
//! 3. Your handler of type `H: async Fn(Session, Update) ->
//! HandleResult<Session>` that receives an update and turns your session into
//! the next state.
//! 4. [`Dispatcher`], which encapsulates your handler and [`Storage`], and has
//! the [`dispatch(Update) -> DispatchResult`] function.
//!
//! Every time you call [`.dispatch(update)`] on your dispatcher, the following
//! steps are executed:
//!
//! 1. If a supplied update is not from a chat, return
//! [`DispatchResult::Unhandled`].
//! 2. If a storage doesn't contain a session from this chat, supply
//! `Session::default()` into you handler, otherwise, supply the previous
//! session.
//! 3. If a handler has returned [`SessionState::Terminate`], remove the
//! session from a storage, otherwise force the storage to update the session.
//!
//! [`Storage`]: crate::dispatching::private::Storage
//! [`Dispatcher`]: crate::dispatching::private::Dispatcher
//! [`dispatch(Update) -> DispatchResult`]:
//! crate::dispatching::private::Dispatcher::dispatch
//! [`.dispatch(update)`]: crate::dispatching::private::Dispatcher::dispatch
//! [`DispatchResult::Unhandled`]: crate::dispatching::DispatchResult::Unhandled
//! [`SessionState::Terminate`]: crate::dispatching::SessionState::Terminate
// TODO: examples
mod chat_update;
mod dispatcher;
mod storage;
pub use chat_update::*;
pub use dispatcher::*;
pub use storage::*;

View file

@ -0,0 +1,32 @@
use async_trait::async_trait;
use super::Storage;
use std::collections::HashMap;
/// 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(Clone, Debug, Eq, PartialEq, Default)]
pub struct InMemStorage<Session> {
map: HashMap<i64, Session>,
}
#[async_trait(?Send)]
#[async_trait]
impl<Session> Storage<Session> for InMemStorage<Session> {
async fn remove_session(&mut self, chat_id: i64) -> Option<Session> {
self.map.remove(&chat_id)
}
async fn update_session(
&mut self,
chat_id: i64,
state: Session,
) -> Option<Session> {
self.map.insert(chat_id, state)
}
}

View file

@ -0,0 +1,32 @@
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::private::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(&mut 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(
&mut self,
chat_id: i64,
session: Session,
) -> Option<Session>;
}

View file

@ -1,348 +0,0 @@
//! A dispatcher based on filters.
use futures::StreamExt;
use crate::{
dispatching::{filters::Filter, ErrorHandler, Handler, Updater},
types::{
CallbackQuery, ChosenInlineResult, InlineQuery, Message, Update,
UpdateKind,
},
};
use either::Either;
type FilterWithHandler<'a, T, E> =
(Box<dyn Filter<T> + 'a>, Box<dyn Handler<T, E> + 'a>);
type FiltersWithHandlers<'a, T, E> = Vec<FilterWithHandler<'a, T, E>>;
/// A dispatcher based on filters.
///
/// It consists of:
/// 1. [`ErrorHandler`] than handles errors both from [`Updater`] and
/// [`Handler`].
/// 2. Filters and handlers.
///
/// First you call [`FilterDispatcher::new(eh)`] to create this dispatcher. `eh`
/// stands for **E**rror **H**andler, you can simply provide a closure that
/// takes [`Either<UpdaterE, HandlerE>`]:
///
/// ```
/// use either::Either;
/// use std::convert::Infallible;
/// use teloxide::{dispatching::FilterDispatcher, RequestError};
///
/// let _ = FilterDispatcher::new(|err: Either<RequestError, Infallible>| {
/// async {
/// dbg!(err);
/// }
/// });
/// ```
///
/// Or you can do it even simpler by providing the built-in error handler
/// [`Print`]:
///
/// ```
/// use std::convert::Infallible;
/// use teloxide::{
/// dispatching::{error_handlers::Print, FilterDispatcher},
/// RequestError,
/// };
///
/// let _ = FilterDispatcher::<'_, Infallible, _>::new::<RequestError>(Print);
/// ```
///
/// And then you register filters and handlers using the methods defined below,
/// and then you call [`.dispatch(updater)`]. Filters and handlers are executed
/// in order of registering. The following flowchart represents how this
/// dispatcher acts:
///
/// <div align="center">
/// <img src="https://raw.githubusercontent.com/teloxide/teloxide/dev/media/FILTER_DP_FLOWCHART.png" />
/// </div>
///
/// ## Examples
///
/// The simplest example:
/// ```no_run
/// # async fn run() {
/// use std::convert::Infallible;
///
/// use teloxide::{
/// dispatching::{updaters::polling_default, FilterDispatcher},
/// types::Message,
/// Bot,
/// };
///
/// async fn handle_edited_message(mes: Message) -> Result<(), Infallible> {
/// println!("Edited message: {:?}", mes);
/// Ok(())
/// }
///
/// let bot = Bot::new("TOKEN");
///
/// // Create a dispatcher which handlers can't fail with the
/// // error handler that just ignores all errors (that can't ever happen).
/// let mut dp = FilterDispatcher::<Infallible, _>::new(|_| async {})
/// // Add a handler, which handles all messages sent to the bot.
/// .message_handler(true, |mes: Message| {
/// async move {
/// println!("New message: {:?}", mes);
/// Ok(())
/// }
/// })
/// // Add a handler, which handles all messages edited in a chat
/// // with the bot.
/// .edited_message_handler(true, handle_edited_message);
///
/// // Start dispatching updates using long polling.
/// dp.dispatch(polling_default(&bot)).await;
/// # }
/// ```
///
/// [`std::fmt::Debug`]: std::fmt::Debug
/// [`.dispatch(updater)`]: FilterDispatcher::dispatch
/// [`ErrorHandler`]: crate::dispatching::error_handlers::ErrorHandler
/// [`Updater`]: crate::dispatching::updaters::Updater
/// [`Handler`]: crate::dispatching::Handler
/// [`FilterDispatcher::new(eh)`]: FilterDispatcher::new
/// [`Either<UpdaterE, HandlerE>`]: either::Either
/// [`Print`]: crate::dispatching::error_handlers::Print
pub struct FilterDispatcher<'a, HandlerE> {
message_handlers: FiltersWithHandlers<'a, Message, HandlerE>,
edited_message_handlers: FiltersWithHandlers<'a, Message, HandlerE>,
channel_post_handlers: FiltersWithHandlers<'a, Message, HandlerE>,
edited_channel_post_handlers: FiltersWithHandlers<'a, Message, HandlerE>,
inline_query_handlers: FiltersWithHandlers<'a, InlineQuery, HandlerE>,
chosen_inline_result_handlers:
FiltersWithHandlers<'a, ChosenInlineResult, HandlerE>,
callback_query_handlers: FiltersWithHandlers<'a, CallbackQuery, HandlerE>,
}
impl<'a, HandlerE> FilterDispatcher<'a, HandlerE> {
pub fn new() -> Self {
FilterDispatcher {
message_handlers: Vec::new(),
edited_message_handlers: Vec::new(),
channel_post_handlers: Vec::new(),
edited_channel_post_handlers: Vec::new(),
inline_query_handlers: Vec::new(),
chosen_inline_result_handlers: Vec::new(),
callback_query_handlers: Vec::new(),
}
}
pub fn message_handler<F, H>(mut self, filter: F, handler: H) -> Self
where
F: Filter<Message> + 'a,
H: Handler<Message, HandlerE> + 'a,
{
self.message_handlers
.push((Box::new(filter), Box::new(handler)));
self
}
pub fn edited_message_handler<F, H>(mut self, filter: F, handler: H) -> Self
where
F: Filter<Message> + 'a,
H: Handler<Message, HandlerE> + 'a,
{
self.edited_message_handlers
.push((Box::new(filter), Box::new(handler)));
self
}
pub fn channel_post_handler<F, H>(mut self, filter: F, handler: H) -> Self
where
F: Filter<Message> + 'a,
H: Handler<Message, HandlerE> + 'a,
{
self.channel_post_handlers
.push((Box::new(filter), Box::new(handler)));
self
}
pub fn edited_channel_post_handler<F, H>(
mut self,
filter: F,
handler: H,
) -> Self
where
F: Filter<Message> + 'a,
H: Handler<Message, HandlerE> + 'a,
{
self.edited_channel_post_handlers
.push((Box::new(filter), Box::new(handler)));
self
}
pub fn inline_query_handler<F, H>(mut self, filter: F, handler: H) -> Self
where
F: Filter<InlineQuery> + 'a,
H: Handler<InlineQuery, HandlerE> + 'a,
{
self.inline_query_handlers
.push((Box::new(filter), Box::new(handler)));
self
}
pub fn chosen_inline_result_handler<F, H>(
mut self,
filter: F,
handler: H,
) -> Self
where
F: Filter<ChosenInlineResult> + 'a,
H: Handler<ChosenInlineResult, HandlerE> + 'a,
{
self.chosen_inline_result_handlers
.push((Box::new(filter), Box::new(handler)));
self
}
pub fn callback_query_handler<F, H>(mut self, filter: F, handler: H) -> Self
where
F: Filter<CallbackQuery> + 'a,
H: Handler<CallbackQuery, HandlerE> + 'a,
{
self.callback_query_handlers
.push((Box::new(filter), Box::new(handler)));
self
}
pub async fn dispatch<U>(
&mut self,
update: Update,
) -> Result<(), HandlerE> {
let Update { kind, id } = update;
let res = match kind.clone() {
UpdateKind::Message(mes) => {
Self::handle(mes, &self.message_handlers).await
}
UpdateKind::EditedMessage(mes) => {
Self::handle(mes, &self.edited_message_handlers).await
}
UpdateKind::ChannelPost(post) => {
Self::handle(post, &self.channel_post_handlers).await
}
UpdateKind::EditedChannelPost(post) => {
Self::handle(post, &self.edited_channel_post_handlers).await
}
UpdateKind::InlineQuery(query) => {
Self::handle(query, &self.inline_query_handlers).await
}
UpdateKind::ChosenInlineResult(result) => {
Self::handle(result, &self.chosen_inline_result_handlers).await
}
UpdateKind::CallbackQuery(callback) => {
Self::handle(callback, &self.callback_query_handlers).await
}
};
log::debug!("dispatched update#{id:?}: {kind:?}", id = id, kind = kind);
res
}
async fn handle<T>(
update: T,
handlers: &[FilterWithHandler<'a, T, HandlerE>],
) -> Result<(), HandlerE>
where
T: std::fmt::Debug,
{
for x in handlers {
if x.0.test(&update) {
return x.1.handle(update).await;
}
}
log::warn!("unhandled update {:?}", update);
Ok(())
}
}
#[cfg(test)]
mod tests {
use std::{
convert::Infallible,
sync::atomic::{AtomicI32, Ordering},
};
use crate::{
dispatching::{FilterDispatcher, Updater},
types::{
Chat, ChatKind, ForwardKind, MediaKind, Message, MessageKind,
Sender, Update, UpdateKind, User,
},
};
#[tokio::test]
async fn first_handler_executes_1_time() {
let counter = &AtomicI32::new(0);
let counter2 = &AtomicI32::new(0);
let mut dp = FilterDispatcher::<Infallible, _>::new(|_| async {})
.message_handler(true, |_mes: Message| async move {
counter.fetch_add(1, Ordering::SeqCst);
Ok::<_, Infallible>(())
})
.message_handler(true, |_mes: Message| async move {
counter2.fetch_add(1, Ordering::SeqCst);
Ok::<_, Infallible>(())
});
dp.dispatch(one_message_updater()).await;
assert_eq!(counter.load(Ordering::SeqCst), 1);
assert_eq!(counter2.load(Ordering::SeqCst), 0);
}
fn message() -> Message {
Message {
id: 6534,
date: 1_567_898_953,
chat: Chat {
id: 218_485_655,
photo: None,
kind: ChatKind::Private {
type_: (),
first_name: Some("W".to_string()),
last_name: None,
username: Some("WaffleLapkin".to_string()),
},
},
kind: MessageKind::Common {
from: Sender::User(User {
id: 457_569_668,
is_bot: true,
first_name: "BT".to_string(),
last_name: None,
username: Some("BloodyTestBot".to_string()),
language_code: None,
}),
forward_kind: ForwardKind::Origin {
reply_to_message: None,
},
edit_date: None,
media_kind: MediaKind::Text {
text: "text".to_string(),
entities: vec![],
},
reply_markup: None,
},
}
}
fn message_update() -> Update {
Update {
id: 0,
kind: UpdateKind::Message(message()),
}
}
fn one_message_updater() -> impl Updater<Infallible> {
use futures::{future::ready, stream};
stream::once(ready(Ok(message_update())))
}
}

View file

@ -1 +0,0 @@
pub mod filter;

View file

@ -1,149 +0,0 @@
//! Error handlers.
// Infallible used here instead of `!` to be compatible with rust <1.41.
use std::{convert::Infallible, fmt::Debug, future::Future, pin::Pin};
/// An asynchronous handler of an error.
pub trait ErrorHandler<E> {
#[must_use]
fn handle_error<'a>(
&'a self,
error: E,
) -> Pin<Box<dyn Future<Output = ()> + 'a>>
where
E: 'a;
}
/// A handler that silently ignores all errors.
///
/// ## Example
/// ```
/// # #[tokio::main]
/// # async fn main_() {
/// use teloxide::dispatching::error_handlers::{ErrorHandler, Ignore};
///
/// Ignore.handle_error(()).await;
/// Ignore.handle_error(404).await;
/// Ignore.handle_error(String::from("error")).await;
/// # }
/// ```
pub struct Ignore;
impl<E> ErrorHandler<E> for Ignore {
#[must_use]
fn handle_error<'a>(
&'a self,
_: E,
) -> Pin<Box<dyn Future<Output = ()> + 'a>>
where
E: 'a,
{
Box::pin(async {})
}
}
/// An error handler that silently ignores all errors that can never happen
/// (e.g.: [`!`] or [`Infallible`]).
///
/// ## Examples
/// ```
/// # #[tokio::main]
/// # async fn main_() {
/// use std::convert::{Infallible, TryInto};
///
/// use teloxide::dispatching::error_handlers::{ErrorHandler, IgnoreSafe};
///
/// let result: Result<String, Infallible> = "str".try_into();
/// match result {
/// Ok(string) => println!("{}", string),
/// Err(inf) => IgnoreSafe.handle_error(inf).await,
/// }
///
/// IgnoreSafe.handle_error(return).await; // return type of `return` is `!` (aka never)
/// # }
/// ```
///
/// ```compile_fail
/// use teloxide::dispatching::dispatchers::filter::error_policy::{
/// ErrorPolicy, IgnoreSafe,
/// };
///
/// IgnoreSafe.handle_error(0);
/// ```
///
/// [`!`]: https://doc.rust-lang.org/std/primitive.never.html
/// [`Infallible`]: std::convert::Infallible
pub struct IgnoreSafe;
#[allow(unreachable_code)]
impl ErrorHandler<Infallible> for IgnoreSafe {
fn handle_error<'a>(
&'a self,
_: Infallible,
) -> Pin<Box<dyn Future<Output = ()> + 'a>>
where
Infallible: 'a,
{
Box::pin(async {})
}
}
/// An error handler that prints all errors passed into it.
///
/// ## Example
/// ```
/// # #[tokio::main]
/// # async fn main_() {
/// use teloxide::dispatching::error_handlers::{ErrorHandler, Print};
///
/// Print.handle_error(()).await;
/// Print.handle_error(404).await;
/// Print.handle_error(String::from("error")).await;
/// # }
/// ```
pub struct Print;
impl<E> ErrorHandler<E> for Print
where
E: Debug,
{
fn handle_error<'a>(
&'a self,
error: E,
) -> Pin<Box<dyn Future<Output = ()> + 'a>>
where
E: 'a,
{
log::debug!("error: {:?}", error);
Box::pin(async {})
}
}
/// The implementation of `ErrorHandler` for `Fn(error) -> Future<Output = ()>`.
///
/// ## Example
/// ```
/// # #[tokio::main]
/// # async fn main_() {
/// use teloxide::dispatching::error_handlers::ErrorHandler;
///
/// let mut closure = |e: i32| async move { eprintln!("Error code{}", e) };
///
/// closure.handle_error(404).await;
/// # }
/// ```
impl<E, F, Fut> ErrorHandler<E> for F
where
F: Fn(E) -> Fut,
Fut: Future<Output = ()>,
{
fn handle_error<'a>(
&'a self,
error: E,
) -> Pin<Box<dyn Future<Output = ()> + 'a>>
where
E: 'a,
{
Box::pin(async move { self(error).await })
}
}

View file

@ -1,30 +1,49 @@
use std::{future::Future, pin::Pin};
/// An asynchronous handler of a value.
pub trait Handler<T, E> {
/// Continue or terminate a user session.
#[derive(Debug, Copy, Clone, Eq, Hash, PartialEq)]
pub enum SessionState<Session> {
Continue(Session),
Terminate,
}
/// A handler of a user session and an update.
///
/// ## Returns
/// Returns [`SessionState::Continue(session)`] if it wants to be called again
/// after a new update, or [`SessionState::Terminate`] if not.
///
/// [`SessionState::Continue(session)`]:
/// crate::dispatching::SessionState::Continue
/// [`SessionState::Terminate`]: crate::dispatching::SessionState::Terminate
pub trait Handler<Session, U> {
#[must_use]
fn handle<'a>(
&'a self,
value: T,
) -> Pin<Box<dyn Future<Output = Result<(), E>> + 'a>>
session: Session,
update: U,
) -> Pin<Box<dyn Future<Output = SessionState<Session>> + 'a>>
where
T: 'a;
Session: 'a,
U: 'a;
}
/// The implementation of `Handler` for `Fn(U) -> Future<Output = Result<(),
/// E>>`.
impl<T, E, F, Fut> Handler<T, E> for F
/// The implementation of `Handler` for `Fn(Session, Update) -> Future<Output =
/// SessionState<Session>>`.
impl<Session, U, F, Fut> Handler<Session, U> for F
where
F: Fn(T) -> Fut,
Fut: Future<Output = Result<(), E>>,
F: Fn(Session, U) -> Fut,
Fut: Future<Output = SessionState<Session>>,
{
fn handle<'a>(
&'a self,
value: T,
session: Session,
update: U,
) -> Pin<Box<dyn Future<Output = Fut::Output> + 'a>>
where
T: 'a,
Session: 'a,
U: 'a,
{
Box::pin(async move { self(value).await })
Box::pin(async move { self(session, update).await })
}
}

View file

@ -1,13 +1,16 @@
//! Update dispatching.
mod dispatchers;
pub mod error_handlers;
/// If an update was handled by a dispatcher or not.
#[derive(Debug, Copy, Clone, Eq, Hash, PartialEq)]
pub enum DispatchResult {
Handled,
Unhandled,
}
pub mod chat;
pub mod filters;
mod handler;
pub mod updaters;
pub mod update_listeners;
pub use dispatchers::filter::FilterDispatcher;
pub use error_handlers::ErrorHandler;
pub use filters::Filter;
pub use handler::Handler;
pub use updaters::Updater;
pub use handler::*;

View file

@ -1,12 +1,13 @@
//! Receiving updates from Telegram.
//!
//! The key trait here is [`Updater`]. You can get it by these functions:
//! The key trait here is [`UpdateListener`]. You can get it by these functions:
//!
//! - [`polling_default`], which returns a default long polling updater.
//! - [`polling`], which returns a long/short polling updater with your
//! - [`polling_default`], which returns a default long polling listener.
//! - [`polling`], which returns a long/short polling listener with your
//! configuration.
//!
//! And then you can pass it directly to a dispatcher.
//! And then you can extract updates from it and pass them directly to a
//! dispatcher.
//!
//! Telegram supports two ways of [getting updates]: [long]/[short] polling and
//! [webhook].
@ -90,10 +91,9 @@
//! <a id="4" href="#4b">^4</a> `offset = N` means that we've already received
//! updates `0..=N`.
//!
//! [`Updater`]: Updater
//! [`UpdateListener`]: UpdateListener
//! [`polling_default`]: polling_default
//! [`polling`]: polling
//! [`Dispatcher`]: crate::dispatching::Dispatcher::dispatch
//! [`Box::get_updates`]: crate::Bot::get_updates
//! [getting updates]: https://core.telegram.org/bots/api#getting-updates
//! [long]: https://en.wikipedia.org/wiki/Push_technology#Long_polling
@ -110,22 +110,22 @@ use crate::{
};
use std::{convert::TryInto, time::Duration};
/// A generic updater.
pub trait Updater<E>: Stream<Item = Result<Update, E>> {
/// A generic update listener.
pub trait UpdateListener<E>: Stream<Item = Result<Update, E>> {
// TODO: add some methods here (.shutdown(), etc).
}
impl<S, E> Updater<E> for S where S: Stream<Item = Result<Update, E>> {}
impl<S, E> UpdateListener<E> for S where S: Stream<Item = Result<Update, E>> {}
/// Returns a long polling updater with the default configuration.
/// Returns a long polling update listener with the default configuration.
///
/// See also: [`polling`](polling).
pub fn polling_default(bot: &Bot) -> impl Updater<RequestError> + '_ {
pub fn polling_default(bot: &Bot) -> impl UpdateListener<RequestError> + '_ {
polling(bot, None, None, None)
}
/// Returns a long/short polling updater with some additional options.
/// Returns a long/short polling update listener with some additional options.
///
/// - `bot`: Using this bot, the returned updater will receive updates.
/// - `bot`: Using this bot, the returned update listener will receive updates.
/// - `timeout`: A timeout for polling.
/// - `limit`: Limits the number of updates to be retrieved at once. Values
/// between 1—100 are accepted.
@ -140,7 +140,7 @@ pub fn polling(
timeout: Option<Duration>,
limit: Option<u8>,
allowed_updates: Option<Vec<AllowedUpdate>>,
) -> impl Updater<RequestError> + '_ {
) -> impl UpdateListener<RequestError> + '_ {
let timeout =
timeout.map(|t| t.as_secs().try_into().expect("timeout is too big"));

View file

@ -57,7 +57,7 @@ pub enum ApiErrorKind {
/// May happen in methods:
/// 1. [`EditMessageText`]
///
/// [`EditMessageText`]: crate::requests::payloads::EditMessageText
/// [`EditMessageText`]: crate::requests::EditMessageText
#[serde(rename = "Bad Request: message is not modified: specified new \
message content and reply markup are exactly the same \
as a current content and reply markup of the message")]
@ -69,8 +69,8 @@ pub enum ApiErrorKind {
/// 1. [`ForwardMessage`]
/// 2. [`DeleteMessage`]
///
/// [`ForwardMessage`]: crate::requests::payloads::ForwardMessage
/// [`DeleteMessage`]: crate::requests::payloads::DeleteMessage
/// [`ForwardMessage`]: crate::requests::ForwardMessage
/// [`DeleteMessage`]: crate::requests::DeleteMessage
#[serde(rename = "Bad Request: MESSAGE_ID_INVALID")]
MessageIdInvalid,
@ -79,7 +79,7 @@ pub enum ApiErrorKind {
/// May happen in methods:
/// 1. [`ForwardMessage`]
///
/// [`ForwardMessage`]: crate::requests::payloads::ForwardMessage
/// [`ForwardMessage`]: crate::requests::ForwardMessage
#[serde(rename = "Bad Request: message to forward not found")]
MessageToForwardNotFound,
@ -88,7 +88,7 @@ pub enum ApiErrorKind {
/// May happen in methods:
/// 1. [`DeleteMessage`]
///
/// [`DeleteMessage`]: crate::requests::payloads::DeleteMessage
/// [`DeleteMessage`]: crate::requests::DeleteMessage
#[serde(rename = "Bad Request: message to delete not found")]
MessageToDeleteNotFound,
@ -97,7 +97,7 @@ pub enum ApiErrorKind {
/// May happen in methods:
/// 1. [`SendMessage`]
///
/// [`SendMessage`]: crate::requests::payloads::SendMessage
/// [`SendMessage`]: crate::requests::SendMessage
#[serde(rename = "Bad Request: message text is empty")]
MessageTextIsEmpty,
@ -106,7 +106,7 @@ pub enum ApiErrorKind {
/// May happen in methods:
/// 1. [`EditMessageText`]
///
/// [`EditMessageText`]: crate::requests::payloads::EditMessageText
/// [`EditMessageText`]: crate::requests::EditMessageText
#[serde(rename = "Bad Request: message can't be edited")]
MessageCantBeEdited,
@ -116,7 +116,7 @@ pub enum ApiErrorKind {
/// May happen in methods:
/// 1. [`DeleteMessage`]
///
/// [`DeleteMessage`]: crate::requests::payloads::DeleteMessage
/// [`DeleteMessage`]: crate::requests::DeleteMessage
#[serde(rename = "Bad Request: message can't be deleted")]
MessageCantBeDeleted,
@ -125,7 +125,7 @@ pub enum ApiErrorKind {
/// May happen in methods:
/// 1. [`EditMessageText`]
///
/// [`EditMessageText`]: crate::requests::payloads::EditMessageText
/// [`EditMessageText`]: crate::requests::EditMessageText
#[serde(rename = "Bad Request: message to edit not found")]
MessageToEditNotFound,
@ -134,7 +134,7 @@ pub enum ApiErrorKind {
/// May happen in methods:
/// 1. [`SendMessage`]
///
/// [`SendMessage`]: crate::requests::payloads::SendMessage
/// [`SendMessage`]: crate::requests::SendMessage
#[serde(rename = "Bad Request: reply message not found")]
MessageToReplyNotFound,
@ -148,7 +148,7 @@ pub enum ApiErrorKind {
/// May happen in methods:
/// 1. [`SendMessage`]
///
/// [`SendMessage`]: crate::requests::payloads::SendMessage
/// [`SendMessage`]: crate::requests::SendMessage
#[serde(rename = "Bad Request: message is too long")]
MessageIsTooLong,
@ -157,7 +157,7 @@ pub enum ApiErrorKind {
/// May happen in methods:
/// 1. [`SendMediaGroup`]
///
/// [`SendMediaGroup`]: crate::requests::payloads::SendMediaGroup
/// [`SendMediaGroup`]: crate::requests::SendMediaGroup
#[serde(rename = "Bad Request: Too much messages to send as an album")]
ToMuchMessages,
@ -166,7 +166,7 @@ pub enum ApiErrorKind {
/// May happen in methods:
/// 1. [`SendPoll`]
///
/// [`SendPoll`]: crate::requests::payloads::SendPoll
/// [`SendPoll`]: crate::requests::SendPoll
#[serde(rename = "Bad Request: poll has already been closed")]
PollHasAlreadyClosed,
@ -175,7 +175,7 @@ pub enum ApiErrorKind {
/// May happen in methods:
/// 1. [`SendPoll`]
///
/// [`SendPoll`]: crate::requests::payloads::SendPoll
/// [`SendPoll`]: crate::requests::SendPoll
#[serde(rename = "Bad Request: poll must have at least 2 option")]
PollMustHaveMoreOptions,
@ -184,7 +184,7 @@ pub enum ApiErrorKind {
/// May happen in methods:
/// 1. [`SendPoll`]
///
/// [`SendPoll`]: crate::requests::payloads::SendPoll
/// [`SendPoll`]: crate::requests::SendPoll
#[serde(rename = "Bad Request: poll can't have more than 10 options")]
PollCantHaveMoreOptions,
@ -193,7 +193,7 @@ pub enum ApiErrorKind {
/// May happen in methods:
/// 1. [`SendPoll`]
///
/// [`SendPoll`]: crate::requests::payloads::SendPoll
/// [`SendPoll`]: crate::requests::SendPoll
#[serde(rename = "Bad Request: poll options must be non-empty")]
PollOptionsMustBeNonEmpty,
@ -202,7 +202,7 @@ pub enum ApiErrorKind {
/// May happen in methods:
/// 1. [`SendPoll`]
///
/// [`SendPoll`]: crate::requests::payloads::SendPoll
/// [`SendPoll`]: crate::requests::SendPoll
#[serde(rename = "Bad Request: poll question must be non-empty")]
PollQuestionMustBeNonEmpty,
@ -212,7 +212,7 @@ pub enum ApiErrorKind {
/// May happen in methods:
/// 1. [`SendPoll`]
///
/// [`SendPoll`]: crate::requests::payloads::SendPoll
/// [`SendPoll`]: crate::requests::SendPoll
#[serde(rename = "Bad Request: poll options length must not exceed 100")]
PollOptionsLengthTooLong,
@ -222,7 +222,7 @@ pub enum ApiErrorKind {
/// May happen in methods:
/// 1. [`SendPoll`]
///
/// [`SendPoll`]: crate::requests::payloads::SendPoll
/// [`SendPoll`]: crate::requests::SendPoll
#[serde(rename = "Bad Request: poll question length must not exceed 255")]
PollQuestionLengthTooLong,
@ -231,7 +231,7 @@ pub enum ApiErrorKind {
/// May happen in methods:
/// 1. [`StopPoll`]
///
/// [`StopPoll`]: crate::requests::payloads::StopPoll
/// [`StopPoll`]: crate::requests::StopPoll
#[serde(rename = "Bad Request: message with poll to stop not found")]
MessageWithPollNotFound,
@ -240,7 +240,7 @@ pub enum ApiErrorKind {
/// May happen in methods:
/// 1. [`StopPoll`]
///
/// [`StopPoll`]: crate::requests::payloads::StopPoll
/// [`StopPoll`]: crate::requests::StopPoll
#[serde(rename = "Bad Request: message is not a poll")]
MessageIsNotAPoll,
@ -250,7 +250,7 @@ pub enum ApiErrorKind {
/// May happen in methods:
/// 1. [`SendMessage`]
///
/// [`SendMessage`]: crate::requests::payloads::SendMessage
/// [`SendMessage`]: crate::requests::SendMessage
#[serde(rename = "Bad Request: chat not found")]
ChatNotFound,
@ -260,7 +260,7 @@ pub enum ApiErrorKind {
/// 1. [`getUserProfilePhotos`]
///
/// [`getUserProfilePhotos`]:
/// crate::requests::payloads::getUserProfilePhotos
/// crate::requests::GetUserProfilePhotos
#[serde(rename = "Bad Request: user not found")]
UserNotFound,
@ -270,7 +270,7 @@ pub enum ApiErrorKind {
/// May happen in methods:
/// 1. [`SetChatDescription`]
///
/// [`SetChatDescription`]: crate::requests::payloads::SetChatDescription
/// [`SetChatDescription`]: crate::requests::SetChatDescription
#[serde(rename = "Bad Request: chat description is not modified")]
ChatDescriptionIsNotModified,
@ -279,7 +279,7 @@ pub enum ApiErrorKind {
/// May happen in methods:
/// 1. [`AnswerCallbackQuery`]
///
/// [`AnswerCallbackQuery`]: crate::requests::payloads::AnswerCallbackQuery
/// [`AnswerCallbackQuery`]: crate::requests::AnswerCallbackQuery
#[serde(rename = "Bad Request: query is too old and response timeout \
expired or query id is invalid")]
InvalidQueryID,
@ -290,7 +290,7 @@ pub enum ApiErrorKind {
/// May happen in methods:
/// 1. [`SendMessage`]
///
/// [`SendMessage`]: crate::requests::payloads::SendMessage
/// [`SendMessage`]: crate::requests::SendMessage
#[serde(rename = "Bad Request: BUTTON_URL_INVALID")]
ButtonURLInvalid,
@ -299,7 +299,7 @@ pub enum ApiErrorKind {
/// May happen in methods:
/// 1. [`SendMessage`]
///
/// [`SendMessage`]: crate::requests::payloads::SendMessage
/// [`SendMessage`]: crate::requests::SendMessage
#[serde(rename = "Bad Request: BUTTON_DATA_INVALID")]
ButtonDataInvalid,
@ -308,7 +308,7 @@ pub enum ApiErrorKind {
/// May happen in methods:
/// 1. [`SendMessage`]
///
/// [`SendMessage`]: crate::requests::payloads::SendMessage
/// [`SendMessage`]: crate::requests::SendMessage
#[serde(rename = "Bad Request: can't parse inline keyboard button: Text \
buttons are unallowed in the inline keyboard")]
TextButtonsAreUnallowed,
@ -318,7 +318,7 @@ pub enum ApiErrorKind {
/// May happen in methods:
/// 1. [`GetFile`]
///
/// [`GetFile`]: crate::requests::payloads::GetFile
/// [`GetFile`]: crate::requests::GetFile
#[serde(rename = "Bad Request: wrong file id")]
WrongFileID,
@ -331,7 +331,7 @@ pub enum ApiErrorKind {
/// May happen in methods:
/// 1. [`SetChatPhoto`]
///
/// [`SetChatPhoto`]: crate::requests::payloads::SetChatPhoto
/// [`SetChatPhoto`]: crate::requests::SetChatPhoto
#[serde(rename = "Bad Request: Photo should be uploaded as an InputFile")]
PhotoAsInputFileRequired,
@ -340,7 +340,7 @@ pub enum ApiErrorKind {
/// May happen in methods:
/// 1. [`AddStickerToSet`]
///
/// [`AddStickerToSet`]: crate::requests::payloads::AddStickerToSet
/// [`AddStickerToSet`]: crate::requests::AddStickerToSet
#[serde(rename = "Bad Request: STICKERSET_INVALID")]
InvalidStickersSet,
@ -350,7 +350,7 @@ pub enum ApiErrorKind {
/// May happen in methods:
/// 1. [`PinMessage`]
///
/// [`PinMessage`]: crate::requests::payloads::PinMessage
/// [`PinMessage`]: crate::requests::PinMessage
#[serde(rename = "Bad Request: not enough rights to pin a message")]
NotEnoughRightsToPinMessage,
@ -365,7 +365,7 @@ pub enum ApiErrorKind {
/// May happen in methods:
/// 1. [`PromoteChatMember`]
///
/// [`PromoteChatMember`]: crate::requests::payloads::PromoteChatMember
/// [`PromoteChatMember`]: crate::requests::PromoteChatMember
#[serde(rename = "Bad Request: can't demote chat creator")]
CantDemoteChatCreator,
@ -374,7 +374,7 @@ pub enum ApiErrorKind {
/// May happen in methods:
/// 1. [`RestrictChatMember`]
///
/// [`RestrictChatMember`]: crate::requests::payloads::RestrictChatMember
/// [`RestrictChatMember`]: crate::requests::RestrictChatMember
#[serde(rename = "Bad Request: can't restrict self")]
CantRestrictSelf,
@ -384,7 +384,7 @@ pub enum ApiErrorKind {
/// May happen in methods:
/// 1. [`RestrictChatMember`]
///
/// [`RestrictChatMember`]: crate::requests::payloads::RestrictChatMember
/// [`RestrictChatMember`]: crate::requests::RestrictChatMember
#[serde(rename = "Bad Request: not enough rights to restrict/unrestrict \
chat member")]
NotEnoughRightsToRestrict,
@ -394,7 +394,7 @@ pub enum ApiErrorKind {
/// May happen in methods:
/// 1. [`SetWebhook`]
///
/// [`SetWebhook`]: crate::requests::payloads::SetWebhook
/// [`SetWebhook`]: crate::requests::SetWebhook
#[serde(rename = "Bad Request: bad webhook: HTTPS url must be provided \
for webhook")]
WebhookRequireHTTPS,
@ -405,7 +405,7 @@ pub enum ApiErrorKind {
/// May happen in methods:
/// 1. [`SetWebhook`]
///
/// [`SetWebhook`]: crate::requests::payloads::SetWebhook
/// [`SetWebhook`]: crate::requests::SetWebhook
#[serde(rename = "Bad Request: bad webhook: Webhook can be set up only \
on ports 80, 88, 443 or 8443")]
BadWebhookPort,
@ -415,7 +415,7 @@ pub enum ApiErrorKind {
/// May happen in methods:
/// 1. [`SetWebhook`]
///
/// [`SetWebhook`]: crate::requests::payloads::SetWebhook
/// [`SetWebhook`]: crate::requests::SetWebhook
#[serde(rename = "Bad Request: bad webhook: Failed to resolve host: \
Name or service not known")]
UnknownHost,
@ -425,7 +425,7 @@ pub enum ApiErrorKind {
/// May happen in methods:
/// 1. [`SetWebhook`]
///
/// [`SetWebhook`]: crate::requests::payloads::SetWebhook
/// [`SetWebhook`]: crate::requests::SetWebhook
#[serde(rename = "Bad Request: can't parse URL")]
CantParseUrl,
@ -434,7 +434,7 @@ pub enum ApiErrorKind {
/// May happen in methods:
/// 1. [`SendMessage`]
///
/// [`SendMessage`]: crate::requests::payloads::SendMessage
/// [`SendMessage`]: crate::requests::SendMessage
#[serde(rename = "Bad Request: can't parse entities")]
CantParseEntities,
@ -443,7 +443,7 @@ pub enum ApiErrorKind {
/// May happen in methods:
/// 1. [`GetUpdates`]
///
/// [`GetUpdates`]: crate::requests::payloads::GetUpdates
/// [`GetUpdates`]: crate::requests::GetUpdates
#[serde(rename = "can't use getUpdates method while webhook is active")]
CantGetUpdates,
@ -452,7 +452,7 @@ pub enum ApiErrorKind {
/// May happen in methods:
/// 1. [`SendMessage`]
///
/// [`SendMessage`]: crate::requests::payloads::SendMessage
/// [`SendMessage`]: crate::requests::SendMessage
#[serde(rename = "Unauthorized: bot was kicked from a chat")]
BotKicked,
@ -461,7 +461,7 @@ pub enum ApiErrorKind {
/// May happen in methods:
/// 1. [`SendMessage`]
///
/// [`SendMessage`]: crate::requests::payloads::SendMessage
/// [`SendMessage`]: crate::requests::SendMessage
#[serde(rename = "Unauthorized: user is deactivated")]
UserDeactivated,
@ -470,7 +470,7 @@ pub enum ApiErrorKind {
/// May happen in methods:
/// 1. [`SendMessage`]
///
/// [`SendMessage`]: crate::requests::payloads::SendMessage
/// [`SendMessage`]: crate::requests::SendMessage
#[serde(
rename = "Unauthorized: bot can't initiate conversation with a user"
)]
@ -481,7 +481,7 @@ pub enum ApiErrorKind {
/// May happen in methods:
/// 1. [`SendMessage`]
///
/// [`SendMessage`]: crate::requests::payloads::SendMessage
/// [`SendMessage`]: crate::requests::SendMessage
#[serde(rename = "Unauthorized: bot can't send messages to bots")]
CantTalkWithBots,
@ -490,7 +490,7 @@ pub enum ApiErrorKind {
/// May happen in methods:
/// 1. [`SendMessage`]
///
/// [`SendMessage`]: crate::requests::payloads::SendMessage
/// [`SendMessage`]: crate::requests::SendMessage
#[serde(rename = "Bad Request: wrong HTTP URL")]
WrongHTTPurl,
@ -500,7 +500,7 @@ pub enum ApiErrorKind {
/// May happen in methods:
/// 1. [`GetUpdates`]
///
/// [`GetUpdates`]: crate::requests::payloads::GetUpdates
/// [`GetUpdates`]: crate::requests::GetUpdates
#[serde(rename = "Conflict: terminated by other getUpdates request; \
make sure that only one bot instance is running")]
TerminatedByOtherGetUpdates,
@ -510,7 +510,7 @@ pub enum ApiErrorKind {
/// May happen in methods:
/// 1. [`GetFile`]
///
/// [`GetFile`]: crate::requests::payloads::GetFile
/// [`GetFile`]: crate::requests::GetFile
#[serde(rename = "Bad Request: invalid file id")]
FileIdInvalid,

View file

@ -39,13 +39,11 @@ pub async fn download_file_stream(
.await?
.error_for_status()?;
Ok(futures::stream::unfold(res, |mut res| {
async {
match res.chunk().await {
Err(err) => Some((Err(err), res)),
Ok(Some(c)) => Some((Ok(c), res)),
Ok(None) => None,
}
Ok(futures::stream::unfold(res, |mut res| async {
match res.chunk().await {
Err(err) => Some((Err(err), res)),
Ok(Some(c)) => Some((Ok(c), res)),
Ok(None) => None,
}
}))
}

View file

@ -1,6 +1,6 @@
use serde::{Deserialize, Serialize};
use crate::types::User;
use crate::types::{Message, User};
/// This object represents one special entity in a text message. For example,
/// hashtags, usernames, URLs, etc.
@ -39,21 +39,85 @@ pub enum MessageEntityKind {
Strikethrough,
}
#[test]
fn recursive_kind() {
use serde_json::from_str;
assert_eq!(
MessageEntity {
kind: MessageEntityKind::TextLink {
url: "ya.ru".into()
},
offset: 1,
length: 2,
},
from_str::<MessageEntity>(
r#"{"type":"text_link","url":"ya.ru","offset":1,"length":2}"#
)
.unwrap()
);
impl MessageEntity {
pub fn text_from(&self, message: &Message) -> Option<String> {
let text = message.text();
Some(String::from(&text?[self.offset..self.offset + self.length]))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::{
Chat, ChatKind, ForwardKind, MediaKind, MessageKind, Sender,
};
#[test]
fn recursive_kind() {
use serde_json::from_str;
assert_eq!(
MessageEntity {
kind: MessageEntityKind::TextLink {
url: "ya.ru".into()
},
offset: 1,
length: 2,
},
from_str::<MessageEntity>(
r#"{"type":"text_link","url":"ya.ru","offset":1,"length":2}"#
)
.unwrap()
);
}
#[test]
fn text_from() {
let message = message();
let expected = Some("yes".to_string());
let entity = message.entities().unwrap()[0].clone();
let actual = entity.text_from(&message);
assert_eq!(actual, expected);
}
fn message() -> Message {
Message {
id: 0,
date: 0,
chat: Chat {
id: 0,
kind: ChatKind::Private {
type_: (),
username: None,
first_name: None,
last_name: None,
},
photo: None,
},
kind: MessageKind::Common {
from: Sender::User(User {
id: 0,
is_bot: false,
first_name: "".to_string(),
last_name: None,
username: None,
language_code: None,
}),
forward_kind: ForwardKind::Origin {
reply_to_message: None,
},
edit_date: None,
media_kind: MediaKind::Text {
text: "no yes no".to_string(),
entities: vec![MessageEntity {
kind: MessageEntityKind::Mention,
offset: 3,
length: 3,
}],
},
reply_markup: None,
},
}
}
}

View file

@ -2,7 +2,10 @@
use serde::{Deserialize, Serialize};
use crate::types::{CallbackQuery, ChosenInlineResult, InlineQuery, Message};
use crate::types::{
CallbackQuery, ChosenInlineResult, InlineQuery, Message, Poll,
PreCheckoutQuery, ShippingQuery,
};
/// This [object] represents an incoming update.
///
@ -57,7 +60,17 @@ pub enum UpdateKind {
/// New incoming callback query.
CallbackQuery(CallbackQuery),
// TODO: Add more variants
/// New incoming shipping query. Only for invoices with flexible price.
ShippingQuery(ShippingQuery),
/// New incoming pre-checkout query. Contains full information about
/// checkout.
PreCheckoutQuery(PreCheckoutQuery),
/// New poll state. Bots receive only updates about stopped polls and
/// polls, which are sent by the bot.
Poll(Poll),
}
#[cfg(test)]
@ -92,7 +105,7 @@ mod test {
}
}"#;
let expected: Update = Update {
let expected = Update {
id: 892_252_934,
kind: UpdateKind::Message(Message {
id: 6557,