diff --git a/Cargo.toml b/Cargo.toml index 6c7b54f3..6b662740 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "async-telegram-bot" +name = "telebofr" version = "0.1.0" edition = "2018" diff --git a/LICENSE b/LICENSE index 75a50a58..edb41b3e 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2019 async-telegram-bot +Copyright (c) 2019 telebofr Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 1a35a603..299433df 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,20 @@ <div align="center"> - <h1>async-telegram-bot</h1> + <img src="ICON.png" width="200"/> + <h1>telebofr</h1> - <a href="https://docs.rs/async-telegram-bot/"> + <a href="https://docs.rs/telebofr/"> <img src="https://img.shields.io/badge/docs.rs-link-blue.svg"> </a> - <a href="https://travis-ci.com/async-telegram-bot/async-telegram-bot"> - <img src="https://travis-ci.com/async-telegram-bot/async-telegram-bot.svg?branch=dev" /> + <a href="https://travis-ci.com/telebofr/telebofr"> + <img src="https://travis-ci.com/telebofr/telebofr.svg?branch=dev" /> </a> <a href="LICENSE"> <img src="https://img.shields.io/badge/license-MIT-blue.svg"> </a> - <a href="https://crates.io/crates/async-telegram-bot"> + <a href="https://crates.io/crates/telebofr"> <img src="https://img.shields.io/badge/crates.io-v0.1.0-orange.svg"> </a> - <br> - <img src="ICON.png" width="300"/> - <br> - A full-featured framework that empowers you to easily build [Telegram bots](https://telegram.org/blog/bot-revolution) using the [`async`/`.await`](https://rust-lang.github.io/async-book/01_getting_started/01_chapter.html) syntax in [Rust](https://www.rust-lang.org/). It handles all the difficult stuff so you can focus only on your business logic. </div> - -## A simple bot -```rust -fn main() { - let bot = Bot::new(API_TOKEN).bla().bla(); -} -``` diff --git a/hw.doc b/hw.doc new file mode 100644 index 00000000..0bdb82bf Binary files /dev/null and b/hw.doc differ diff --git a/rustfmt.toml b/rustfmt.toml index f99a2b6f..cee9b586 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -1,4 +1,5 @@ format_code_in_doc_comments = true wrap_comments = true format_strings = true -max_width = 80 \ No newline at end of file +max_width = 80 +merge_imports = true \ No newline at end of file diff --git a/src/bot/api.rs b/src/bot/api.rs index a75328c1..7a43b6c1 100644 --- a/src/bot/api.rs +++ b/src/bot/api.rs @@ -1,24 +1,28 @@ use crate::{ bot::Bot, requests::{ - AnswerPreCheckoutQuery, AnswerShippingQuery, EditMessageLiveLocation, - ForwardMessage, GetFile, GetMe, KickChatMember, PinChatMessage, - PromoteChatMember, RestrictChatMember, SendAudio, SendChatAction, - SendContact, SendLocation, SendMediaGroup, SendMessage, SendPhoto, - SendPoll, SendVenue, SendVideoNote, SendVoice, StopMessageLiveLocation, - UnbanChatMember, UnpinChatMessage, GetUpdates + AnswerCallbackQuery, AnswerPreCheckoutQuery, AnswerShippingQuery, + DeleteChatPhoto, DeleteChatStickerSet, EditMessageLiveLocation, + ExportCharInviteLink, ForwardMessage, GetChat, GetChatAdministrators, + GetChatMember, GetChatMembersCount, GetFile, GetMe, GetUpdates, + KickChatMember, LeaveChat, PinChatMessage, PromoteChatMember, + RestrictChatMember, SendAnimation, SendAudio, SendChatAction, + SendContact, SendDocument, SendLocation, SendMediaGroup, SendMessage, + SendPhoto, SendPoll, SendVenue, SendVideo, SendVideoNote, SendVoice, + SetChatDescription, SetChatPermissions, SetChatPhoto, + SetChatStickerSet, SetChatTitle, StopMessageLiveLocation, + UnbanChatMember, UnpinChatMessage, }, types::{ChatAction, ChatId, ChatPermissions, InputFile, InputMedia}, }; -/// Telegram functions impl Bot { pub fn get_me(&self) -> GetMe { - GetMe::new(self.ctx()) + GetMe::new(self) } pub fn get_updates(&self) -> GetUpdates { - GetUpdates::new(self.ctx()) + GetUpdates::new(self) } pub fn send_message<C, T>(&self, chat_id: C, text: T) -> SendMessage @@ -26,7 +30,7 @@ impl Bot { C: Into<ChatId>, T: Into<String>, { - SendMessage::new(self.ctx(), chat_id, text) + SendMessage::new(self, chat_id, text) } pub fn edit_message_live_location<Lt, Lg>( @@ -38,7 +42,7 @@ impl Bot { Lt: Into<f64>, Lg: Into<f64>, { - EditMessageLiveLocation::new(self.ctx(), latitude, longitude) + EditMessageLiveLocation::new(self, latitude, longitude) } pub fn forward_message<C, F, M>( @@ -52,7 +56,7 @@ impl Bot { F: Into<ChatId>, M: Into<i32>, { - ForwardMessage::new(self.ctx(), chat_id, from_chat_id, message_id) + ForwardMessage::new(self, chat_id, from_chat_id, message_id) } pub fn send_audio<C, A>(&self, chat_id: C, audio: A) -> SendAudio @@ -60,7 +64,7 @@ impl Bot { C: Into<ChatId>, A: Into<InputFile>, { - SendAudio::new(self.ctx(), chat_id, audio) + SendAudio::new(self, chat_id, audio) } pub fn send_location<C, Lt, Lg>( @@ -74,7 +78,7 @@ impl Bot { Lt: Into<f64>, Lg: Into<f64>, { - SendLocation::new(self.ctx(), chat_id, latitude, longitude) + SendLocation::new(self, chat_id, latitude, longitude) } pub fn send_media_group<C, M>(&self, chat_id: C, media: M) -> SendMediaGroup @@ -82,7 +86,7 @@ impl Bot { C: Into<ChatId>, M: Into<Vec<InputMedia>>, { - SendMediaGroup::new(self.ctx(), chat_id, media) + SendMediaGroup::new(self, chat_id, media) } pub fn send_photo<C, P>(&self, chat_id: C, photo: P) -> SendPhoto @@ -90,18 +94,18 @@ impl Bot { C: Into<ChatId>, P: Into<InputFile>, { - SendPhoto::new(self.ctx(), chat_id, photo) + SendPhoto::new(self, chat_id, photo) } pub fn stop_message_live_location(&self) -> StopMessageLiveLocation { - StopMessageLiveLocation::new(self.ctx()) + StopMessageLiveLocation::new(self) } pub fn get_file<F>(&self, file_id: F) -> GetFile where F: Into<String>, { - GetFile::new(self.ctx(), file_id) + GetFile::new(self, file_id) } pub fn answer_pre_checkout_query<I, O>( @@ -113,7 +117,14 @@ impl Bot { I: Into<String>, O: Into<bool>, { - AnswerPreCheckoutQuery::new(self.ctx(), pre_checkout_query_id, ok) + AnswerPreCheckoutQuery::new(self, pre_checkout_query_id, ok) + } + + pub fn get_chat<I>(&self, chat_id: I) -> GetChat + where + I: Into<ChatId>, + { + GetChat::new(self, chat_id) } pub fn answer_shipping_query<I, O>( @@ -125,7 +136,7 @@ impl Bot { I: Into<String>, O: Into<bool>, { - AnswerShippingQuery::new(self.ctx(), shipping_query_id, ok) + AnswerShippingQuery::new(self, shipping_query_id, ok) } pub fn kick_chat_member<C, U>( @@ -137,7 +148,7 @@ impl Bot { C: Into<ChatId>, U: Into<i32>, { - KickChatMember::new(self.ctx(), chat_id, user_id) + KickChatMember::new(self, chat_id, user_id) } pub fn pin_chat_message<C, M>( @@ -149,7 +160,7 @@ impl Bot { C: Into<ChatId>, M: Into<i32>, { - PinChatMessage::new(self.ctx(), chat_id, message_id) + PinChatMessage::new(self, chat_id, message_id) } pub fn promote_chat_member<C, U>( @@ -161,7 +172,7 @@ impl Bot { C: Into<ChatId>, U: Into<i32>, { - PromoteChatMember::new(self.ctx(), chat_id, user_id) + PromoteChatMember::new(self, chat_id, user_id) } pub fn restrict_chat_member<C, U, P>( @@ -175,7 +186,7 @@ impl Bot { U: Into<i32>, P: Into<ChatPermissions>, { - RestrictChatMember::new(self.ctx(), chat_id, user_id, permissions) + RestrictChatMember::new(self, chat_id, user_id, permissions) } pub fn send_chat_action<C, A>( @@ -187,7 +198,7 @@ impl Bot { C: Into<ChatId>, A: Into<ChatAction>, { - SendChatAction::new(self.ctx(), chat_id, action) + SendChatAction::new(self, chat_id, action) } pub fn send_contact<C, P, F>( @@ -201,7 +212,7 @@ impl Bot { P: Into<String>, F: Into<String>, { - SendContact::new(self.ctx(), chat_id, phone_number, first_name) + SendContact::new(self, chat_id, phone_number, first_name) } pub fn send_poll<C, Q, O>( @@ -215,7 +226,7 @@ impl Bot { Q: Into<String>, O: Into<Vec<String>>, { - SendPoll::new(self.ctx(), chat_id, question, options) + SendPoll::new(self, chat_id, question, options) } pub fn send_venue<C, Lt, Lg, T, A>( @@ -233,7 +244,7 @@ impl Bot { T: Into<String>, A: Into<String>, { - SendVenue::new(self.ctx(), chat_id, latitude, longitude, title, address) + SendVenue::new(self, chat_id, latitude, longitude, title, address) } pub fn send_video_note<C, V>( @@ -243,17 +254,24 @@ impl Bot { ) -> SendVideoNote where C: Into<ChatId>, - V: Into<String>, // TODO: InputFile + V: Into<InputFile>, { - SendVideoNote::new(self.ctx(), chat_id, video_note) + SendVideoNote::new(self, chat_id, video_note) } pub fn send_voice<C, V>(&self, chat_id: C, voice: V) -> SendVoice where C: Into<ChatId>, - V: Into<String>, // TODO: InputFile + V: Into<InputFile>, { - SendVoice::new(self.ctx(), chat_id, voice) + SendVoice::new(self, chat_id, voice) + } + + pub fn send_chat_description<C>(&self, chat_id: C) -> SetChatDescription + where + C: Into<ChatId>, + { + SetChatDescription::new(self, chat_id) } pub fn unban_chat_member<C, U>( @@ -265,13 +283,144 @@ impl Bot { C: Into<ChatId>, U: Into<i32>, { - UnbanChatMember::new(self.ctx(), chat_id, user_id) + UnbanChatMember::new(self, chat_id, user_id) } pub fn unpin_chat_message<C>(&self, chat_id: C) -> UnpinChatMessage where C: Into<ChatId>, { - UnpinChatMessage::new(self.ctx(), chat_id) + UnpinChatMessage::new(self, chat_id) + } + + pub fn answer_callback_query<S>( + &self, + callback_query_id: S, + ) -> AnswerCallbackQuery + where + S: Into<String>, + { + AnswerCallbackQuery::new(self, callback_query_id) + } + + pub fn delete_chat_sticker_set<C>(&self, chat_id: C) -> DeleteChatStickerSet + where + C: Into<ChatId>, + { + DeleteChatStickerSet::new(self, chat_id) + } + + pub fn set_chat_sticker_set<C, S>( + &self, + chat_id: C, + sticker_set_name: S, + ) -> SetChatStickerSet + where + C: Into<ChatId>, + S: Into<String>, + { + SetChatStickerSet::new(self, chat_id, sticker_set_name) + } + + pub fn get_chat_member<C, I>(&self, chat_id: C, user_id: I) -> GetChatMember + where + C: Into<ChatId>, + I: Into<i32>, + { + GetChatMember::new(self, chat_id, user_id) + } + + pub fn get_chat_administrators<C, I>( + &self, + chat_id: C, + ) -> GetChatAdministrators + where + C: Into<ChatId>, + { + GetChatAdministrators::new(self, chat_id) + } + + pub fn get_chat_members_count<C>(&self, chat_id: C) -> GetChatMembersCount + where + C: Into<ChatId>, + { + GetChatMembersCount::new(self, chat_id) + } + + pub fn send_video<C, V>(&self, chat_id: C, video: V) -> SendVideo + where + C: Into<ChatId>, + V: Into<InputFile>, + { + SendVideo::new(self, chat_id, video) + } + + pub fn send_document<C, D>(&self, chat_id: C, document: D) -> SendDocument + where + C: Into<ChatId>, + D: Into<InputFile>, + { + SendDocument::new(self, chat_id, document) + } + + pub fn send_animation<C, S>( + &self, + chat_id: C, + animation: S, + ) -> SendAnimation + where + C: Into<ChatId>, + S: Into<InputFile>, + { + SendAnimation::new(self, chat_id, animation) + } + + pub fn set_chat_title<C, T>(&self, chat_id: C, title: T) -> SetChatTitle + where + C: Into<ChatId>, + T: Into<String>, + { + SetChatTitle::new(self, chat_id, title) + } + + pub fn delete_chat_photo<C>(&self, chat_id: C) -> DeleteChatPhoto + where + C: Into<ChatId>, + { + DeleteChatPhoto::new(self, chat_id) + } + + pub fn leave_chat<C>(&self, chat_id: C) -> LeaveChat + where + C: Into<ChatId>, + { + LeaveChat::new(self, chat_id) + } + + pub fn set_chat_photo<C, P>(&self, chat_id: C, photo: P) -> SetChatPhoto + where + C: Into<ChatId>, + P: Into<InputFile>, + { + SetChatPhoto::new(self, chat_id, photo) + } + + pub fn export_chat_invite_link<C>(&self, chat_id: C) -> ExportCharInviteLink + where + C: Into<ChatId>, + { + ExportCharInviteLink::new(self, chat_id) + } + + pub fn set_chat_permissions<C, CP>( + &self, + chat_id: C, + permissions: CP, + ) -> SetChatPermissions + where + C: Into<ChatId>, + CP: Into<ChatPermissions>, + { + SetChatPermissions::new(self, chat_id, permissions) } } diff --git a/src/bot/download.rs b/src/bot/download.rs index d711f151..abb2da78 100644 --- a/src/bot/download.rs +++ b/src/bot/download.rs @@ -8,7 +8,7 @@ use crate::network::download_file_stream; use crate::{bot::Bot, network::download_file, DownloadError}; impl Bot { - /// Download file from telegram into `destination`. + /// Download a file from Telegram into `destination`. /// `path` can be obtained from [`get_file`] method. /// /// For downloading as Stream of Chunks see [`download_file_stream`]. @@ -16,11 +16,10 @@ impl Bot { /// ## Examples /// /// ```no_run - /// use async_telegram_bot::{ - /// bot::Bot, requests::Request, types::File as TgFile, - /// }; + /// use telebofr::types::File as TgFile; /// use tokio::fs::File; - /// # use async_telegram_bot::RequestError; + /// # use telebofr::RequestError; + /// use telebofr::Bot; /// /// # async fn run() -> Result<(), Box<dyn std::error::Error>> { /// let bot = Bot::new("TOKEN"); @@ -44,9 +43,9 @@ impl Bot { download_file(&self.client, &self.token, path, destination).await } - /// Download file from telegram. + /// Download a file from Telegram. /// - /// `path` can be obtained from [`get_file`] method. + /// `path` can be obtained from the [`get_file`] method. /// /// For downloading into [`AsyncWrite`] (e.g. [`tokio::fs::File`]) /// see [`download_file`]. diff --git a/src/bot/mod.rs b/src/bot/mod.rs index 34020623..f2b45f51 100644 --- a/src/bot/mod.rs +++ b/src/bot/mod.rs @@ -1,39 +1,45 @@ -//! A Telegram bot. - use reqwest::Client; -use crate::requests::RequestContext; - mod api; mod download; +/// A Telegram bot used to build requests. +#[derive(Debug, Clone)] pub struct Bot { token: String, client: Client, } -/// Constructors impl Bot { - pub fn new(token: &str) -> Self { + pub fn new<S>(token: S) -> Self + where + S: Into<String>, + { Bot { - token: String::from(token), + token: token.into(), client: Client::new(), } } - pub fn with_client(token: &str, client: Client) -> Self { + pub fn with_client<S>(token: S, client: Client) -> Self + where + S: Into<String>, + { Bot { - token: String::from(token), + token: token.into(), client, } } } impl Bot { - fn ctx(&self) -> RequestContext { - RequestContext { - token: &self.token, - client: &self.client, - } + #[inline] + pub fn token(&self) -> &str { + &self.token + } + + #[inline] + pub fn client(&self) -> &Client { + &self.client } } diff --git a/src/dispatcher/mod.rs b/src/dispatcher/mod.rs deleted file mode 100644 index b957411a..00000000 --- a/src/dispatcher/mod.rs +++ /dev/null @@ -1,9 +0,0 @@ -//! Update dispatching. - -pub mod filter; -pub mod handler; -pub mod simple; -pub mod updater; - -pub use filter::Filter; -pub use handler::Handler; diff --git a/src/dispatcher/simple/mod.rs b/src/dispatcher/simple/mod.rs deleted file mode 100644 index 63ce3a28..00000000 --- a/src/dispatcher/simple/mod.rs +++ /dev/null @@ -1,304 +0,0 @@ -pub mod error_policy; - -use crate::{ - dispatcher::{ - filter::Filter, - handler::Handler, - updater::Updater, - }, - types::{ - Update, - Message, - UpdateKind, - CallbackQuery, - ChosenInlineResult, - }, -}; - -use futures::StreamExt; -use crate::dispatcher::simple::error_policy::ErrorPolicy; - - -type Handlers<'a, T, E> = Vec<(Box<dyn Filter<T> + 'a>, Box<dyn Handler<'a, T, E> + 'a>)>; - -/// Dispatcher that dispatches updates from telegram. -/// -/// This is 'simple' implementation with following limitations: -/// - Error (`E` generic parameter) _must_ implement [`std::fmt::Debug`] -/// - All 'handlers' are boxed -/// - Handler's fututres are also boxed -/// - [Custom error policy] is also boxed -/// - All errors from [updater] are ignored (TODO: remove this limitation) -/// - All handlers executed in order (this means that in dispatcher have -/// 2 upadtes it will first execute some handler into complition with -/// first update and **then** search for handler for second update, -/// this is probably wrong) -/// -/// ## Examples -/// -/// Simplest example: -/// ```no_run -/// # async fn run() { -/// use std::convert::Infallible; -/// use async_telegram_bot::{ -/// bot::Bot, -/// types::Message, -/// dispatcher::{ -/// updater::polling, -/// simple::{Dispatcher, error_policy::ErrorPolicy}, -/// }, -/// }; -/// -/// async fn handle_edited_message(mes: Message) { -/// println!("Edited message: {:?}", mes) -/// } -/// -/// let bot = Bot::new("TOKEN"); -/// -/// // create dispatcher which handlers can't fail -/// // with error policy that just ignores all errors (that can't ever happen) -/// let mut dp = Dispatcher::<Infallible>::new(ErrorPolicy::Ignore) -/// // Add 'handler' that will handle all messages sent to the bot -/// .message_handler(true, |mes: Message| async move { -/// println!("New message: {:?}", mes) -/// }) -/// // Add 'handler' that will handle all -/// // messages edited in chat with the bot -/// .edited_message_handler(true, handle_edited_message); -/// -/// // Start dispatching updates from long polling -/// dp.dispatch(polling(&bot)).await; -/// # } -/// ``` -/// -/// [`std::fmt::Debug`]: std::fmt::Debug -/// [Custom error policy]: crate::dispatcher::simple::error_policy::ErrorPolicy::Custom -/// [updater]: crate::dispatcher::updater -pub struct Dispatcher<'a, E> { - message_handlers: Handlers<'a, Message, E>, - edited_message_handlers: Handlers<'a, Message, E>, - channel_post_handlers: Handlers<'a, Message, E>, - edited_channel_post_handlers: Handlers<'a, Message, E>, - inline_query_handlers: Handlers<'a, (), E>, - chosen_inline_result_handlers: Handlers<'a, ChosenInlineResult, E>, - callback_query_handlers: Handlers<'a, CallbackQuery, E>, - error_policy: ErrorPolicy<'a, E>, -} - -impl<'a, E> Dispatcher<'a, E> -where - E: std::fmt::Debug, // TODO: Is this really necessary? -{ - pub fn new(error_policy: ErrorPolicy<'a, E>) -> Self { - Dispatcher { - 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(), - error_policy - } - } - - pub fn message_handler<F, H>(mut self, filter: F, handler: H) -> Self - where - F: Filter<Message> + 'a, - H: Handler<'a, Message, E> + '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<'a, Message, E> + '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<'a, Message, E> + '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<'a, Message, E> + '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<()> + 'a, - H: Handler<'a, (), E> + '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<'a, ChosenInlineResult, E> + '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<'a, CallbackQuery, E> + 'a, - { - self.callback_query_handlers.push((Box::new(filter), Box::new(handler))); - self - } - - // TODO: Can someone simplify this? - pub async fn dispatch<U, UE>(&mut self, updates: U) - where - U: Updater<UE> + 'a - { - updates.for_each(|res| { - async { - let res = res; - let Update { kind, id } = match res { - Ok(upd) => upd, - _ => return // TODO: proper error handling - }; - - log::debug!("Handled update#{id:?}: {kind:?}", id = id, kind = kind); - - // TODO: can someone extract this to a function? - macro_rules! call { - ($h:expr, $value:expr) => {{ - let value = $value; - let handler = $h.iter().find_map(|e| { - let (filter, handler) = e; - if filter.test(&value) { - Some(handler) - } else { - None - } - }); - - match handler { - Some(handler) => { - if let Err(err) = handler.handle(value).await { - self.error_policy.handle_error(err).await; - } - }, - None => log::warn!("Unhandled update: {:?}", value) - } - }}; - } - - match kind { - UpdateKind::Message(mes) => call!(self.message_handlers, mes), - UpdateKind::EditedMessage(mes) => call!(self.edited_message_handlers, mes), - UpdateKind::ChannelPost(post) => call!(self.channel_post_handlers, post), - UpdateKind::EditedChannelPost(post) => call!(self.edited_channel_post_handlers, post), - UpdateKind::InlineQuery(query) => call!(self.inline_query_handlers, query), - UpdateKind::ChosenInlineResult(result) => call!(self.chosen_inline_result_handlers, result), - UpdateKind::CallbackQuery(callback) => call!(self.callback_query_handlers, callback), - } - } - }) - .await; - } -} - - -#[cfg(test)] -mod tests { - use std::convert::Infallible; - use std::sync::atomic::{AtomicI32, Ordering}; - - use crate::{ - types::{ - Message, ChatKind, MessageKind, Sender, ForwardKind, MediaKind, Chat, User, Update, UpdateKind - }, - dispatcher::{simple::{Dispatcher, error_policy::ErrorPolicy}, updater::StreamUpdater}, - }; - use futures::Stream; - - #[tokio::test] - async fn first_handler_executes_1_time() { - let counter = &AtomicI32::new(0); - let counter2 = &AtomicI32::new(0); - - let mut dp = Dispatcher::<Infallible>::new(ErrorPolicy::Ignore) - .message_handler(true, |_mes: Message| async move { - counter.fetch_add(1, Ordering::SeqCst); - }) - .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: 1567898953, - chat: Chat { - id: 218485655, - 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: 457569668, - 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() -> StreamUpdater<impl Stream<Item=Result<Update, Infallible>>> { - use futures::future::ready; - use futures::stream; - - StreamUpdater::new( - stream::once(ready(Ok(message_update()))) - ) - } -} diff --git a/src/dispatcher/simple/error_policy.rs b/src/dispatching/dispatchers/filter/error_policy.rs similarity index 77% rename from src/dispatcher/simple/error_policy.rs rename to src/dispatching/dispatchers/filter/error_policy.rs index d71b254b..8e922c63 100644 --- a/src/dispatcher/simple/error_policy.rs +++ b/src/dispatching/dispatchers/filter/error_policy.rs @@ -1,6 +1,4 @@ -use std::pin::Pin; -use std::future::Future; -use std::fmt::Debug; +use std::{fmt::Debug, future::Future, pin::Pin}; // TODO: shouldn't it be trait? pub enum ErrorPolicy<'a, E> { @@ -15,14 +13,12 @@ where { pub async fn handle_error(&self, error: E) { match self { - Self::Ignore => {}, + Self::Ignore => {} Self::Log => { // TODO: better message log::error!("Error in handler: {:?}", error) } - Self::Custom(func) => { - func(error).await - } + Self::Custom(func) => func(error).await, } } @@ -33,4 +29,4 @@ where { Self::Custom(Box::new(move |e| Box::pin(f(e)))) } -} \ No newline at end of file +} diff --git a/src/dispatching/dispatchers/filter/mod.rs b/src/dispatching/dispatchers/filter/mod.rs new file mode 100644 index 00000000..b3ed259f --- /dev/null +++ b/src/dispatching/dispatchers/filter/mod.rs @@ -0,0 +1,373 @@ +use futures::StreamExt; + +use async_trait::async_trait; + +use crate::{ + dispatching::{ + dispatchers::filter::error_policy::ErrorPolicy, filters::Filter, + handler::Handler, updater::Updater, Dispatcher, + }, + types::{CallbackQuery, ChosenInlineResult, Message, Update, UpdateKind}, +}; + +pub mod error_policy; + +type Handlers<'a, T, E> = + Vec<(Box<dyn Filter<T> + 'a>, Box<dyn Handler<'a, T, E> + 'a>)>; + +/// Dispatcher that dispatches updates from telegram. +/// +/// This is 'filter' implementation with following limitations: +/// - Error (`E` generic parameter) _must_ implement [`std::fmt::Debug`] +/// - All 'handlers' are boxed +/// - Handler's fututres are also boxed +/// - [Custom error policy] is also boxed +/// - All errors from [updater] are ignored (TODO: remove this limitation) +/// - All handlers executed in order (this means that in dispatching have 2 +/// upadtes it will first execute some handler into complition with first +/// update and **then** search for handler for second update, this is probably +/// wrong) +/// +/// ## Examples +/// +/// Simplest example: +/// ```no_run +/// # use telebofr::Bot; +/// use telebofr::types::Message; +/// async fn run() { +/// use std::convert::Infallible; +/// use telebofr::{ +/// dispatching::{ +/// dispatchers::filter::{error_policy::ErrorPolicy, FilterDispatcher}, +/// updater::polling, +/// }, +/// }; +/// +/// async fn handle_edited_message(mes: Message) { +/// println!("Edited message: {:?}", mes) +/// } +/// +/// let bot = Bot::new("TOKEN"); +/// +/// // create dispatching which handlers can't fail +/// // with error policy that just ignores all errors (that can't ever happen) +/// let mut dp = FilterDispatcher::<Infallible>::new(ErrorPolicy::Ignore) +/// // Add 'handler' that will handle all messages sent to the bot +/// .message_handler(true, |mes: Message| { +/// async move { println!("New message: {:?}", mes) } +/// }) +/// // Add 'handler' that will handle all +/// // messages edited in chat with the bot +/// .edited_message_handler(true, handle_edited_message); +/// +/// // Start dispatching updates from long polling +/// dp.dispatch(polling(&bot)).await; +/// # } +/// ``` +/// +/// [`std::fmt::Debug`]: std::fmt::Debug +/// [Custom error policy]: +/// crate::dispatching::filter::error_policy::ErrorPolicy::Custom [updater]: +/// crate::dispatching::updater +pub struct FilterDispatcher<'a, E> { + message_handlers: Handlers<'a, Message, E>, + edited_message_handlers: Handlers<'a, Message, E>, + channel_post_handlers: Handlers<'a, Message, E>, + edited_channel_post_handlers: Handlers<'a, Message, E>, + inline_query_handlers: Handlers<'a, (), E>, + chosen_inline_result_handlers: Handlers<'a, ChosenInlineResult, E>, + callback_query_handlers: Handlers<'a, CallbackQuery, E>, + error_policy: ErrorPolicy<'a, E>, +} + +impl<'a, E> FilterDispatcher<'a, E> +where + E: std::fmt::Debug, // TODO: Is this really necessary? +{ + pub fn new(error_policy: ErrorPolicy<'a, E>) -> 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(), + error_policy, + } + } + + pub fn message_handler<F, H>(mut self, filter: F, handler: H) -> Self + where + F: Filter<Message> + 'a, + H: Handler<'a, Message, E> + '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<'a, Message, E> + '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<'a, Message, E> + '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<'a, Message, E> + '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<()> + 'a, + H: Handler<'a, (), E> + '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<'a, ChosenInlineResult, E> + '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<'a, CallbackQuery, E> + 'a, + { + self.callback_query_handlers + .push((Box::new(filter), Box::new(handler))); + self + } + + // TODO: Can someone simplify this? + pub async fn dispatch<U>(&mut self, updates: U) + where + U: Updater + 'a, + { + updates + .for_each(|res| { + async { + let res = res; + let Update { kind, id } = match res { + Ok(upd) => upd, + _ => return, // TODO: proper error handling + }; + + log::debug!( + "Handled update#{id:?}: {kind:?}", + id = id, + kind = kind + ); + + match kind { + 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; + } + } + } + }) + .await; + } + + async fn handle<T>(&self, update: T, handlers: &Handlers<'a, T, E>) + where + T: std::fmt::Debug, + { + let handler = handlers.iter().find_map(|e| { + let (filter, handler) = e; + if filter.test(&update) { + Some(handler) + } else { + None + } + }); + + match handler { + Some(handler) => { + if let Err(err) = handler.handle(update).await { + self.error_policy.handle_error(err).await + } + } + None => { + log::warn!("unhandled update {:?}", update); + } + } + } +} + +#[async_trait(? Send)] +impl<'a, U, E> Dispatcher<'a, U> for FilterDispatcher<'a, E> +where + E: std::fmt::Debug, + U: Updater + 'a, +{ + async fn dispatch(&'a mut self, updater: U) { + FilterDispatcher::dispatch(self, updater).await + } +} + +#[cfg(test)] +mod tests { + use std::{ + convert::Infallible, + sync::atomic::{AtomicI32, Ordering}, + }; + + use futures::Stream; + + use crate::{ + dispatching::{ + dispatchers::filter::{ + error_policy::ErrorPolicy, FilterDispatcher, + }, + updater::StreamUpdater, + }, + 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(ErrorPolicy::Ignore) + .message_handler(true, |_mes: Message| { + async move { + counter.fetch_add(1, Ordering::SeqCst); + } + }) + .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: 1567898953, + chat: Chat { + id: 218485655, + 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: 457569668, + 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( + ) -> StreamUpdater<impl Stream<Item = Result<Update, Infallible>>> { + use futures::{future::ready, stream}; + + StreamUpdater::new(stream::once(ready(Ok(message_update())))) + } +} diff --git a/src/dispatching/dispatchers/mod.rs b/src/dispatching/dispatchers/mod.rs new file mode 100644 index 00000000..1ec549e1 --- /dev/null +++ b/src/dispatching/dispatchers/mod.rs @@ -0,0 +1,3 @@ +pub use filter::FilterDispatcher; + +pub mod filter; diff --git a/src/dispatching/filters/command.rs b/src/dispatching/filters/command.rs new file mode 100644 index 00000000..b807050a --- /dev/null +++ b/src/dispatching/filters/command.rs @@ -0,0 +1,110 @@ +use crate::{dispatching::Filter, types::Message}; + +pub struct CommandFilter { + command: String, +} + +impl Filter<Message> for CommandFilter { + fn test(&self, value: &Message) -> bool { + match value.text() { + Some(text) => match text.split_whitespace().next() { + Some(command) => self.command == command, + None => false, + }, + None => false, + } + } +} + +impl CommandFilter { + pub fn new<T>(command: T) -> Self + where + T: Into<String>, + { + Self { + command: '/'.to_string() + &command.into(), + } + } + pub fn with_prefix<T>(command: T, prefix: T) -> Self + where + T: Into<String>, + { + Self { + command: prefix.into() + &command.into(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::types::{ + Chat, ChatKind, ForwardKind, MediaKind, MessageKind, Sender, User, + }; + + #[test] + fn commands_are_equal() { + let filter = CommandFilter::new("command".to_string()); + let message = create_message_with_text("/command".to_string()); + assert!(filter.test(&message)); + } + + #[test] + fn commands_are_not_equal() { + let filter = CommandFilter::new("command".to_string()); + let message = + create_message_with_text("/not_equal_command".to_string()); + assert_eq!(filter.test(&message), false); + } + + #[test] + fn command_have_args() { + let filter = CommandFilter::new("command".to_string()); + let message = + create_message_with_text("/command arg1 arg2".to_string()); + assert!(filter.test(&message)); + } + + #[test] + fn message_have_only_whitespace() { + let filter = CommandFilter::new("command".to_string()); + let message = create_message_with_text(" ".to_string()); + assert_eq!(filter.test(&message), false); + } + + fn create_message_with_text(text: String) -> 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, + entities: vec![], + }, + reply_markup: None, + }, + } + } +} diff --git a/src/dispatcher/filter.rs b/src/dispatching/filters/main.rs similarity index 80% rename from src/dispatcher/filter.rs rename to src/dispatching/filters/main.rs index e1c267ad..8c513405 100644 --- a/src/dispatcher/filter.rs +++ b/src/dispatching/filters/main.rs @@ -6,7 +6,7 @@ pub trait Filter<T> { } /// ``` -/// use async_telegram_bot::dispatcher::filter::Filter; +/// use telebofr::dispatching::filters::Filter; /// /// let closure = |i: &i32| -> bool { *i >= 42 }; /// assert!(closure.test(&42)); @@ -22,13 +22,15 @@ impl<T, F: Fn(&T) -> bool> Filter<T> for F { } /// ``` -/// use async_telegram_bot::dispatcher::filter::Filter; +/// use telebofr::dispatching::filters::Filter; /// /// assert!(true.test(&())); /// assert_eq!(false.test(&()), false); /// ``` impl<T> Filter<T> for bool { - fn test(&self, _: &T) -> bool { *self } + fn test(&self, _: &T) -> bool { + *self + } } /// And filter. @@ -40,7 +42,7 @@ impl<T> Filter<T> for bool { /// /// ## Examples /// ``` -/// use async_telegram_bot::dispatcher::filter::{And, Filter}; +/// use telebofr::dispatching::filters::{And, Filter}; /// /// // Note: bool can be treated as `Filter` that always return self. /// assert_eq!(And::new(true, false).test(&()), false); @@ -71,18 +73,17 @@ where /// /// ## Examples /// ``` -/// use async_telegram_bot::dispatcher::filter::{and, Filter}; +/// use telebofr::dispatching::filters::{and, Filter}; /// /// assert!(and(true, true).test(&())); /// assert_eq!(and(true, false).test(&()), false); /// ``` /// -/// [`And::new`]: crate::dispatcher::filter::And::new +/// [`And::new`]: crate::dispatching::filter::And::new pub fn and<A, B>(a: A, b: B) -> And<A, B> { And::new(a, b) } - /// Or filter. /// /// Passes if at least one underlying filters passes. @@ -92,7 +93,7 @@ pub fn and<A, B>(a: A, b: B) -> And<A, B> { /// /// ## Examples /// ``` -/// use async_telegram_bot::dispatcher::filter::{Or, Filter}; +/// use telebofr::dispatching::filters::{Filter, Or}; /// /// // Note: bool can be treated as `Filter` that always return self. /// assert!(Or::new(true, false).test(&())); @@ -123,25 +124,24 @@ where /// /// ## Examples /// ``` -/// use async_telegram_bot::dispatcher::filter::{or, Filter}; +/// use telebofr::dispatching::filters::{or, Filter}; /// /// assert!(or(true, false).test(&())); /// assert_eq!(or(false, false).test(&()), false); /// ``` /// -/// [`Or::new`]: crate::dispatcher::filter::Or::new +/// [`Or::new`]: crate::dispatching::filter::Or::new pub fn or<A, B>(a: A, b: B) -> Or<A, B> { Or::new(a, b) } - /// Not filter. /// /// Passes if underlying filter don't pass. /// /// ## Examples /// ``` -/// use async_telegram_bot::dispatcher::filter::{Not, Filter}; +/// use telebofr::dispatching::filters::{Filter, Not}; /// /// // Note: bool can be treated as `Filter` that always return self. /// assert!(Not::new(false).test(&())); @@ -169,13 +169,13 @@ where /// /// ## Examples /// ``` -/// use async_telegram_bot::dispatcher::filter::{not, Filter}; +/// use telebofr::dispatching::filters::{not, Filter}; /// /// assert!(not(false).test(&())); /// assert_eq!(not(true).test(&()), false); /// ``` /// -/// [`Not::new`]: crate::dispatcher::filter::Not::new +/// [`Not::new`]: crate::dispatching::filter::Not::new pub fn not<A>(a: A) -> Not<A> { Not::new(a) } @@ -187,7 +187,7 @@ pub fn not<A>(a: A) -> Not<A> { /// /// ## Examples /// ``` -/// use async_telegram_bot::{all, dispatcher::filter::Filter}; +/// use telebofr::{all, dispatching::filters::Filter}; /// /// assert!(all![true].test(&())); /// assert!(all![true, true].test(&())); @@ -199,12 +199,12 @@ pub fn not<A>(a: A) -> Not<A> { /// assert_eq!(all![false, false].test(&()), false); /// ``` /// -/// [filter]: crate::dispatcher::filter::Filter +/// [filter]: crate::dispatching::filter::Filter #[macro_export] macro_rules! all { ($one:expr) => { $one }; ($head:expr, $($tail:tt)+) => { - $crate::dispatcher::filter::And::new( + $crate::dispatching::filters::And::new( $head, $crate::all!($($tail)+) ) @@ -218,7 +218,7 @@ macro_rules! all { /// /// ## Examples /// ``` -/// use async_telegram_bot::{any, dispatcher::filter::Filter}; +/// use telebofr::{any, dispatching::filters::Filter}; /// /// assert!(any![true].test(&())); /// assert!(any![true, true].test(&())); @@ -230,24 +230,23 @@ macro_rules! all { /// assert_eq!(any![false, false, false].test(&()), false); /// ``` /// -/// [filter]: crate::dispatcher::filter::Filter +/// [filter]: crate::dispatching::filter::Filter #[macro_export] macro_rules! any { ($one:expr) => { $one }; ($head:expr, $($tail:tt)+) => { - $crate::dispatcher::filter::Or::new( + $crate::dispatching::filters::Or::new( $head, $crate::all!($($tail)+) ) }; } - /// Simple wrapper around `Filter` that adds `|` and `&` operators. /// /// ## Examples /// ``` -/// use async_telegram_bot::dispatcher::filter::{Filter, f, F, And, Or}; +/// use telebofr::dispatching::filters::{f, And, Filter, Or, F}; /// /// let flt1 = |i: &i32| -> bool { *i > 17 }; /// let flt2 = |i: &i32| -> bool { *i < 42 }; @@ -259,7 +258,6 @@ macro_rules! any { /// assert_eq!(and.test(&50), false); // `flt2` doesn't pass /// assert_eq!(and.test(&16), false); // `flt1` doesn't pass /// -/// /// let or = f(flt1) | flt3; /// assert!(or.test(&19)); // `flt1` passes /// assert!(or.test(&16)); // `flt2` passes @@ -267,9 +265,8 @@ macro_rules! any { /// /// assert_eq!(or.test(&17), false); // both don't pass /// -/// /// // Note: only first filter in chain should be wrapped in `f(...)` -/// let complicated: F<Or<And<_, _>, _>>= f(flt1) & flt2 | flt3; +/// let complicated: F<Or<And<_, _>, _>> = f(flt1) & flt2 | flt3; /// assert!(complicated.test(&2)); // `flt3` passes /// assert!(complicated.test(&21)); // `flt1` and `flt2` pass /// @@ -280,14 +277,14 @@ pub struct F<A>(A); /// Constructor fn for [F] /// -/// [F]: crate::dispatcher::filter::F; +/// [F]: crate::dispatching::filter::F; pub fn f<A>(a: A) -> F<A> { F(a) } impl<T, A> Filter<T> for F<A> where - A: Filter<T> + A: Filter<T>, { fn test(&self, value: &T) -> bool { self.0.test(value) @@ -310,13 +307,14 @@ impl<A, B> std::ops::BitOr<B> for F<A> { } } +/* workaround for `E0207` compiler error */ /// Extensions for filters -pub trait FilterExt<T /* workaround for `E0207` compiler error */> { +pub trait FilterExt<T> { /// Alias for [`Not::new`] /// /// ## Examples /// ``` - /// use async_telegram_bot::dispatcher::filter::{Filter, FilterExt}; + /// use telebofr::dispatching::filters::{Filter, FilterExt}; /// /// let flt = |i: &i32| -> bool { *i > 0 }; /// let flt = flt.not(); @@ -324,8 +322,11 @@ pub trait FilterExt<T /* workaround for `E0207` compiler error */> { /// assert_eq!(flt.test(&1), false); /// ``` /// - /// [`Not::new`]: crate::dispatcher::filter::Not::new - fn not(self) -> Not<Self> where Self: Sized { + /// [`Not::new`]: crate::dispatching::filter::Not::new + fn not(self) -> Not<Self> + where + Self: Sized, + { Not::new(self) } @@ -333,7 +334,7 @@ pub trait FilterExt<T /* workaround for `E0207` compiler error */> { /// /// ## Examples /// ``` - /// use async_telegram_bot::dispatcher::filter::{Filter, FilterExt}; + /// use telebofr::dispatching::filters::{Filter, FilterExt}; /// /// let flt = |i: &i32| -> bool { *i > 0 }; /// let flt = flt.and(|i: &i32| *i < 42); @@ -343,8 +344,11 @@ pub trait FilterExt<T /* workaround for `E0207` compiler error */> { /// assert_eq!(flt.test(&43), false); /// ``` /// - /// [`Not::new`]: crate::dispatcher::filter::And::new - fn and<B>(self, other: B) -> And<Self, B> where Self: Sized { + /// [`Not::new`]: crate::dispatching::filter::And::new + fn and<B>(self, other: B) -> And<Self, B> + where + Self: Sized, + { And::new(self, other) } @@ -352,7 +356,7 @@ pub trait FilterExt<T /* workaround for `E0207` compiler error */> { /// /// ## Examples /// ``` - /// use async_telegram_bot::dispatcher::filter::{Filter, FilterExt}; + /// use telebofr::dispatching::filters::{Filter, FilterExt}; /// /// let flt = |i: &i32| -> bool { *i < 0 }; /// let flt = flt.or(|i: &i32| *i > 42); @@ -362,8 +366,11 @@ pub trait FilterExt<T /* workaround for `E0207` compiler error */> { /// assert_eq!(flt.test(&17), false); /// ``` /// - /// [`Not::new`]: crate::dispatcher::filter::Or::new - fn or<B>(self, other: B) -> Or<Self, B> where Self: Sized { + /// [`Not::new`]: crate::dispatching::filter::Or::new + fn or<B>(self, other: B) -> Or<Self, B> + where + Self: Sized, + { Or::new(self, other) } } diff --git a/src/dispatching/filters/message_caption.rs b/src/dispatching/filters/message_caption.rs new file mode 100644 index 00000000..0539390e --- /dev/null +++ b/src/dispatching/filters/message_caption.rs @@ -0,0 +1,100 @@ +use crate::{dispatching::Filter, types::Message}; + +/// Filter which compare caption of media with another text. +/// Returns true if the caption of media is equal to another text, otherwise +/// false. +/// +/// NOTE: filter compares only caption of media, does not compare text of +/// message! +/// +/// If you want to compare text of message use +/// [MessageTextFilter] +/// +/// If you want to compare text and caption use +/// [MessageTextCaptionFilter] +/// +/// [MessageTextFilter]: telebofr::dispatching::filters::MessageTextFilter +/// [MessageTextCaptionFilter]: +/// telebofr::dispatching::filters::MessageTextCaptionFilter +pub struct MessageCaptionFilter { + text: String, +} + +impl Filter<Message> for MessageCaptionFilter { + fn test(&self, value: &Message) -> bool { + match value.caption() { + Some(caption) => self.text == caption, + None => false, + } + } +} + +impl MessageCaptionFilter { + pub fn new<T>(text: T) -> Self + where + T: Into<String>, + { + Self { text: text.into() } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::types::{ + Chat, ChatKind, ForwardKind, MediaKind, MessageKind, Sender, User, + }; + + #[test] + fn captions_are_equal() { + let filter = MessageCaptionFilter::new("caption".to_string()); + let message = create_message_with_caption("caption".to_string()); + assert!(filter.test(&message)); + } + + #[test] + fn captions_are_not_equal() { + let filter = MessageCaptionFilter::new("caption".to_string()); + let message = + create_message_with_caption("not equal caption".to_string()); + assert_eq!(filter.test(&message), false); + } + + fn create_message_with_caption(caption: String) -> 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::Photo { + photo: vec![], + caption: Some(caption), + caption_entities: vec![], + media_group_id: None, + }, + reply_markup: None, + }, + } + } +} diff --git a/src/dispatching/filters/message_text.rs b/src/dispatching/filters/message_text.rs new file mode 100644 index 00000000..28db8206 --- /dev/null +++ b/src/dispatching/filters/message_text.rs @@ -0,0 +1,95 @@ +use crate::{dispatching::Filter, types::Message}; + +/// Filter which compare message text with another text. +/// Returns true if the message text is equal to another text, otherwise false. +/// +/// NOTE: filter compares only text message, does not compare caption of media! +/// +/// If you want to compare caption use +/// [MessageCaptionFilter] +/// +/// If you want to compare text and caption use +/// [MessageTextCaptionFilter] +/// +/// [MessageCaptionFilter]: telebofr::dispatching::filters::MessageCaptionFilter +/// [MessageTextCaptionFilter]: +/// telebofr::dispatching::filters::MessageTextCaptionFilter +pub struct MessageTextFilter { + text: String, +} + +impl Filter<Message> for MessageTextFilter { + fn test(&self, value: &Message) -> bool { + match value.text() { + Some(text) => self.text == text, + None => false, + } + } +} + +impl MessageTextFilter { + pub fn new<T>(text: T) -> Self + where + T: Into<String>, + { + Self { text: text.into() } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::types::{ + Chat, ChatKind, ForwardKind, MediaKind, MessageKind, Sender, User, + }; + + #[test] + fn texts_are_equal() { + let filter = MessageTextFilter::new("text"); + let message = create_message_with_text("text".to_string()); + assert!(filter.test(&message)); + } + + #[test] + fn texts_are_not_equal() { + let filter = MessageTextFilter::new("text"); + let message = create_message_with_text("not equal text".to_string()); + assert_eq!(filter.test(&message), false); + } + + fn create_message_with_text(text: String) -> 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, + entities: vec![], + }, + reply_markup: None, + }, + } + } +} diff --git a/src/dispatching/filters/message_text_caption.rs b/src/dispatching/filters/message_text_caption.rs new file mode 100644 index 00000000..5ee0df1f --- /dev/null +++ b/src/dispatching/filters/message_text_caption.rs @@ -0,0 +1,152 @@ +use crate::{dispatching::Filter, types::Message}; + +/// Filter which compare message text or caption of media with another text. +/// Returns true if the message text or caption of media is equal to another +/// text, otherwise false. +/// +/// NOTE: filter compares text of message or if it is not exists, compares +/// caption of the message! +/// +/// If you want to compare only caption use +/// [MessageCaptionFilter] +/// +/// If you want to compare only text use +/// [MessageTextFilter] +/// +/// [MessageCaptionFilter]: telebofr::dispatching::filters::MessageCaptionFilter +/// [MessageTextFilter]: telebofr::filter::filters::MessageTextFilter +pub struct MessageTextCaptionFilter { + text: String, +} + +impl Filter<Message> for MessageTextCaptionFilter { + fn test(&self, value: &Message) -> bool { + match value.text() { + Some(text) => self.text == text, + None => match value.caption() { + Some(caption) => self.text == caption, + None => false, + }, + } + } +} + +impl MessageTextCaptionFilter { + pub fn new<T>(text: T) -> Self + where + T: Into<String>, + { + Self { text: text.into() } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::types::{ + Chat, ChatKind, ForwardKind, MediaKind, MessageKind, Sender, User, + }; + + #[test] + fn texts_are_equal() { + let filter = MessageTextCaptionFilter::new("text"); + let message = create_message_with_text("text".to_string()); + assert!(filter.test(&message)); + } + + #[test] + fn texts_are_not_equal() { + let filter = MessageTextCaptionFilter::new("text"); + let message = create_message_with_text("not equal text".to_string()); + assert_eq!(filter.test(&message), false); + } + + fn create_message_with_text(text: String) -> 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, + entities: vec![], + }, + reply_markup: None, + }, + } + } + + #[test] + fn captions_are_equal() { + let filter = MessageTextCaptionFilter::new("caption".to_string()); + let message = create_message_with_caption("caption".to_string()); + assert!(filter.test(&message)); + } + + #[test] + fn captions_are_not_equal() { + let filter = MessageTextCaptionFilter::new("caption".to_string()); + let message = + create_message_with_caption("not equal caption".to_string()); + assert_eq!(filter.test(&message), false); + } + + fn create_message_with_caption(caption: String) -> 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::Photo { + photo: vec![], + caption: Some(caption), + caption_entities: vec![], + media_group_id: None, + }, + reply_markup: None, + }, + } + } +} diff --git a/src/dispatching/filters/mod.rs b/src/dispatching/filters/mod.rs new file mode 100644 index 00000000..b3f9237f --- /dev/null +++ b/src/dispatching/filters/mod.rs @@ -0,0 +1,13 @@ +pub use main::*; + +pub use command::*; +pub use message_caption::*; +pub use message_text::*; +pub use message_text_caption::*; + +mod main; + +mod command; +mod message_caption; +mod message_text; +mod message_text_caption; diff --git a/src/dispatcher/handler.rs b/src/dispatching/handler.rs similarity index 72% rename from src/dispatcher/handler.rs rename to src/dispatching/handler.rs index 7c52928b..9ce74580 100644 --- a/src/dispatcher/handler.rs +++ b/src/dispatching/handler.rs @@ -1,12 +1,15 @@ +use std::{future::Future, pin::Pin}; + use futures::FutureExt; -use std::future::Future; -use std::pin::Pin; pub type HandlerResult<E> = Result<(), E>; /// Asynchronous handler for event `T` (like `&self, I -> Future` fn) pub trait Handler<'a, T, E> { - fn handle(&self, value: T) -> Pin<Box<dyn Future<Output = HandlerResult<E>> + 'a>>; + fn handle( + &self, + value: T, + ) -> Pin<Box<dyn Future<Output = HandlerResult<E>> + 'a>>; } pub trait IntoHandlerResult<E> { @@ -32,7 +35,10 @@ where R: IntoHandlerResult<E> + 'a, E: 'a, { - fn handle(&self, value: T) -> Pin<Box<dyn Future<Output = HandlerResult<E>> + 'a>> { + fn handle( + &self, + value: T, + ) -> Pin<Box<dyn Future<Output = HandlerResult<E>> + 'a>> { Box::pin(self(value).map(IntoHandlerResult::into_hr)) } } diff --git a/src/dispatching/mod.rs b/src/dispatching/mod.rs new file mode 100644 index 00000000..c239778f --- /dev/null +++ b/src/dispatching/mod.rs @@ -0,0 +1,15 @@ +//! Update dispatching. + +use async_trait::async_trait; +pub use filters::Filter; +pub use handler::Handler; + +pub mod dispatchers; +pub mod filters; +pub mod handler; +pub mod updater; + +#[async_trait(? Send)] +pub trait Dispatcher<'a, U> { + async fn dispatch(&'a mut self, updater: U); +} diff --git a/src/dispatcher/updater.rs b/src/dispatching/updater.rs similarity index 75% rename from src/dispatcher/updater.rs rename to src/dispatching/updater.rs index 8dd14da1..47d131ec 100644 --- a/src/dispatcher/updater.rs +++ b/src/dispatching/updater.rs @@ -3,19 +3,17 @@ use std::{ task::{Context, Poll}, }; -use pin_project::pin_project; -use futures::{Stream, StreamExt, stream}; +use futures::{stream, Stream, StreamExt}; -use crate::{ - bot::Bot, - types::Update, - RequestError, -}; +use pin_project::pin_project; + +use crate::{bot::Bot, types::Update, RequestError}; // Currently just a placeholder, but I'll add here some methods /// Updater is stream of updates. /// -/// Telegram supports 2 ways of [getting updates]: [long polling](Long Polling) and webhook +/// Telegram supports 2 ways of [getting updates]: [long polling](Long Polling) +/// and webhook /// /// ## Long Polling /// @@ -99,12 +97,16 @@ use crate::{ /// [GetUpdates]: crate::requests::GetUpdates /// [getting updates]: https://core.telegram.org/bots/api#getting-updates /// [wiki]: https://en.wikipedia.org/wiki/Push_technology#Long_polling -pub trait Updater<E>: Stream<Item=Result<Update, E>> {} +pub trait Updater: + Stream<Item = Result<Update, <Self as Updater>::Error>> +{ + type Error; +} #[pin_project] pub struct StreamUpdater<S> { #[pin] - stream: S + stream: S, } impl<S> StreamUpdater<S> { @@ -113,35 +115,49 @@ impl<S> StreamUpdater<S> { } } -impl<S, E> Stream for StreamUpdater<S> where S: Stream<Item=Result<Update, E>> { +impl<S, E> Stream for StreamUpdater<S> +where + S: Stream<Item = Result<Update, E>>, +{ type Item = Result<Update, E>; - fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> { + fn poll_next( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll<Option<Self::Item>> { self.project().stream.poll_next(cx) } } -impl<S, E> Updater<E> for StreamUpdater<S> where S: Stream<Item=Result<Update, E>> {} +impl<S, E> Updater for StreamUpdater<S> +where + S: Stream<Item = Result<Update, E>>, +{ + type Error = E; +} -pub fn polling<'a>(bot: &'a Bot) -> impl Updater<RequestError> + 'a { - let stream = stream::unfold((bot, 0), |(bot, mut offset)| async move { - // this match converts Result<Vec<_>, _> -> Vec<Result<_, _>> - let updates = match bot.get_updates().offset(offset).send().await { - Ok(updates) => { - if let Some(upd) = updates.last() { - offset = upd.id + 1; +pub fn polling<'a>(bot: &'a Bot) -> impl Updater<Error = RequestError> + 'a { + let stream = stream::unfold((bot, 0), |(bot, mut offset)| { + async move { + // this match converts Result<Vec<_>, _> -> Vec<Result<_, _>> + let updates = match bot.get_updates().offset(offset).send().await { + Ok(updates) => { + if let Some(upd) = updates.last() { + offset = upd.id + 1; + } + updates.into_iter().map(Ok).collect::<Vec<_>>() } - updates.into_iter().map(|u| Ok(u)).collect::<Vec<_>>() - }, - Err(err) => vec![Err(err)] - }; - Some((stream::iter(updates), (bot, offset))) + Err(err) => vec![Err(err)], + }; + Some((stream::iter(updates), (bot, offset))) + } }) - .flatten(); + .flatten(); StreamUpdater { stream } } // TODO implement webhook (this actually require webserver and probably we // should add cargo feature that adds webhook) -//pub fn webhook<'a>(bot: &'a Bot, cfg: WebhookConfig) -> Updater<impl Stream<Item=Result<Update, ???>> + 'a> {} +//pub fn webhook<'a>(bot: &'a cfg: WebhookConfig) -> Updater<impl +// Stream<Item=Result<Update, ???>> + 'a> {} diff --git a/src/lib.rs b/src/lib.rs index 8d244ff6..838a211a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,12 +5,13 @@ extern crate serde; #[macro_use] extern crate thiserror; +pub use bot::Bot; pub use errors::{DownloadError, RequestError}; mod errors; mod network; -pub mod bot; -pub mod dispatcher; +mod bot; +pub mod dispatching; pub mod requests; pub mod types; diff --git a/src/network/request.rs b/src/network/request.rs index 0222fad2..40bb8b2d 100644 --- a/src/network/request.rs +++ b/src/network/request.rs @@ -11,8 +11,8 @@ pub async fn request_multipart<T>( method_name: &str, params: Form, ) -> ResponseResult<T> - where - T: DeserializeOwned, +where + T: DeserializeOwned, { process_response( client @@ -30,8 +30,8 @@ pub async fn request_simple<T>( token: &str, method_name: &str, ) -> ResponseResult<T> - where - T: DeserializeOwned, +where + T: DeserializeOwned, { process_response( client diff --git a/src/network/telegram_response.rs b/src/network/telegram_response.rs index 05e7acb7..82185c0c 100644 --- a/src/network/telegram_response.rs +++ b/src/network/telegram_response.rs @@ -1,7 +1,9 @@ use reqwest::StatusCode; use crate::{ - requests::ResponseResult, types::ResponseParameters, RequestError, + requests::ResponseResult, + types::{False, ResponseParameters, True}, + RequestError, }; #[derive(Deserialize)] @@ -10,14 +12,14 @@ pub enum TelegramResponse<R> { Ok { /// A dummy field. Used only for deserialization. #[allow(dead_code)] - ok: bool, // TODO: True type + ok: True, result: R, }, Err { /// A dummy field. Used only for deserialization. #[allow(dead_code)] - ok: bool, // TODO: False type + ok: False, description: String, error_code: u16, diff --git a/src/requests/answer_callback_query.rs b/src/requests/answer_callback_query.rs new file mode 100644 index 00000000..6bc42024 --- /dev/null +++ b/src/requests/answer_callback_query.rs @@ -0,0 +1,125 @@ +use async_trait::async_trait; + +use crate::{ + bot::Bot, + network, + requests::{Request, ResponseResult}, + types::True, +}; + +/// Use this method to send answers to callback queries sent from inline +/// keyboards. The answer will be displayed to the user as a notification at the +/// top of the chat screen or as an alert. On success, True is returned. +/// +/// Alternatively, the user can be redirected to the specified Game URL. For +/// this option to work, you must first create a game for your bot via +/// @Botfather and accept the terms. Otherwise, you may use links like +/// t.me/your_bot?start=XXXX that open your bot with a parameter. +#[derive(Debug, Clone, Serialize)] +pub struct AnswerCallbackQuery<'a> { + #[serde(skip_serializing)] + bot: &'a Bot, + + /// Unique identifier for the query to be answered. + callback_query_id: String, + + /// Text of the notification. If not specified, nothing will be shown to + /// the user, 0-200 characters + #[serde(skip_serializing_if = "Option::is_none")] + text: Option<String>, + + /// If true, an alert will be shown by the client instead of a notification + /// at the top of the chat screen. Defaults to false. + #[serde(skip_serializing_if = "Option::is_none")] + show_alert: Option<bool>, + + /// URL that will be opened by the user's client. If you have created a + /// Game and accepted the conditions via @Botfather, specify the URL that + /// opens your game – note that this will only work if the query comes from + /// a callback_game button. + #[serde(skip_serializing_if = "Option::is_none")] + url: Option<String>, + + /// The maximum amount of time in seconds that the result of the callback + /// query may be cached client-side. Telegram apps will support caching + /// starting in version 3.14. Defaults to 0. + #[serde(skip_serializing_if = "Option::is_none")] + cache_time: Option<i32>, +} + +#[async_trait] +impl Request for AnswerCallbackQuery<'_> { + type Output = True; + + async fn send_boxed(self) -> ResponseResult<Self::Output> { + self.send().await + } +} + +impl AnswerCallbackQuery<'_> { + pub async fn send(self) -> ResponseResult<True> { + network::request_json( + self.bot.client(), + self.bot.token(), + "answerCallbackQuery", + &self, + ) + .await + } +} + +impl<'a> AnswerCallbackQuery<'a> { + pub(crate) fn new<S>(bot: &'a Bot, callback_query_id: S) -> Self + where + S: Into<String>, + { + Self { + bot, + callback_query_id: callback_query_id.into(), + text: None, + show_alert: None, + url: None, + cache_time: None, + } + } + + pub fn callback_query_id<S>(mut self, value: S) -> Self + where + S: Into<String>, + { + self.callback_query_id = value.into(); + self + } + + pub fn text<S>(mut self, value: S) -> Self + where + S: Into<String>, + { + self.text = Some(value.into()); + self + } + + pub fn show_alert<B>(mut self, value: B) -> Self + where + B: Into<bool>, + { + self.show_alert = Some(value.into()); + self + } + + pub fn url<S>(mut self, value: S) -> Self + where + S: Into<String>, + { + self.url = Some(value.into()); + self + } + + pub fn cache_time<I>(mut self, value: I) -> Self + where + I: Into<i32>, + { + self.cache_time = Some(value.into()); + self + } +} diff --git a/src/requests/answer_pre_checkout_query.rs b/src/requests/answer_pre_checkout_query.rs index b7f5bfe1..e4842ef9 100644 --- a/src/requests/answer_pre_checkout_query.rs +++ b/src/requests/answer_pre_checkout_query.rs @@ -1,8 +1,9 @@ use async_trait::async_trait; use crate::{ + bot::Bot, network, - requests::{Request, RequestContext, ResponseResult}, + requests::{Request, ResponseResult}, types::True, }; @@ -16,7 +17,7 @@ use crate::{ /// [`Update`]: crate::types::Update pub struct AnswerPreCheckoutQuery<'a> { #[serde(skip_serializing)] - ctx: RequestContext<'a>, + bot: &'a Bot, /// Unique identifier for the query to be answered pub pre_checkout_query_id: String, @@ -48,8 +49,8 @@ impl Request for AnswerPreCheckoutQuery<'_> { impl AnswerPreCheckoutQuery<'_> { pub async fn send(self) -> ResponseResult<True> { network::request_json( - &self.ctx.client, - &self.ctx.token, + self.bot.client(), + self.bot.token(), "answerPreCheckoutQuery", &self, ) @@ -59,7 +60,7 @@ impl AnswerPreCheckoutQuery<'_> { impl<'a> AnswerPreCheckoutQuery<'a> { pub(crate) fn new<S, B>( - ctx: RequestContext<'a>, + bot: &'a Bot, pre_checkout_query_id: S, ok: B, ) -> Self @@ -68,7 +69,7 @@ impl<'a> AnswerPreCheckoutQuery<'a> { B: Into<bool>, { Self { - ctx, + bot, pre_checkout_query_id: pre_checkout_query_id.into(), ok: ok.into(), error_message: None, diff --git a/src/requests/answer_shipping_query.rs b/src/requests/answer_shipping_query.rs index b7a25069..90b59fa9 100644 --- a/src/requests/answer_shipping_query.rs +++ b/src/requests/answer_shipping_query.rs @@ -1,8 +1,9 @@ use async_trait::async_trait; use crate::{ + bot::Bot, network, - requests::{Request, RequestContext, ResponseResult}, + requests::{Request, ResponseResult}, types::{ShippingOption, True}, }; @@ -15,7 +16,7 @@ use crate::{ /// [`Update`]: crate::types::Update pub struct AnswerShippingQuery<'a> { #[serde(skip_serializing)] - ctx: RequestContext<'a>, + bot: &'a Bot, /// Unique identifier for the query to be answered pub shipping_query_id: String, @@ -49,8 +50,8 @@ impl Request for AnswerShippingQuery<'_> { impl AnswerShippingQuery<'_> { pub async fn send(self) -> ResponseResult<True> { network::request_json( - &self.ctx.client, - &self.ctx.token, + self.bot.client(), + self.bot.token(), "answerShippingQuery", &self, ) @@ -59,17 +60,13 @@ impl AnswerShippingQuery<'_> { } impl<'a> AnswerShippingQuery<'a> { - pub(crate) fn new<S, B>( - ctx: RequestContext<'a>, - shipping_query_id: S, - ok: B, - ) -> Self + pub(crate) fn new<S, B>(bot: &'a Bot, shipping_query_id: S, ok: B) -> Self where S: Into<String>, B: Into<bool>, { Self { - ctx, + bot, shipping_query_id: shipping_query_id.into(), ok: ok.into(), shipping_options: None, diff --git a/src/requests/delete_chat_photo.rs b/src/requests/delete_chat_photo.rs new file mode 100644 index 00000000..f674d4f9 --- /dev/null +++ b/src/requests/delete_chat_photo.rs @@ -0,0 +1,73 @@ +use async_trait::async_trait; + +use crate::{ + bot::Bot, + network, + requests::{Request, ResponseResult}, + types::{ChatId, True}, +}; + +#[derive(Debug, Clone, Serialize)] +pub struct DeleteChatPhoto<'a> { + #[serde(skip_serializing)] + bot: &'a Bot, + + chat_id: ChatId, +} + +#[async_trait] +impl Request for DeleteChatPhoto<'_> { + type Output = True; + + async fn send_boxed(self) -> ResponseResult<Self::Output> { + self.send().await + } +} + +impl DeleteChatPhoto<'_> { + async fn send(self) -> ResponseResult<True> { + network::request_json( + self.bot.client(), + self.bot.token(), + "deleteChatPhoto", + &self, + ) + .await + } +} + +impl<'a> DeleteChatPhoto<'a> { + pub(crate) fn new<C>(bot: &'a Bot, chat_id: C) -> Self + where + C: Into<ChatId>, + { + Self { + bot, + chat_id: chat_id.into(), + } + } + + pub fn chat_id<C>(mut self, chat_id: C) -> Self + where + C: Into<ChatId>, + { + self.chat_id = chat_id.into(); + self + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn serialize() { + let bot = Bot::new("token"); + let chat_id = 123; + let method = DeleteChatPhoto::new(&bot, chat_id); + + let expected = r#"{"chat_id":123}"#; + let actual = serde_json::to_string::<DeleteChatPhoto>(&method).unwrap(); + assert_eq!(actual, expected); + } +} diff --git a/src/requests/delete_chat_sticker_set.rs b/src/requests/delete_chat_sticker_set.rs new file mode 100644 index 00000000..63a5260f --- /dev/null +++ b/src/requests/delete_chat_sticker_set.rs @@ -0,0 +1,64 @@ +use async_trait::async_trait; + +use crate::{ + bot::Bot, + network, + requests::{Request, ResponseResult}, + types::{ChatId, True}, +}; + +/// Use this method to delete a group sticker set from a supergroup. The bot +/// must be an administrator in the chat for this to work and must have the +/// appropriate admin rights. Use the field can_set_sticker_set optionally +/// returned in getChat requests to check if the bot can use this method. +/// Returns True on success. +#[derive(Debug, Clone, Serialize)] +pub struct DeleteChatStickerSet<'a> { + #[serde(skip_serializing)] + bot: &'a Bot, + + /// Unique identifier for the target chat or username of the target + /// supergroup (in the format @supergroupusername) + chat_id: ChatId, +} + +#[async_trait] +impl Request for DeleteChatStickerSet<'_> { + type Output = True; + + async fn send_boxed(self) -> ResponseResult<Self::Output> { + self.send().await + } +} + +impl DeleteChatStickerSet<'_> { + async fn send(&self) -> ResponseResult<True> { + network::request_json( + self.bot.client(), + self.bot.token(), + "deleteChatStickerSet", + &self, + ) + .await + } +} + +impl<'a> DeleteChatStickerSet<'a> { + pub(crate) fn new<C>(bot: &'a Bot, chat_id: C) -> Self + where + C: Into<ChatId>, + { + Self { + bot, + chat_id: chat_id.into(), + } + } + + pub fn chat_id<C>(mut self, value: C) -> Self + where + C: Into<ChatId>, + { + self.chat_id = value.into(); + self + } +} diff --git a/src/requests/edit_message_live_location.rs b/src/requests/edit_message_live_location.rs index c4c0fc4e..8ac0e77a 100644 --- a/src/requests/edit_message_live_location.rs +++ b/src/requests/edit_message_live_location.rs @@ -1,8 +1,9 @@ use async_trait::async_trait; use crate::{ + bot::Bot, network, - requests::{Request, RequestContext, ResponseResult}, + requests::{Request, ResponseResult}, types::{ChatId, Message, ReplyMarkup}, }; @@ -17,7 +18,7 @@ use crate::{ /// [`Message`]: crate::types::Message pub struct EditMessageLiveLocation<'a> { #[serde(skip_serializing)] - ctx: RequestContext<'a>, + bot: &'a Bot, #[serde(skip_serializing_if = "Option::is_none")] /// Required if inline_message_id is not specified. Unique identifier for @@ -53,8 +54,8 @@ impl Request for EditMessageLiveLocation<'_> { impl EditMessageLiveLocation<'_> { pub async fn send(self) -> ResponseResult<Message> { network::request_json( - &self.ctx.client, - &self.ctx.token, + self.bot.client(), + self.bot.token(), "editMessageLiveLocation", &self, ) @@ -63,17 +64,13 @@ impl EditMessageLiveLocation<'_> { } impl<'a> EditMessageLiveLocation<'a> { - pub(crate) fn new<Lt, Lg>( - ctx: RequestContext<'a>, - latitude: Lt, - longitude: Lg, - ) -> Self + pub(crate) fn new<Lt, Lg>(bot: &'a Bot, latitude: Lt, longitude: Lg) -> Self where Lt: Into<f64>, Lg: Into<f64>, { Self { - ctx, + bot, chat_id: None, message_id: None, inline_message_id: None, diff --git a/src/requests/export_chat_invite_link.rs b/src/requests/export_chat_invite_link.rs new file mode 100644 index 00000000..dfb809b0 --- /dev/null +++ b/src/requests/export_chat_invite_link.rs @@ -0,0 +1,74 @@ +use async_trait::async_trait; + +use crate::{ + bot::Bot, + network, + requests::{Request, ResponseResult}, + types::ChatId, +}; + +#[derive(Debug, Clone, Serialize)] +pub struct ExportCharInviteLink<'a> { + #[serde(skip_serializing)] + bot: &'a Bot, + + chat_id: ChatId, +} + +#[async_trait] +impl Request for ExportCharInviteLink<'_> { + type Output = String; + + async fn send_boxed(self) -> ResponseResult<Self::Output> { + self.send().await + } +} + +impl ExportCharInviteLink<'_> { + async fn send(self) -> ResponseResult<String> { + network::request_json( + self.bot.client(), + self.bot.token(), + "exportChatInviteLink", + &self, + ) + .await + } +} + +impl<'a> ExportCharInviteLink<'a> { + pub(crate) fn new<C>(bot: &'a Bot, chat_id: C) -> Self + where + C: Into<ChatId>, + { + Self { + bot, + chat_id: chat_id.into(), + } + } + + pub fn chat_id<C>(mut self, chat_id: C) -> Self + where + C: Into<ChatId>, + { + self.chat_id = chat_id.into(); + self + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn serialize() { + let bot = Bot::new("token"); + let chat_id = 123; + let method = ExportCharInviteLink::new(&bot, chat_id); + + let expected = r#"{"chat_id":123}"#; + let actual = + serde_json::to_string::<ExportCharInviteLink>(&method).unwrap(); + assert_eq!(actual, expected); + } +} diff --git a/src/requests/form_builder.rs b/src/requests/form_builder.rs index 0d292683..bc355eab 100644 --- a/src/requests/form_builder.rs +++ b/src/requests/form_builder.rs @@ -79,7 +79,10 @@ macro_rules! impl_for_struct { impl_for_struct!(bool, i32, i64); -impl<T> IntoFormValue for Option<T> where T: IntoFormValue { +impl<T> IntoFormValue for Option<T> +where + T: IntoFormValue, +{ fn into_form_value(self) -> Option<FormValue> { self.and_then(IntoFormValue::into_form_value) } @@ -87,8 +90,8 @@ impl<T> IntoFormValue for Option<T> where T: IntoFormValue { impl IntoFormValue for &[InputMedia] { fn into_form_value(self) -> Option<FormValue> { - let json = serde_json::to_string(self) - .expect("serde_json::to_string failed"); + let json = + serde_json::to_string(self).expect("serde_json::to_string failed"); Some(FormValue::Str(json)) } } @@ -113,7 +116,7 @@ impl IntoFormValue for ChatId { fn into_form_value(self) -> Option<FormValue> { let string = match self { ChatId::Id(id) => id.to_string(), - ChatId::ChannelUsername(username) => username.clone(), + ChatId::ChannelUsername(username) => username, }; Some(FormValue::Str(string)) } @@ -121,7 +124,7 @@ impl IntoFormValue for ChatId { impl IntoFormValue for String { fn into_form_value(self) -> Option<FormValue> { - Some(FormValue::Str(self.to_owned())) + Some(FormValue::Str(self)) } } diff --git a/src/requests/forward_message.rs b/src/requests/forward_message.rs index 9c16ae67..b99166b0 100644 --- a/src/requests/forward_message.rs +++ b/src/requests/forward_message.rs @@ -1,8 +1,9 @@ use async_trait::async_trait; use crate::{ + bot::Bot, network, - requests::{Request, RequestContext, ResponseResult}, + requests::{Request, ResponseResult}, types::{ChatId, Message}, }; @@ -11,7 +12,7 @@ use crate::{ /// [`Message`] is returned. pub struct ForwardMessage<'a> { #[serde(skip_serializing)] - ctx: RequestContext<'a>, + bot: &'a Bot, /// Unique identifier for the target chat or username of the target channel /// (in the format @channelusername) @@ -40,8 +41,8 @@ impl Request for ForwardMessage<'_> { impl ForwardMessage<'_> { pub async fn send(self) -> ResponseResult<Message> { network::request_json( - self.ctx.client, - self.ctx.token, + self.bot.client(), + self.bot.token(), "forwardMessage", &self, ) @@ -51,7 +52,7 @@ impl ForwardMessage<'_> { impl<'a> ForwardMessage<'a> { pub(crate) fn new<C, Fc, M>( - ctx: RequestContext<'a>, + bot: &'a Bot, chat_id: C, from_chat_id: Fc, message_id: M, @@ -62,7 +63,7 @@ impl<'a> ForwardMessage<'a> { M: Into<i32>, { Self { - ctx, + bot, chat_id: chat_id.into(), from_chat_id: from_chat_id.into(), message_id: message_id.into(), diff --git a/src/requests/get_chat.rs b/src/requests/get_chat.rs index 7135f226..6a8bc711 100644 --- a/src/requests/get_chat.rs +++ b/src/requests/get_chat.rs @@ -1,8 +1,9 @@ use async_trait::async_trait; use crate::{ + bot::Bot, network, - requests::{Request, RequestContext, ResponseResult}, + requests::{Request, ResponseResult}, types::{Chat, ChatId}, }; @@ -13,7 +14,7 @@ use crate::{ #[derive(Debug, Clone, Serialize)] pub struct GetChat<'a> { #[serde(skip_serializing)] - ctx: RequestContext<'a>, + bot: &'a Bot, /// Unique identifier for the target chat or username /// of the target supergroup or channel (in the format @channelusername) chat_id: ChatId, @@ -31,8 +32,8 @@ impl Request for GetChat<'_> { impl GetChat<'_> { pub async fn send(self) -> ResponseResult<Chat> { network::request_json( - &self.ctx.client, - &self.ctx.token, + self.bot.client(), + self.bot.token(), "getChat", &self, ) @@ -41,11 +42,21 @@ impl GetChat<'_> { } impl<'a> GetChat<'a> { - pub fn chat_id<C>(mut self, value: C) -> Self + pub(crate) fn new<F>(bot: &'a Bot, chat_id: F) -> Self + where + F: Into<ChatId>, + { + Self { + bot, + chat_id: chat_id.into(), + } + } + + pub fn chat_id<C>(mut self, chat_id: C) -> Self where C: Into<ChatId>, { - self.chat_id = value.into(); + self.chat_id = chat_id.into(); self } } diff --git a/src/requests/get_chat_administrators.rs b/src/requests/get_chat_administrators.rs new file mode 100644 index 00000000..ba35a99c --- /dev/null +++ b/src/requests/get_chat_administrators.rs @@ -0,0 +1,64 @@ +use async_trait::async_trait; + +use crate::{ + bot::Bot, + network, + requests::{Request, ResponseResult}, + types::{ChatId, ChatMember}, +}; + +/// Use this method to get a list of administrators in a chat. On success, +/// returns an Array of ChatMember objects that contains information about all +/// chat administrators except other bots. If the chat is a group or a +/// supergroup and no administrators were appointed, only the creator will be +/// returned +#[derive(Debug, Clone, Serialize)] +pub struct GetChatAdministrators<'a> { + #[serde(skip_serializing)] + bot: &'a Bot, + + /// Unique identifier for the target chat or username of the target + /// supergroup or channel (in the format @channelusername) + chat_id: ChatId, +} + +#[async_trait] +impl Request for GetChatAdministrators<'_> { + type Output = Vec<ChatMember>; + + async fn send_boxed(self) -> ResponseResult<Self::Output> { + self.send().await + } +} + +impl GetChatAdministrators<'_> { + async fn send(&self) -> ResponseResult<Vec<ChatMember>> { + network::request_json( + self.bot.client(), + self.bot.token(), + "getChatAdministrators", + &self, + ) + .await + } +} + +impl<'a> GetChatAdministrators<'a> { + pub(crate) fn new<C>(bot: &'a Bot, chat_id: C) -> Self + where + C: Into<ChatId>, + { + Self { + bot, + chat_id: chat_id.into(), + } + } + + pub fn chat_id<C>(mut self, value: C) -> Self + where + C: Into<ChatId>, + { + self.chat_id = value.into(); + self + } +} diff --git a/src/requests/get_chat_member.rs b/src/requests/get_chat_member.rs new file mode 100644 index 00000000..3a03fd12 --- /dev/null +++ b/src/requests/get_chat_member.rs @@ -0,0 +1,74 @@ +use async_trait::async_trait; + +use crate::{ + bot::Bot, + network, + requests::{Request, ResponseResult}, + types::{ChatId, ChatMember}, +}; + +/// Use this method to get information about a member of a chat. Returns a +/// ChatMember object on success. +#[derive(Debug, Clone, Serialize)] +pub struct GetChatMember<'a> { + #[serde(skip_serializing)] + bot: &'a Bot, + + /// Unique identifier for the target chat or username of the target + /// supergroup or channel (in the format @channelusername) + chat_id: ChatId, + + /// Unique identifier of the target user + user_id: i32, +} + +#[async_trait] +impl Request for GetChatMember<'_> { + type Output = ChatMember; + + async fn send_boxed(self) -> ResponseResult<Self::Output> { + self.send().await + } +} + +impl GetChatMember<'_> { + async fn send(&self) -> ResponseResult<ChatMember> { + network::request_json( + self.bot.client(), + self.bot.token(), + "getChatMember", + &self, + ) + .await + } +} + +impl<'a> GetChatMember<'a> { + pub(crate) fn new<C, I>(bot: &'a Bot, chat_id: C, user_id: I) -> Self + where + C: Into<ChatId>, + I: Into<i32>, + { + Self { + bot, + chat_id: chat_id.into(), + user_id: user_id.into(), + } + } + + pub fn chat_id<C>(mut self, value: C) -> Self + where + C: Into<ChatId>, + { + self.chat_id = value.into(); + self + } + + pub fn user_id<I>(mut self, value: I) -> Self + where + I: Into<i32>, + { + self.user_id = value.into(); + self + } +} diff --git a/src/requests/get_chat_members_count.rs b/src/requests/get_chat_members_count.rs new file mode 100644 index 00000000..ae224c59 --- /dev/null +++ b/src/requests/get_chat_members_count.rs @@ -0,0 +1,61 @@ +use async_trait::async_trait; + +use crate::{ + bot::Bot, + network, + requests::{Request, ResponseResult}, + types::{Chat, ChatId}, +}; + +/// Use this method to get the number of members in a chat. Returns Int on +/// success. +#[derive(Debug, Clone, Serialize)] +pub struct GetChatMembersCount<'a> { + #[serde(skip_serializing)] + bot: &'a Bot, + + /// Unique identifier for the target chat or username + /// of the target supergroup or channel (in the format @channelusername) + chat_id: ChatId, +} + +#[async_trait] +impl Request for GetChatMembersCount<'_> { + type Output = Chat; + + async fn send_boxed(self) -> ResponseResult<Self::Output> { + self.send().await + } +} + +impl GetChatMembersCount<'_> { + pub async fn send(self) -> ResponseResult<Chat> { + network::request_json( + self.bot.client(), + self.bot.token(), + "getChatMembersCount", + &self, + ) + .await + } +} + +impl<'a> GetChatMembersCount<'a> { + pub fn new<C>(bot: &'a Bot, chat_id: C) -> Self + where + C: Into<ChatId>, + { + Self { + bot, + chat_id: chat_id.into(), + } + } + + pub fn chat_id<C>(mut self, value: C) -> Self + where + C: Into<ChatId>, + { + self.chat_id = value.into(); + self + } +} diff --git a/src/requests/get_file.rs b/src/requests/get_file.rs index 59785b17..43cb809d 100644 --- a/src/requests/get_file.rs +++ b/src/requests/get_file.rs @@ -1,8 +1,9 @@ use async_trait::async_trait; use crate::{ + bot::Bot, network, - requests::{Request, RequestContext, ResponseResult}, + requests::{Request, ResponseResult}, types::File, }; @@ -16,7 +17,7 @@ use crate::{ #[derive(Debug, Clone, Serialize)] pub struct GetFile<'a> { #[serde(skip_serializing)] - ctx: RequestContext<'a>, + bot: &'a Bot, /// File identifier to get info about pub file_id: String, } @@ -33,8 +34,8 @@ impl Request for GetFile<'_> { impl GetFile<'_> { pub async fn send(self) -> ResponseResult<File> { network::request_json( - &self.ctx.client, - &self.ctx.token, + self.bot.client(), + self.bot.token(), "getFile", &self, ) @@ -43,12 +44,12 @@ impl GetFile<'_> { } impl<'a> GetFile<'a> { - pub(crate) fn new<F>(ctx: RequestContext<'a>, value: F) -> Self + pub(crate) fn new<F>(bot: &'a Bot, value: F) -> Self where F: Into<String>, { Self { - ctx, + bot, file_id: value.into(), } } diff --git a/src/requests/get_me.rs b/src/requests/get_me.rs index 20cfa3ef..6efe9543 100644 --- a/src/requests/get_me.rs +++ b/src/requests/get_me.rs @@ -1,16 +1,17 @@ use async_trait::async_trait; use crate::{ + bot::Bot, network, - requests::{Request, RequestContext, ResponseResult}, + requests::{Request, ResponseResult}, types::User, }; #[derive(Debug, Clone)] -/// A simple method for testing your bot's auth token. Requires no parameters. +/// A filter method for testing your bot's auth token. Requires no parameters. /// Returns basic information about the bot in form of a [`User`] object. pub struct GetMe<'a> { - ctx: RequestContext<'a>, + bot: &'a Bot, } #[async_trait] @@ -24,12 +25,13 @@ impl Request for GetMe<'_> { impl GetMe<'_> { pub async fn send(self) -> ResponseResult<User> { - network::request_simple(self.ctx.client, self.ctx.token, "getMe").await + network::request_simple(self.bot.client(), self.bot.token(), "getMe") + .await } } impl<'a> GetMe<'a> { - pub(crate) fn new(ctx: RequestContext<'a>) -> Self { - GetMe { ctx } + pub(crate) fn new(bot: &'a Bot) -> Self { + GetMe { bot } } } diff --git a/src/requests/get_updates.rs b/src/requests/get_updates.rs index 176a0e91..8e25a784 100644 --- a/src/requests/get_updates.rs +++ b/src/requests/get_updates.rs @@ -1,15 +1,16 @@ use async_trait::async_trait; use crate::{ + bot::Bot, network, - requests::{Request, RequestContext, ResponseResult}, + requests::{Request, ResponseResult}, types::Update, }; #[derive(Debug, Clone, Serialize)] pub struct GetUpdates<'a> { #[serde(skip_serializing)] - ctx: RequestContext<'a>, + bot: &'a Bot, pub offset: Option<i32>, pub limit: Option<u8>, @@ -41,8 +42,8 @@ impl Request for GetUpdates<'_> { impl GetUpdates<'_> { pub async fn send(self) -> ResponseResult<Vec<Update>> { network::request_json( - &self.ctx.client, - &self.ctx.token, + self.bot.client(), + self.bot.token(), "getUpdates", &self, ) @@ -51,9 +52,9 @@ impl GetUpdates<'_> { } impl<'a> GetUpdates<'a> { - pub(crate) fn new(ctx: RequestContext<'a>) -> Self { + pub(crate) fn new(bot: &'a Bot) -> Self { Self { - ctx, + bot, offset: None, limit: None, timeout: None, diff --git a/src/requests/get_user_profile_photos.rs b/src/requests/get_user_profile_photos.rs index 6044441d..88ccaf5a 100644 --- a/src/requests/get_user_profile_photos.rs +++ b/src/requests/get_user_profile_photos.rs @@ -1,8 +1,9 @@ use async_trait::async_trait; use crate::{ + bot::Bot, network, - requests::{Request, RequestContext, ResponseResult}, + requests::{Request, ResponseResult}, types::UserProfilePhotos, }; @@ -11,7 +12,7 @@ use crate::{ #[derive(Debug, Clone, Serialize)] pub struct GetUserProfilePhotos<'a> { #[serde(skip_serializing)] - ctx: RequestContext<'a>, + bot: &'a Bot, /// Unique identifier of the target user pub user_id: i32, /// Sequential number of the first photo to be returned. By default, all @@ -36,8 +37,8 @@ impl Request for GetUserProfilePhotos<'_> { impl GetUserProfilePhotos<'_> { async fn send(self) -> ResponseResult<UserProfilePhotos> { network::request_json( - &self.ctx.client, - &self.ctx.token, + self.bot.client(), + self.bot.token(), "getUserProfilePhotos", &self, ) @@ -46,12 +47,12 @@ impl GetUserProfilePhotos<'_> { } impl<'a> GetUserProfilePhotos<'a> { - pub fn new<U>(ctx: RequestContext<'a>, user_id: U) -> Self + pub fn new<U>(bot: &'a Bot, user_id: U) -> Self where U: Into<i32>, { Self { - ctx, + bot, user_id: user_id.into(), offset: None, limit: None, diff --git a/src/requests/kick_chat_member.rs b/src/requests/kick_chat_member.rs index 9dec2cd6..cdbef073 100644 --- a/src/requests/kick_chat_member.rs +++ b/src/requests/kick_chat_member.rs @@ -1,8 +1,9 @@ use async_trait::async_trait; use crate::{ + bot::Bot, network, - requests::{Request, RequestContext, ResponseResult}, + requests::{Request, ResponseResult}, types::{ChatId, True}, }; @@ -14,7 +15,7 @@ use crate::{ #[derive(Debug, Clone, Serialize)] pub struct KickChatMember<'a> { #[serde(skip_serializing)] - ctx: RequestContext<'a>, + bot: &'a Bot, ///Unique identifier for the target group or username of the target /// supergroup or channel (in the format @channelusername) pub chat_id: ChatId, @@ -39,8 +40,8 @@ impl Request for KickChatMember<'_> { impl KickChatMember<'_> { async fn send(self) -> ResponseResult<True> { network::request_json( - self.ctx.client, - self.ctx.token, + self.bot.client(), + self.bot.token(), "kickChatMember", &self, ) @@ -49,17 +50,13 @@ impl KickChatMember<'_> { } impl<'a> KickChatMember<'a> { - pub(crate) fn new<C, U>( - ctx: RequestContext<'a>, - chat_id: C, - user_id: U, - ) -> Self + pub(crate) fn new<C, U>(bot: &'a Bot, chat_id: C, user_id: U) -> Self where C: Into<ChatId>, U: Into<i32>, { Self { - ctx, + bot, chat_id: chat_id.into(), user_id: user_id.into(), until_date: None, diff --git a/src/requests/leave_chat.rs b/src/requests/leave_chat.rs new file mode 100644 index 00000000..1f52a1f6 --- /dev/null +++ b/src/requests/leave_chat.rs @@ -0,0 +1,59 @@ +use async_trait::async_trait; + +use crate::{ + bot::Bot, + network, + requests::{Request, ResponseResult}, + types::ChatId, +}; + +/// Use this method for your bot to leave a group, supergroup or channel. +/// Returns True on success. +#[derive(Debug, Clone, Serialize)] +pub struct LeaveChat<'a> { + #[serde(skip_serializing)] + bot: &'a Bot, + /// Unique identifier for the target chat or username + /// of the target supergroup or channel (in the format @channelusername) + chat_id: ChatId, +} + +#[async_trait] +impl Request for LeaveChat<'_> { + type Output = bool; + + async fn send_boxed(self) -> ResponseResult<Self::Output> { + self.send().await + } +} + +impl LeaveChat<'_> { + pub async fn send(self) -> ResponseResult<bool> { + network::request_json( + self.bot.client(), + self.bot.token(), + "leaveChat", + &self, + ) + .await + } +} + +impl<'a> LeaveChat<'a> { + pub(crate) fn new<F>(bot: &'a Bot, chat_id: F) -> Self + where + F: Into<ChatId>, + { + Self { + bot, + chat_id: chat_id.into(), + } + } + pub fn chat_id<C>(mut self, chat_id: C) -> Self + where + C: Into<ChatId>, + { + self.chat_id = chat_id.into(); + self + } +} diff --git a/src/requests/mod.rs b/src/requests/mod.rs index 50f7ffaa..e9bbc265 100644 --- a/src/requests/mod.rs +++ b/src/requests/mod.rs @@ -1,45 +1,70 @@ -//! Raw API functions. +//! API requests. -use reqwest::Client; -use serde::de::DeserializeOwned; - -use async_trait::async_trait; - -use crate::RequestError; - -pub use self::{ - answer_pre_checkout_query::AnswerPreCheckoutQuery, - answer_shipping_query::AnswerShippingQuery, - edit_message_live_location::EditMessageLiveLocation, - forward_message::ForwardMessage, get_chat::GetChat, get_file::GetFile, - get_me::GetMe, get_updates::GetUpdates, - get_user_profile_photos::GetUserProfilePhotos, - kick_chat_member::KickChatMember, pin_chat_message::PinChatMessage, - promote_chat_member::PromoteChatMember, - restrict_chat_member::RestrictChatMember, send_animation::SendAnimation, - send_audio::SendAudio, send_chat_action::SendChatAction, - send_contact::SendContact, send_document::SendDocument, - send_location::SendLocation, send_media_group::SendMediaGroup, - send_message::SendMessage, send_photo::SendPhoto, send_poll::SendPoll, - send_venue::SendVenue, send_video::SendVideo, - send_video_note::SendVideoNote, send_voice::SendVoice, - stop_message_live_location::StopMessageLiveLocation, - unban_chat_member::UnbanChatMember, unpin_chat_message::UnpinChatMessage, -}; +pub use answer_callback_query::*; +pub use answer_pre_checkout_query::*; +pub use answer_shipping_query::*; +pub use delete_chat_photo::*; +pub use delete_chat_sticker_set::*; +pub use edit_message_live_location::*; +pub use export_chat_invite_link::*; +pub use forward_message::*; +pub use get_chat::*; +pub use get_chat_administrators::*; +pub use get_chat_member::*; +pub use get_chat_members_count::*; +pub use get_file::*; +pub use get_me::*; +pub use get_updates::*; +pub use get_user_profile_photos::*; +pub use kick_chat_member::*; +pub use leave_chat::*; +pub use pin_chat_message::*; +pub use promote_chat_member::*; +pub use restrict_chat_member::*; +pub use send_animation::*; +pub use send_audio::*; +pub use send_chat_action::*; +pub use send_contact::*; +pub use send_document::*; +pub use send_location::*; +pub use send_media_group::*; +pub use send_message::*; +pub use send_photo::*; +pub use send_poll::*; +pub use send_venue::*; +pub use send_video::*; +pub use send_video_note::*; +pub use send_voice::*; +pub use set_chat_description::*; +pub use set_chat_permissions::*; +pub use set_chat_photo::*; +pub use set_chat_sticker_set::*; +pub use set_chat_title::*; +pub use stop_message_live_location::*; +pub use unban_chat_member::*; +pub use unpin_chat_message::*; mod form_builder; mod utils; +mod answer_callback_query; mod answer_pre_checkout_query; mod answer_shipping_query; +mod delete_chat_photo; +mod delete_chat_sticker_set; mod edit_message_live_location; +mod export_chat_invite_link; mod forward_message; mod get_chat; +mod get_chat_administrators; +mod get_chat_member; +mod get_chat_members_count; mod get_file; mod get_me; mod get_updates; mod get_user_profile_photos; mod kick_chat_member; +mod leave_chat; mod pin_chat_message; mod promote_chat_member; mod restrict_chat_member; @@ -57,12 +82,20 @@ mod send_venue; mod send_video; mod send_video_note; mod send_voice; +mod set_chat_description; +mod set_chat_permissions; +mod set_chat_photo; +mod set_chat_sticker_set; +mod set_chat_title; mod stop_message_live_location; mod unban_chat_member; mod unpin_chat_message; +use async_trait::async_trait; +use serde::de::DeserializeOwned; + /// A type that is returned from `Request::send_boxed`. -pub type ResponseResult<T> = Result<T, RequestError>; +pub type ResponseResult<T> = Result<T, crate::RequestError>; /// A request that can be sent to Telegram. #[async_trait] @@ -73,12 +106,3 @@ pub trait Request { /// Send this request. async fn send_boxed(self) -> ResponseResult<Self::Output>; } - -/// A context used to send all the requests. -#[derive(Debug, Clone)] -pub struct RequestContext<'a> { - /// An HTTPS client. - pub client: &'a Client, - /// A token of your bot. - pub token: &'a str, -} diff --git a/src/requests/pin_chat_message.rs b/src/requests/pin_chat_message.rs index 9033e4dd..c8567e36 100644 --- a/src/requests/pin_chat_message.rs +++ b/src/requests/pin_chat_message.rs @@ -1,8 +1,9 @@ use async_trait::async_trait; use crate::{ + bot::Bot, network, - requests::{Request, RequestContext, ResponseResult}, + requests::{Request, ResponseResult}, types::{ChatId, True}, }; @@ -13,7 +14,7 @@ use crate::{ #[derive(Debug, Clone, Serialize)] pub struct PinChatMessage<'a> { #[serde(skip_serializing)] - ctx: RequestContext<'a>, + bot: &'a Bot, /// Unique identifier for the target chat or username /// of the target supergroup or channel (in the format @channelusername) pub chat_id: ChatId, @@ -22,17 +23,13 @@ pub struct PinChatMessage<'a> { } impl<'a> PinChatMessage<'a> { - pub(crate) fn new<C, M>( - ctx: RequestContext<'a>, - chat_id: C, - message_id: M, - ) -> Self + pub(crate) fn new<C, M>(bot: &'a Bot, chat_id: C, message_id: M) -> Self where C: Into<ChatId>, M: Into<i32>, { Self { - ctx, + bot, chat_id: chat_id.into(), message_id: message_id.into(), disable_notification: None, @@ -59,8 +56,8 @@ impl Request for PinChatMessage<'_> { impl PinChatMessage<'_> { async fn send(self) -> ResponseResult<True> { network::request_json( - &self.ctx.client, - &self.ctx.token, + self.bot.client(), + self.bot.token(), "pinChatMessage", &self, ) diff --git a/src/requests/promote_chat_member.rs b/src/requests/promote_chat_member.rs index d6c47d9d..2c416233 100644 --- a/src/requests/promote_chat_member.rs +++ b/src/requests/promote_chat_member.rs @@ -1,8 +1,9 @@ use async_trait::async_trait; use crate::{ + bot::Bot, network, - requests::{Request, RequestContext, ResponseResult}, + requests::{Request, ResponseResult}, types::{ChatId, True}, }; @@ -13,7 +14,7 @@ use crate::{ #[derive(Debug, Clone, Serialize)] pub struct PromoteChatMember<'a> { #[serde(skip_serializing)] - ctx: RequestContext<'a>, + bot: &'a Bot, ///Unique identifier for the target chat or username of the target channel /// (in the format @channelusername) pub chat_id: ChatId, @@ -62,8 +63,8 @@ impl Request for PromoteChatMember<'_> { impl PromoteChatMember<'_> { pub async fn send(self) -> ResponseResult<True> { network::request_json( - &self.ctx.client, - &self.ctx.token, + self.bot.client(), + self.bot.token(), "promoteChatMember", &self, ) @@ -72,17 +73,13 @@ impl PromoteChatMember<'_> { } impl<'a> PromoteChatMember<'a> { - pub(crate) fn new<C, U>( - ctx: RequestContext<'a>, - chat_id: C, - user_id: U, - ) -> Self + pub(crate) fn new<C, U>(bot: &'a Bot, chat_id: C, user_id: U) -> Self where C: Into<ChatId>, U: Into<i32>, { Self { - ctx, + bot, chat_id: chat_id.into(), user_id: user_id.into(), can_change_info: None, diff --git a/src/requests/restrict_chat_member.rs b/src/requests/restrict_chat_member.rs index 5243484e..f17b5ba6 100644 --- a/src/requests/restrict_chat_member.rs +++ b/src/requests/restrict_chat_member.rs @@ -1,8 +1,9 @@ use async_trait::async_trait; use crate::{ + bot::Bot, network, - requests::{Request, RequestContext, ResponseResult}, + requests::{Request, ResponseResult}, types::{ChatId, ChatPermissions, True}, }; @@ -13,7 +14,7 @@ use crate::{ #[derive(Debug, Clone, Serialize)] pub struct RestrictChatMember<'a> { #[serde(skip_serializing)] - ctx: RequestContext<'a>, + bot: &'a Bot, ///Unique identifier for the target chat or username of the target /// supergroup (in the format @supergroupusername) pub chat_id: ChatId, @@ -40,8 +41,8 @@ impl Request for RestrictChatMember<'_> { impl RestrictChatMember<'_> { async fn send(self) -> ResponseResult<True> { network::request_json( - &self.ctx.client, - &self.ctx.token, + self.bot.client(), + self.bot.token(), "restrictChatMember", &self, ) @@ -51,7 +52,7 @@ impl RestrictChatMember<'_> { impl<'a> RestrictChatMember<'a> { pub(crate) fn new<C, U, P>( - ctx: RequestContext<'a>, + bot: &'a Bot, chat_id: C, user_id: U, permissions: P, @@ -62,7 +63,7 @@ impl<'a> RestrictChatMember<'a> { P: Into<ChatPermissions>, { Self { - ctx, + bot, chat_id: chat_id.into(), user_id: user_id.into(), permissions: permissions.into(), diff --git a/src/requests/send_animation.rs b/src/requests/send_animation.rs index 96b08299..0bfa8a3e 100644 --- a/src/requests/send_animation.rs +++ b/src/requests/send_animation.rs @@ -1,10 +1,12 @@ use async_trait::async_trait; -use crate::network; -use crate::requests::{Request, RequestContext, ResponseResult}; -use crate::types::{ChatId, Message, ParseMode, ReplyMarkup}; +use crate::{ + bot::Bot, + network, + requests::{Request, ResponseResult}, + types::{ChatId, InputFile, Message, ParseMode, ReplyMarkup}, +}; -///TODO: add to bot api ///Use this method to send animation files (GIF or H.264/MPEG-4 AVC video /// without sound). On success, the sent Message is returned. Bots can currently /// send animation files of up to 50 MB in size, this limit may be changed in @@ -12,7 +14,7 @@ use crate::types::{ChatId, Message, ParseMode, ReplyMarkup}; #[derive(Debug, Clone, Serialize)] pub struct SendAnimation<'a> { #[serde(skip_serializing)] - ctx: RequestContext<'a>, + bot: &'a Bot, ///Unique identifier for the target chat or username of the target channel /// (in the format @channelusername) pub chat_id: ChatId, @@ -20,8 +22,7 @@ pub struct SendAnimation<'a> { /// exists on the Telegram servers (recommended), pass an HTTP URL as a /// String for Telegram to get an animation from the Internet, or upload a /// new animation using multipart/form-data. More info on Sending Files » - pub animation: String, - // InputFile or String + pub animation: InputFile, ///Duration of sent animation in seconds #[serde(skip_serializing_if = "Option::is_none")] pub duration: Option<u64>, @@ -40,8 +41,7 @@ pub struct SendAnimation<'a> { /// if the thumbnail was uploaded using multipart/form-data under /// <file_attach_name> » #[serde(skip_serializing_if = "Option::is_none")] - pub thumb: Option<String>, - // InputFile or String Optional + pub thumb: Option<InputFile>, ///Animation caption (may also be used when resending animation by /// file_id), 0-1024 characters #[serde(skip_serializing_if = "Option::is_none")] @@ -76,8 +76,8 @@ impl Request for SendAnimation<'_> { impl SendAnimation<'_> { async fn send(self) -> ResponseResult<Message> { network::request_json( - &self.ctx.client, - &self.ctx.token, + self.bot.client(), + self.bot.token(), "sendAnimation", &self, ) @@ -86,17 +86,13 @@ impl SendAnimation<'_> { } impl<'a> SendAnimation<'a> { - pub(crate) fn new<C, S>( - ctx: RequestContext<'a>, - chat_id: C, - animation: S, - ) -> Self + pub(crate) fn new<C, S>(bot: &'a Bot, chat_id: C, animation: S) -> Self where C: Into<ChatId>, - S: Into<String>, + S: Into<InputFile>, { Self { - ctx, + bot, chat_id: chat_id.into(), animation: animation.into(), duration: None, @@ -143,7 +139,7 @@ impl<'a> SendAnimation<'a> { } pub fn thumb<T>(mut self, value: T) -> Self where - T: Into<String>, + T: Into<InputFile>, { self.thumb = Some(value.into()); self diff --git a/src/requests/send_audio.rs b/src/requests/send_audio.rs index 731f47ad..95d29b61 100644 --- a/src/requests/send_audio.rs +++ b/src/requests/send_audio.rs @@ -1,10 +1,9 @@ use async_trait::async_trait; use crate::{ + bot::Bot, network, - requests::{ - form_builder::FormBuilder, Request, RequestContext, ResponseResult, - }, + requests::{form_builder::FormBuilder, Request, ResponseResult}, types::{ChatId, InputFile, Message, ParseMode, ReplyMarkup}, }; @@ -18,7 +17,7 @@ use crate::{ /// [`Message`]: crate::types::Message /// [`SendVoice`]: crate::requests::SendVoice pub struct SendAudio<'a> { - ctx: RequestContext<'a>, + bot: &'a Bot, /// Unique identifier for the target chat or username of the target channel /// (in the format @channelusername) @@ -86,10 +85,9 @@ impl SendAudio<'_> { .add("audio", self.audio) .add("thumb", self.thumb); - network::request_multipart( - &self.ctx.client, - &self.ctx.token, + self.bot.client(), + self.bot.token(), "sendAudio", params.build(), ) @@ -98,17 +96,13 @@ impl SendAudio<'_> { } impl<'a> SendAudio<'a> { - pub(crate) fn new<C, A>( - ctx: RequestContext<'a>, - chat_id: C, - audio: A, - ) -> Self + pub(crate) fn new<C, A>(bot: &'a Bot, chat_id: C, audio: A) -> Self where C: Into<ChatId>, A: Into<InputFile>, { Self { - ctx, + bot, chat_id: chat_id.into(), audio: audio.into(), caption: None, diff --git a/src/requests/send_chat_action.rs b/src/requests/send_chat_action.rs index 54e898bb..af4db368 100644 --- a/src/requests/send_chat_action.rs +++ b/src/requests/send_chat_action.rs @@ -1,8 +1,9 @@ use async_trait::async_trait; use crate::{ + bot::Bot, network, - requests::{Request, RequestContext, ResponseResult}, + requests::{Request, ResponseResult}, types::{ChatAction, ChatId, True}, }; @@ -13,7 +14,7 @@ use crate::{ #[derive(Debug, Clone, Serialize)] pub struct SendChatAction<'a> { #[serde(skip_serializing)] - ctx: RequestContext<'a>, + bot: &'a Bot, /// Unique identifier for the target chat or /// username of the target channel (in the format @channelusername) pub chat_id: ChatId, @@ -37,8 +38,8 @@ impl Request for SendChatAction<'_> { impl SendChatAction<'_> { pub async fn send(self) -> ResponseResult<True> { network::request_json( - &self.ctx.client, - &self.ctx.token, + self.bot.client(), + self.bot.token(), "sendChatAction", &self, ) @@ -47,17 +48,13 @@ impl SendChatAction<'_> { } impl<'a> SendChatAction<'a> { - pub(crate) fn new<Cid, Ca>( - ctx: RequestContext<'a>, - chat_id: Cid, - action: Ca, - ) -> Self + pub(crate) fn new<Cid, Ca>(bot: &'a Bot, chat_id: Cid, action: Ca) -> Self where Cid: Into<ChatId>, Ca: Into<ChatAction>, { Self { - ctx, + bot, chat_id: chat_id.into(), action: action.into(), } diff --git a/src/requests/send_contact.rs b/src/requests/send_contact.rs index 307fb321..56e64cf9 100644 --- a/src/requests/send_contact.rs +++ b/src/requests/send_contact.rs @@ -1,8 +1,9 @@ use async_trait::async_trait; use crate::{ + bot::Bot, network, - requests::{Request, RequestContext, ResponseResult}, + requests::{Request, ResponseResult}, types::{ChatId, Message, ReplyMarkup}, }; @@ -11,7 +12,7 @@ use crate::{ #[derive(Debug, Clone, Serialize)] pub struct SendContact<'a> { #[serde(skip_serializing)] - ctx: RequestContext<'a>, + bot: &'a Bot, /// Unique identifier for the target chat or /// username of the target channel (in the format @channelusername) pub chat_id: ChatId, @@ -54,8 +55,8 @@ impl Request for SendContact<'_> { impl SendContact<'_> { pub async fn send(self) -> ResponseResult<Message> { network::request_json( - &self.ctx.client, - &self.ctx.token, + self.bot.client(), + self.bot.token(), "sendContact", &self, ) @@ -65,7 +66,7 @@ impl SendContact<'_> { impl<'a> SendContact<'a> { pub(crate) fn new<C, P, F>( - ctx: RequestContext<'a>, + bot: &'a Bot, chat_id: C, phone_number: P, first_name: F, @@ -76,7 +77,7 @@ impl<'a> SendContact<'a> { F: Into<String>, { Self { - ctx, + bot, chat_id: chat_id.into(), phone_number: phone_number.into(), first_name: first_name.into(), diff --git a/src/requests/send_document.rs b/src/requests/send_document.rs index 298baa02..9224955c 100644 --- a/src/requests/send_document.rs +++ b/src/requests/send_document.rs @@ -1,20 +1,19 @@ use async_trait::async_trait; use crate::{ + bot::Bot, network, - requests::{Request, RequestContext, ResponseResult}, - types::{ChatId, Message, ParseMode, ReplyMarkup}, + requests::{Request, ResponseResult}, + types::{ChatId, InputFile, Message, ParseMode, ReplyMarkup}, }; -// TODO: add method to bot/api - ///Use this method to send general files. On success, the sent Message is /// returned. Bots can currently send files of any type of up to 50 MB in size, /// this limit may be changed in the future. #[derive(Debug, Clone, Serialize)] pub struct SendDocument<'a> { #[serde(skip_serializing)] - ctx: RequestContext<'a>, + bot: &'a Bot, /// Unique identifier for the target chat or username of the target /// channel (in the format @channelusername) pub chat_id: ChatId, @@ -22,8 +21,7 @@ pub struct SendDocument<'a> { /// the Telegram servers (recommended), pass an HTTP URL as a String for /// Telegram to get a file from the Internet, or upload a new one using /// multipart/form-data.» - pub document: String, - //InputFile or String + pub document: InputFile, /// Thumbnail of the file sent; can be ignored if thumbnail generation for /// the file is supported server-side. The thumbnail should be in JPEG /// format and less than 200 kB in size. A thumbnail‘s width and height @@ -33,8 +31,7 @@ pub struct SendDocument<'a> { /// if the thumbnail was uploaded using multipart/form-data under /// <file_attach_name>. More info on Sending Files » #[serde(skip_serializing_if = "Option::is_none")] - pub thumb: Option<String>, - //InputFile or String + pub thumb: Option<InputFile>, /// Document caption (may also be used when resending documents by /// file_id), 0-1024 characters #[serde(skip_serializing_if = "Option::is_none")] @@ -69,8 +66,8 @@ impl Request for SendDocument<'_> { impl SendDocument<'_> { pub async fn send(self) -> ResponseResult<Message> { network::request_json( - &self.ctx.client, - &self.ctx.token, + self.bot.client(), + self.bot.token(), "sendDocument", &self, ) @@ -79,17 +76,13 @@ impl SendDocument<'_> { } impl<'a> SendDocument<'a> { - pub(crate) fn new<C, D>( - ctx: RequestContext<'a>, - chat_id: C, - document: D, - ) -> Self + pub(crate) fn new<C, D>(bot: &'a Bot, chat_id: C, document: D) -> Self where C: Into<ChatId>, - D: Into<String>, + D: Into<InputFile>, { Self { - ctx, + bot, chat_id: chat_id.into(), document: document.into(), thumb: None, @@ -111,7 +104,7 @@ impl<'a> SendDocument<'a> { pub fn document<T>(mut self, value: T) -> Self where - T: Into<String>, + T: Into<InputFile>, { self.document = value.into(); self @@ -119,7 +112,7 @@ impl<'a> SendDocument<'a> { pub fn thumb<T>(mut self, value: T) -> Self where - T: Into<String>, + T: Into<InputFile>, { self.thumb = Some(value.into()); self diff --git a/src/requests/send_location.rs b/src/requests/send_location.rs index a37162b2..bd76e9a2 100644 --- a/src/requests/send_location.rs +++ b/src/requests/send_location.rs @@ -3,8 +3,9 @@ use serde::Serialize; use async_trait::async_trait; use crate::{ + bot::Bot, network, - requests::{Request, RequestContext, ResponseResult}, + requests::{Request, ResponseResult}, types::{ChatId, Message, ReplyMarkup}, }; @@ -13,7 +14,7 @@ use crate::{ /// is returned. pub struct SendLocation<'a> { #[serde(skip_serializing)] - ctx: RequestContext<'a>, + bot: &'a Bot, /// Unique identifier for the target chat or username of the target channel /// (in the format @channelusername) @@ -50,8 +51,8 @@ impl Request for SendLocation<'_> { impl SendLocation<'_> { pub async fn send(self) -> ResponseResult<Message> { network::request_json( - &self.ctx.client, - &self.ctx.token, + self.bot.client(), + self.bot.token(), "sendLocation", &self, ) @@ -61,7 +62,7 @@ impl SendLocation<'_> { impl<'a> SendLocation<'a> { pub(crate) fn new<Lt, Lg, C>( - ctx: RequestContext<'a>, + bot: &'a Bot, chat_id: C, latitude: Lt, longitude: Lg, @@ -72,7 +73,7 @@ impl<'a> SendLocation<'a> { C: Into<ChatId>, { Self { - ctx, + bot, chat_id: chat_id.into(), latitude: latitude.into(), longitude: longitude.into(), diff --git a/src/requests/send_media_group.rs b/src/requests/send_media_group.rs index d189a926..ea503ed8 100644 --- a/src/requests/send_media_group.rs +++ b/src/requests/send_media_group.rs @@ -1,17 +1,16 @@ use async_trait::async_trait; use crate::{ + bot::Bot, network::request_multipart, - requests::{ - form_builder::FormBuilder, Request, RequestContext, ResponseResult, - }, - types::{ChatId, InputMedia, Message, InputFile}, + requests::{form_builder::FormBuilder, Request, ResponseResult}, + types::{ChatId, InputFile, InputMedia, Message}, }; /// Use this method to send a group of photos or videos as an album. #[derive(Debug, Clone)] pub struct SendMediaGroup<'a> { - ctx: RequestContext<'a>, + bot: &'a Bot, pub chat_id: ChatId, pub media: Vec<InputMedia>, @@ -37,17 +36,20 @@ impl SendMediaGroup<'_> { .add("disable_notification", self.disable_notification) .add("reply_to_message_id", self.reply_to_message_id); - let form = self.media.into_iter().filter_map(|e| InputFile::from(e).into()) - .fold(form, |acc, path: std::path::PathBuf| - acc.add_file( - &path.file_name().unwrap().to_string_lossy().into_owned(), - path, - ) - ); + let form = self + .media + .into_iter() + .filter_map(|e| InputFile::from(e).into()) + .fold(form, |acc, path: std::path::PathBuf| { + acc.add_file( + &path.file_name().unwrap().to_string_lossy().into_owned(), + path, + ) + }); request_multipart( - &self.ctx.client, - &self.ctx.token, + self.bot.client(), + self.bot.token(), "sendMediaGroup", form.build(), ) @@ -56,17 +58,13 @@ impl SendMediaGroup<'_> { } impl<'a> SendMediaGroup<'a> { - pub(crate) fn new<C, M>( - ctx: RequestContext<'a>, - chat_id: C, - media: M, - ) -> Self + pub(crate) fn new<C, M>(bot: &'a Bot, chat_id: C, media: M) -> Self where C: Into<ChatId>, M: Into<Vec<InputMedia>>, { SendMediaGroup { - ctx, + bot, chat_id: chat_id.into(), media: media.into(), disable_notification: None, diff --git a/src/requests/send_message.rs b/src/requests/send_message.rs index adca7c4f..cd16ca21 100644 --- a/src/requests/send_message.rs +++ b/src/requests/send_message.rs @@ -1,8 +1,9 @@ use async_trait::async_trait; use crate::{ + bot::Bot, network, - requests::{Request, RequestContext, ResponseResult}, + requests::{Request, ResponseResult}, types::{ChatId, Message, ParseMode, ReplyMarkup}, }; @@ -11,7 +12,7 @@ use crate::{ /// returned. pub struct SendMessage<'a> { #[serde(skip_serializing)] - ctx: RequestContext<'a>, + bot: &'a Bot, /// Unique identifier for the target chat or username of the target channel /// (in the format @channelusername) @@ -55,8 +56,8 @@ impl Request for SendMessage<'_> { impl SendMessage<'_> { pub async fn send(self) -> ResponseResult<Message> { network::request_json( - self.ctx.client, - self.ctx.token, + self.bot.client(), + self.bot.token(), "sendMessage", &self, ) @@ -65,17 +66,13 @@ impl SendMessage<'_> { } impl<'a> SendMessage<'a> { - pub(crate) fn new<C, S>( - ctx: RequestContext<'a>, - chat_id: C, - text: S, - ) -> Self + pub(crate) fn new<C, S>(bot: &'a Bot, chat_id: C, text: S) -> Self where C: Into<ChatId>, S: Into<String>, { SendMessage { - ctx, + bot, chat_id: chat_id.into(), text: text.into(), parse_mode: None, diff --git a/src/requests/send_photo.rs b/src/requests/send_photo.rs index 8092a55a..76c1b18b 100644 --- a/src/requests/send_photo.rs +++ b/src/requests/send_photo.rs @@ -1,10 +1,9 @@ use async_trait::async_trait; use crate::{ + bot::Bot, network, - requests::{ - form_builder::FormBuilder, Request, RequestContext, ResponseResult, - }, + requests::{form_builder::FormBuilder, Request, ResponseResult}, types::{ChatId, InputFile, Message, ParseMode, ReplyMarkup}, }; @@ -12,7 +11,7 @@ use crate::{ /// Use this method to send photos. On success, the sent [`Message`] is /// returned. pub struct SendPhoto<'a> { - ctx: RequestContext<'a>, + bot: &'a Bot, /// Unique identifier for the target chat or username of the target channel /// (in the format @channelusername) @@ -64,8 +63,8 @@ impl SendPhoto<'_> { .add("photo", self.photo); network::request_multipart( - &self.ctx.client, - &self.ctx.token, + self.bot.client(), + self.bot.token(), "sendPhoto", params.build(), ) @@ -74,17 +73,13 @@ impl SendPhoto<'_> { } impl<'a> SendPhoto<'a> { - pub(crate) fn new<C, P>( - ctx: RequestContext<'a>, - chat_id: C, - photo: P, - ) -> Self + pub(crate) fn new<C, P>(bot: &'a Bot, chat_id: C, photo: P) -> Self where C: Into<ChatId>, P: Into<InputFile>, { Self { - ctx, + bot, chat_id: chat_id.into(), photo: photo.into(), caption: None, diff --git a/src/requests/send_poll.rs b/src/requests/send_poll.rs index 9d8466d3..d5d2e2da 100644 --- a/src/requests/send_poll.rs +++ b/src/requests/send_poll.rs @@ -1,8 +1,9 @@ use async_trait::async_trait; use crate::{ + bot::Bot, network, - requests::{Request, RequestContext, ResponseResult}, + requests::{Request, ResponseResult}, types::{ChatId, Message, ReplyMarkup}, }; @@ -11,7 +12,7 @@ use crate::{ #[derive(Debug, Clone, Serialize)] pub struct SendPoll<'a> { #[serde(skip_serializing)] - ctx: RequestContext<'a>, + bot: &'a Bot, /// identifier for the target chat or username of the target channel (in /// the format @channelusername). A native poll can't be sent to a private /// chat. @@ -44,8 +45,8 @@ impl Request for SendPoll<'_> { impl SendPoll<'_> { pub async fn send(self) -> ResponseResult<Message> { network::request_json( - &self.ctx.client, - &self.ctx.token, + self.bot.client(), + self.bot.token(), "sendPoll", &self, ) @@ -55,7 +56,7 @@ impl SendPoll<'_> { impl<'a> SendPoll<'a> { pub(crate) fn new<C, Q, O>( - ctx: RequestContext<'a>, + bot: &'a Bot, chat_id: C, question: Q, options: O, @@ -66,7 +67,7 @@ impl<'a> SendPoll<'a> { O: Into<Vec<String>>, { Self { - ctx, + bot, chat_id: chat_id.into(), question: question.into(), options: options.into(), diff --git a/src/requests/send_venue.rs b/src/requests/send_venue.rs index 6fdd45e7..30e501c8 100644 --- a/src/requests/send_venue.rs +++ b/src/requests/send_venue.rs @@ -1,8 +1,9 @@ use async_trait::async_trait; use crate::{ + bot::Bot, network, - requests::{Request, RequestContext, ResponseResult}, + requests::{Request, ResponseResult}, types::{ChatId, Message, ReplyMarkup}, }; @@ -11,7 +12,7 @@ use crate::{ #[derive(Debug, Clone, Serialize)] pub struct SendVenue<'a> { #[serde(skip_serializing)] - ctx: RequestContext<'a>, + bot: &'a Bot, /// Unique identifier for the target chat or /// username of the target channel (in the format @channelusername) pub chat_id: ChatId, @@ -59,8 +60,8 @@ impl Request for SendVenue<'_> { impl SendVenue<'_> { pub async fn send(self) -> ResponseResult<Message> { network::request_json( - &self.ctx.client, - &self.ctx.token, + self.bot.client(), + self.bot.token(), "sendVenue", &self, ) @@ -70,7 +71,7 @@ impl SendVenue<'_> { impl<'a> SendVenue<'a> { pub(crate) fn new<Lt, Lg, C, T, A>( - ctx: RequestContext<'a>, + bot: &'a Bot, chat_id: C, latitude: Lt, longitude: Lg, @@ -85,7 +86,7 @@ impl<'a> SendVenue<'a> { A: Into<String>, { Self { - ctx, + bot, chat_id: chat_id.into(), latitude: latitude.into(), longitude: longitude.into(), diff --git a/src/requests/send_video.rs b/src/requests/send_video.rs index 5d8d6696..4763fde1 100644 --- a/src/requests/send_video.rs +++ b/src/requests/send_video.rs @@ -1,10 +1,12 @@ use async_trait::async_trait; -use crate::network; -use crate::requests::{Request, RequestContext, ResponseResult}; -use crate::types::{ChatId, Message, ParseMode, ReplyMarkup}; +use crate::{ + bot::Bot, + network, + requests::{Request, ResponseResult}, + types::{ChatId, InputFile, Message, ParseMode, ReplyMarkup}, +}; -//TODO: add action to bot api ///Use this method to send video files, Telegram clients support mp4 videos /// (other formats may be sent as Document). On success, the sent Message is /// returned. Bots can currently send video files of up to 50 MB in size, this @@ -12,7 +14,7 @@ use crate::types::{ChatId, Message, ParseMode, ReplyMarkup}; #[derive(Debug, Clone, Serialize)] pub struct SendVideo<'a> { #[serde(skip_serializing)] - ctx: RequestContext<'a>, + bot: &'a Bot, ///Unique identifier for the target chat or username of the target channel /// (in the format @channelusername) pub chat_id: ChatId, @@ -20,7 +22,7 @@ pub struct SendVideo<'a> { /// the Telegram servers (recommended), pass an HTTP URL as a String for /// Telegram to get a video from the Internet, or upload a new video using /// multipart/form-data. More info on Sending Files » - pub video: String, + pub video: InputFile, ///Duration of sent video in seconds #[serde(skip_serializing_if = "Option::is_none")] pub duration: Option<u64>, @@ -39,8 +41,7 @@ pub struct SendVideo<'a> { /// if the thumbnail was uploaded using multipart/form-data under /// <file_attach_name>. More info on Sending Files » #[serde(skip_serializing_if = "Option::is_none")] - pub thumb: Option<String>, - //InputFile or String + pub thumb: Option<InputFile>, ///Video caption (may also be used when resending videos by file_id), /// 0-1024 characters #[serde(skip_serializing_if = "Option::is_none")] @@ -78,8 +79,8 @@ impl Request for SendVideo<'_> { impl SendVideo<'_> { async fn send(self) -> ResponseResult<Message> { network::request_json( - &self.ctx.client, - &self.ctx.token, + self.bot.client(), + self.bot.token(), "sendVideo", &self, ) @@ -88,17 +89,13 @@ impl SendVideo<'_> { } impl<'a> SendVideo<'a> { - pub(crate) fn new<C, V>( - ctx: RequestContext<'a>, - chat_id: C, - video: V, - ) -> Self + pub(crate) fn new<C, V>(bot: &'a Bot, chat_id: C, video: V) -> Self where C: Into<ChatId>, - V: Into<String>, + V: Into<InputFile>, { Self { - ctx, + bot, chat_id: chat_id.into(), video: video.into(), duration: None, @@ -123,7 +120,7 @@ impl<'a> SendVideo<'a> { pub fn video<T>(mut self, value: T) -> Self where - T: Into<String>, + T: Into<InputFile>, { self.video = value.into(); self @@ -152,7 +149,7 @@ impl<'a> SendVideo<'a> { } pub fn thumb<T>(mut self, value: T) -> Self where - T: Into<String>, + T: Into<InputFile>, { self.thumb = Some(value.into()); self diff --git a/src/requests/send_video_note.rs b/src/requests/send_video_note.rs index c824bfdf..70e6c9df 100644 --- a/src/requests/send_video_note.rs +++ b/src/requests/send_video_note.rs @@ -1,9 +1,10 @@ use async_trait::async_trait; use crate::{ + bot::Bot, network, - requests::{Request, RequestContext, ResponseResult}, - types::{ChatId, Message, ReplyMarkup}, + requests::{Request, ResponseResult}, + types::{ChatId, InputFile, Message, ReplyMarkup}, }; ///As of v.4.0, Telegram clients support rounded square mp4 videos of up to 1 @@ -12,7 +13,7 @@ use crate::{ #[derive(Debug, Clone, Serialize)] pub struct SendVideoNote<'a> { #[serde(skip_serializing)] - ctx: RequestContext<'a>, + bot: &'a Bot, ///Unique identifier for the target chat or username of the target channel /// (in the format @channelusername) pub chat_id: ChatId, @@ -20,8 +21,7 @@ pub struct SendVideoNote<'a> { /// exists on the Telegram servers (recommended) or upload a new video /// using multipart/form-data. More info on Sending Files ». Sending video /// notes by a URL is currently unsupported - pub video_note: String, - // InputFile or String + pub video_note: InputFile, ///Duration of sent video in seconds #[serde(skip_serializing_if = "Option::is_none")] pub duration: Option<u64>, @@ -37,8 +37,7 @@ pub struct SendVideoNote<'a> { /// if the thumbnail was uploaded using multipart/form-data under /// <file_attach_name>. More info on Sending Files » #[serde(skip_serializing_if = "Option::is_none")] - pub thumb: Option<String>, - // InputFile or String + pub thumb: Option<InputFile>, ///Sends the message silently. Users will receive a notification with no /// sound. #[serde(skip_serializing_if = "Option::is_none")] @@ -65,8 +64,8 @@ impl Request for SendVideoNote<'_> { impl SendVideoNote<'_> { pub async fn send(self) -> ResponseResult<Message> { network::request_json( - &self.ctx.client, - &self.ctx.token, + self.bot.client(), + self.bot.token(), "sendVideoNote", &self, ) @@ -75,17 +74,13 @@ impl SendVideoNote<'_> { } impl<'a> SendVideoNote<'a> { - pub(crate) fn new<C, V>( - ctx: RequestContext<'a>, - chat_id: C, - video_note: V, - ) -> Self + pub(crate) fn new<C, V>(bot: &'a Bot, chat_id: C, video_note: V) -> Self where C: Into<ChatId>, - V: Into<String>, + V: Into<InputFile>, { Self { - ctx, + bot, chat_id: chat_id.into(), video_note: video_note.into(), duration: None, @@ -107,7 +102,7 @@ impl<'a> SendVideoNote<'a> { pub fn video_note<T>(mut self, value: T) -> Self where - T: Into<String>, + T: Into<InputFile>, { self.video_note = value.into(); self @@ -131,7 +126,7 @@ impl<'a> SendVideoNote<'a> { pub fn thumb<T>(mut self, value: T) -> Self where - T: Into<String>, + T: Into<InputFile>, { self.thumb = Some(value.into()); self diff --git a/src/requests/send_voice.rs b/src/requests/send_voice.rs index 75f139ea..af3724b9 100644 --- a/src/requests/send_voice.rs +++ b/src/requests/send_voice.rs @@ -1,9 +1,10 @@ use async_trait::async_trait; use crate::{ + bot::Bot, network, - requests::{Request, RequestContext, ResponseResult}, - types::{ChatId, Message, ParseMode, ReplyMarkup}, + requests::{Request, ResponseResult}, + types::{ChatId, InputFile, Message, ParseMode, ReplyMarkup}, }; ///Use this method to send audio files, if you want Telegram clients to display @@ -15,7 +16,7 @@ use crate::{ #[derive(Debug, Clone, Serialize)] pub struct SendVoice<'a> { #[serde(skip_serializing)] - ctx: RequestContext<'a>, + bot: &'a Bot, /// Unique identifier for the target chat or username of the target channel /// (in the format @channelusername) pub chat_id: ChatId, @@ -23,8 +24,7 @@ pub struct SendVoice<'a> { /// on the Telegram servers (recommended), pass an HTTP URL as a String for /// Telegram to get a file from the Internet, or upload a new one using /// multipart/form-data. More info on Sending Files » - pub voice: String, - //InputFile or String + pub voice: InputFile, /// Voice message caption, 0-1024 characters #[serde(skip_serializing_if = "Option::is_none")] pub caption: Option<String>, @@ -62,8 +62,8 @@ impl Request for SendVoice<'_> { impl SendVoice<'_> { pub async fn send(self) -> ResponseResult<Message> { network::request_json( - &self.ctx.client, - &self.ctx.token, + self.bot.client(), + self.bot.token(), "sendVoice", &self, ) @@ -72,17 +72,13 @@ impl SendVoice<'_> { } impl<'a> SendVoice<'a> { - pub(crate) fn new<C, V>( - ctx: RequestContext<'a>, - chat_id: C, - voice: V, - ) -> Self + pub(crate) fn new<C, V>(bot: &'a Bot, chat_id: C, voice: V) -> Self where C: Into<ChatId>, - V: Into<String>, + V: Into<InputFile>, { Self { - ctx, + bot, chat_id: chat_id.into(), voice: voice.into(), caption: None, @@ -104,7 +100,7 @@ impl<'a> SendVoice<'a> { pub fn voice<T>(mut self, value: T) -> Self where - T: Into<String>, + T: Into<InputFile>, { self.voice = value.into(); self diff --git a/src/requests/set_chat_description.rs b/src/requests/set_chat_description.rs new file mode 100644 index 00000000..e3cf9c68 --- /dev/null +++ b/src/requests/set_chat_description.rs @@ -0,0 +1,99 @@ +use async_trait::async_trait; + +use crate::{ + bot::Bot, + network, + requests::{Request, ResponseResult}, + types::{ChatId, True}, +}; + +#[derive(Debug, Clone, Serialize)] +pub struct SetChatDescription<'a> { + #[serde(skip_serializing)] + bot: &'a Bot, + + chat_id: ChatId, + #[serde(skip_serializing_if = "Option::is_none")] + description: Option<String>, +} + +#[async_trait] +impl Request for SetChatDescription<'_> { + type Output = True; + + async fn send_boxed(self) -> ResponseResult<Self::Output> { + self.send().await + } +} + +impl SetChatDescription<'_> { + pub async fn send(self) -> ResponseResult<True> { + network::request_json( + &self.bot.client(), + &self.bot.token(), + "setChatDescription", + &self, + ) + .await + } +} + +impl<'a> SetChatDescription<'a> { + pub(crate) fn new<C>(bot: &'a Bot, chat_id: C) -> Self + where + C: Into<ChatId>, + { + Self { + bot, + chat_id: chat_id.into(), + description: None, + } + } + + pub fn chat_id<T>(mut self, chat_id: T) -> Self + where + T: Into<ChatId>, + { + self.chat_id = chat_id.into(); + self + } + + pub fn description<T>(mut self, description: T) -> Self + where + T: Into<String>, + { + self.description = Some(description.into()); + self + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn serialize_new() { + let bot = Bot::new("token"); + let chat_id = 123; + let method = SetChatDescription::new(&bot, chat_id); + + let expected = r#"{"chat_id":123}"#; + let actual = + serde_json::to_string::<SetChatDescription>(&method).unwrap(); + assert_eq!(actual, expected); + } + + #[test] + fn serialize_description() { + let bot = Bot::new("token"); + let chat_id = 123; + let description = "description"; + let method = + SetChatDescription::new(&bot, chat_id).description(description); + + let expected = r#"{"chat_id":123,"description":"description"}"#; + let actual = + serde_json::to_string::<SetChatDescription>(&method).unwrap(); + assert_eq!(actual, expected); + } +} diff --git a/src/requests/set_chat_permissions.rs b/src/requests/set_chat_permissions.rs new file mode 100644 index 00000000..a7307935 --- /dev/null +++ b/src/requests/set_chat_permissions.rs @@ -0,0 +1,96 @@ +use async_trait::async_trait; + +use crate::{ + bot::Bot, + network, + requests::{Request, ResponseResult}, + types::{ChatId, ChatPermissions, True}, +}; + +#[derive(Debug, Clone, Serialize)] +pub struct SetChatPermissions<'a> { + #[serde(skip_serializing)] + bot: &'a Bot, + + chat_id: ChatId, + permissions: ChatPermissions, +} + +#[async_trait] +impl Request for SetChatPermissions<'_> { + type Output = True; + + async fn send_boxed(self) -> ResponseResult<Self::Output> { + self.send().await + } +} + +impl SetChatPermissions<'_> { + async fn send(self) -> ResponseResult<True> { + network::request_json( + self.bot.client(), + self.bot.token(), + "setChatPermissions", + &self, + ) + .await + } +} + +impl<'a> SetChatPermissions<'a> { + pub(crate) fn new<C, CP>(bot: &'a Bot, chat_id: C, permissions: CP) -> Self + where + C: Into<ChatId>, + CP: Into<ChatPermissions>, + { + Self { + bot, + chat_id: chat_id.into(), + permissions: permissions.into(), + } + } + + pub fn chat_id<C>(mut self, chat_id: C) -> Self + where + C: Into<ChatId>, + { + self.chat_id = chat_id.into(); + self + } + + pub fn permissions<CP>(mut self, permissions: CP) -> Self + where + CP: Into<ChatPermissions>, + { + self.permissions = permissions.into(); + self + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn serialize() { + let bot = Bot::new("token"); + let chat_id = 123; + let permissions = ChatPermissions { + can_send_messages: Some(true), + can_send_media_messages: None, + can_send_polls: None, + can_send_other_messages: None, + can_add_web_page_previews: None, + can_change_info: None, + can_invite_users: None, + can_pin_messages: None, + }; + let method = SetChatPermissions::new(&bot, chat_id, permissions); + + let expected = + r#"{"chat_id":123,"permissions":{"can_send_messages":true}}"#; + let actual = + serde_json::to_string::<SetChatPermissions>(&method).unwrap(); + assert_eq!(actual, expected); + } +} diff --git a/src/requests/set_chat_photo.rs b/src/requests/set_chat_photo.rs new file mode 100644 index 00000000..6a372d1a --- /dev/null +++ b/src/requests/set_chat_photo.rs @@ -0,0 +1,90 @@ +use async_trait::async_trait; + +use crate::{ + bot::Bot, + network, + requests::{form_builder::FormBuilder, Request, ResponseResult}, + types::{ChatId, InputFile, True}, +}; + +#[derive(Debug, Clone, Serialize)] +pub struct SetChatPhoto<'a> { + #[serde(skip_serializing)] + bot: &'a Bot, + + chat_id: ChatId, + photo: InputFile, +} + +#[async_trait] +impl Request for SetChatPhoto<'_> { + type Output = True; + + async fn send_boxed(self) -> ResponseResult<Self::Output> { + self.send().await + } +} + +impl SetChatPhoto<'_> { + async fn send(self) -> ResponseResult<True> { + let params = FormBuilder::new() + .add("chat_id", self.chat_id) + .add("photo", self.photo); + + network::request_multipart( + self.bot.client(), + self.bot.token(), + "setChatPhoto", + params.build(), + ) + .await + } +} + +impl<'a> SetChatPhoto<'a> { + pub(crate) fn new<C, P>(bot: &'a Bot, chat_id: C, photo: P) -> Self + where + C: Into<ChatId>, + P: Into<InputFile>, + { + Self { + bot, + chat_id: chat_id.into(), + photo: photo.into(), + } + } + + pub fn chat_id<C>(mut self, chat_id: C) -> Self + where + C: Into<ChatId>, + { + self.chat_id = chat_id.into(); + self + } + + pub fn photo<P>(mut self, photo: P) -> Self + where + P: Into<InputFile>, + { + self.photo = photo.into(); + self + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn serialize() { + let bot = Bot::new("token"); + let chat_id = 123; + let photo_url = "https://some_url".to_string(); + let method = + SetChatPhoto::new(&bot, chat_id, InputFile::Url(photo_url)); + + let expected = r#"{"chat_id":123,"photo":"https://some_url"}"#; + let actual = serde_json::to_string::<SetChatPhoto>(&method).unwrap(); + assert_eq!(actual, expected); + } +} diff --git a/src/requests/set_chat_sticker_set.rs b/src/requests/set_chat_sticker_set.rs new file mode 100644 index 00000000..a6166cf5 --- /dev/null +++ b/src/requests/set_chat_sticker_set.rs @@ -0,0 +1,81 @@ +use async_trait::async_trait; + +use crate::{ + bot::Bot, + network, + requests::{Request, ResponseResult}, + types::{ChatId, True}, +}; + +/// Use this method to set a new group sticker set for a supergroup. The bot +/// must be an administrator in the chat for this to work and must have the +/// appropriate admin rights. Use the field can_set_sticker_set optionally +/// returned in getChat requests to check if the bot can use this method. +/// Returns True on success. +#[derive(Debug, Clone, Serialize)] +pub struct SetChatStickerSet<'a> { + #[serde(skip_serializing)] + bot: &'a Bot, + + /// Unique identifier for the target chat or username of the target + /// supergroup (in the format @supergroupusername) + chat_id: ChatId, + + /// Name of the sticker set to be set as the group sticker set + sticker_set_name: String, +} + +#[async_trait] +impl Request for SetChatStickerSet<'_> { + type Output = True; + + async fn send_boxed(self) -> ResponseResult<Self::Output> { + self.send().await + } +} + +impl SetChatStickerSet<'_> { + async fn send(&self) -> ResponseResult<True> { + network::request_json( + self.bot.client(), + self.bot.token(), + "setChatStickerSet", + &self, + ) + .await + } +} + +impl<'a> SetChatStickerSet<'a> { + pub(crate) fn new<C, S>( + bot: &'a Bot, + chat_id: C, + sticker_set_name: S, + ) -> Self + where + C: Into<ChatId>, + S: Into<String>, + { + Self { + bot, + chat_id: chat_id.into(), + sticker_set_name: sticker_set_name.into(), + } + } + + pub fn chat_id<C>(mut self, value: C) -> Self + where + C: Into<ChatId>, + { + self.chat_id = value.into(); + self + } + + pub fn sticker_set_name<S>(mut self, value: S) -> Self + where + S: Into<String>, + { + self.sticker_set_name = value.into(); + self + } +} diff --git a/src/requests/set_chat_title.rs b/src/requests/set_chat_title.rs new file mode 100644 index 00000000..a86f8e2b --- /dev/null +++ b/src/requests/set_chat_title.rs @@ -0,0 +1,85 @@ +use async_trait::async_trait; + +use crate::{ + bot::Bot, + network, + requests::{Request, ResponseResult}, + types::{ChatId, True}, +}; + +#[derive(Debug, Clone, Serialize)] +pub struct SetChatTitle<'a> { + #[serde(skip_serializing)] + bot: &'a Bot, + + chat_id: ChatId, + title: String, +} + +#[async_trait] +impl Request for SetChatTitle<'_> { + type Output = True; + + async fn send_boxed(self) -> ResponseResult<Self::Output> { + self.send().await + } +} + +impl SetChatTitle<'_> { + async fn send(self) -> ResponseResult<True> { + network::request_json( + &self.bot.client(), + &self.bot.token(), + "setChatTitle", + &self, + ) + .await + } +} + +impl<'a> SetChatTitle<'a> { + pub(crate) fn new<C, T>(bot: &'a Bot, chat_id: C, title: T) -> Self + where + C: Into<ChatId>, + T: Into<String>, + { + Self { + bot, + chat_id: chat_id.into(), + title: title.into(), + } + } + + pub fn chat_id<C>(mut self, chat_id: C) -> Self + where + C: Into<ChatId>, + { + self.chat_id = chat_id.into(); + self + } + + pub fn title<C>(mut self, title: C) -> Self + where + C: Into<String>, + { + self.title = title.into(); + self + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn serialize() { + let bot = Bot::new("token"); + let chat_id = 123; + let title = "title"; + let method = SetChatTitle::new(&bot, chat_id, title); + + let expected = r#"{"chat_id":123,"title":"title"}"#; + let actual = serde_json::to_string::<SetChatTitle>(&method).unwrap(); + assert_eq!(actual, expected); + } +} diff --git a/src/requests/stop_message_live_location.rs b/src/requests/stop_message_live_location.rs index 5455944e..7ded4251 100644 --- a/src/requests/stop_message_live_location.rs +++ b/src/requests/stop_message_live_location.rs @@ -1,8 +1,9 @@ use async_trait::async_trait; use crate::{ + bot::Bot, network, - requests::{Request, RequestContext, ResponseResult}, + requests::{Request, ResponseResult}, types::{ChatId, InlineKeyboardMarkup, Message}, }; @@ -12,7 +13,7 @@ use crate::{ #[derive(Debug, Clone, Serialize)] pub struct StopMessageLiveLocation<'a> { #[serde(skip_serializing)] - ctx: RequestContext<'a>, + bot: &'a Bot, /// Required if inline_message_id is not specified. Unique identifier for /// the target chat or username of the target channel (in the format /// @channelusername) @@ -44,8 +45,8 @@ impl Request for StopMessageLiveLocation<'_> { impl StopMessageLiveLocation<'_> { pub async fn send(self) -> ResponseResult<Message> { network::request_json( - &self.ctx.client, - &self.ctx.token, + self.bot.client(), + self.bot.token(), "stopMessageLiveLocation", &self, ) @@ -54,9 +55,9 @@ impl StopMessageLiveLocation<'_> { } impl<'a> StopMessageLiveLocation<'a> { - pub(crate) fn new(ctx: RequestContext<'a>) -> Self { + pub(crate) fn new(bot: &'a Bot) -> Self { Self { - ctx, + bot, chat_id: None, message_id: None, inline_message_id: None, diff --git a/src/requests/unban_chat_member.rs b/src/requests/unban_chat_member.rs index fb772d0d..2af14c12 100644 --- a/src/requests/unban_chat_member.rs +++ b/src/requests/unban_chat_member.rs @@ -1,8 +1,9 @@ use async_trait::async_trait; use crate::{ + bot::Bot, network, - requests::{Request, RequestContext, ResponseResult}, + requests::{Request, ResponseResult}, types::ChatId, }; @@ -13,7 +14,7 @@ use crate::{ #[derive(Debug, Clone, Serialize)] pub struct UnbanChatMember<'a> { #[serde(skip_serializing)] - ctx: RequestContext<'a>, + bot: &'a Bot, ///Unique identifier for the target group or username of the target /// supergroup or channel (in the format @channelusername) pub chat_id: ChatId, @@ -33,8 +34,8 @@ impl Request for UnbanChatMember<'_> { impl UnbanChatMember<'_> { pub async fn send(self) -> ResponseResult<bool> { network::request_json( - &self.ctx.client, - &self.ctx.token, + self.bot.client(), + self.bot.token(), "unbanChatMember", &self, ) @@ -43,17 +44,13 @@ impl UnbanChatMember<'_> { } impl<'a> UnbanChatMember<'a> { - pub(crate) fn new<C, U>( - ctx: RequestContext<'a>, - chat_id: C, - user_id: U, - ) -> Self + pub(crate) fn new<C, U>(bot: &'a Bot, chat_id: C, user_id: U) -> Self where C: Into<ChatId>, U: Into<i32>, { Self { - ctx, + bot, chat_id: chat_id.into(), user_id: user_id.into(), } diff --git a/src/requests/unpin_chat_message.rs b/src/requests/unpin_chat_message.rs index 6bde53d0..1e7f9ef6 100644 --- a/src/requests/unpin_chat_message.rs +++ b/src/requests/unpin_chat_message.rs @@ -1,15 +1,16 @@ use async_trait::async_trait; use crate::{ + bot::Bot, network, - requests::{Request, RequestContext, ResponseResult}, + requests::{Request, ResponseResult}, types::{ChatId, True}, }; #[derive(Debug, Clone, Serialize)] pub struct UnpinChatMessage<'a> { #[serde(skip_serializing)] - pub ctx: RequestContext<'a>, + bot: &'a Bot, pub chat_id: ChatId, } @@ -26,8 +27,8 @@ impl Request for UnpinChatMessage<'_> { impl UnpinChatMessage<'_> { pub async fn send(self) -> ResponseResult<True> { network::request_json( - &self.ctx.client, - &self.ctx.token, + self.bot.client(), + self.bot.token(), "unpinChatMessage", &self, ) @@ -36,12 +37,12 @@ impl UnpinChatMessage<'_> { } impl<'a> UnpinChatMessage<'a> { - pub(crate) fn new<C>(ctx: RequestContext<'a>, value: C) -> Self + pub(crate) fn new<C>(bot: &'a Bot, value: C) -> Self where C: Into<ChatId>, { Self { - ctx, + bot, chat_id: value.into(), } } diff --git a/src/types/chat_permissions.rs b/src/types/chat_permissions.rs index 08a5b908..869398aa 100644 --- a/src/types/chat_permissions.rs +++ b/src/types/chat_permissions.rs @@ -1,11 +1,19 @@ #[derive(Debug, Deserialize, Hash, PartialEq, Eq, Serialize, Clone)] pub struct ChatPermissions { + #[serde(skip_serializing_if = "Option::is_none")] pub can_send_messages: Option<bool>, + #[serde(skip_serializing_if = "Option::is_none")] pub can_send_media_messages: Option<bool>, + #[serde(skip_serializing_if = "Option::is_none")] pub can_send_polls: Option<bool>, + #[serde(skip_serializing_if = "Option::is_none")] pub can_send_other_messages: Option<bool>, + #[serde(skip_serializing_if = "Option::is_none")] pub can_add_web_page_previews: Option<bool>, + #[serde(skip_serializing_if = "Option::is_none")] pub can_change_info: Option<bool>, + #[serde(skip_serializing_if = "Option::is_none")] pub can_invite_users: Option<bool>, + #[serde(skip_serializing_if = "Option::is_none")] pub can_pin_messages: Option<bool>, } diff --git a/src/types/encrypted_credintials.rs b/src/types/encrypted_credentials.rs similarity index 100% rename from src/types/encrypted_credintials.rs rename to src/types/encrypted_credentials.rs diff --git a/src/types/encrypted_passport_element.rs b/src/types/encrypted_passport_element.rs index 425ff429..5788d047 100644 --- a/src/types/encrypted_passport_element.rs +++ b/src/types/encrypted_passport_element.rs @@ -1,4 +1,4 @@ -use super::passport_file::PassportFile; +use super::PassportFile; #[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] pub struct EncryptedPassportElement { diff --git a/src/types/inline_keyboard_button.rs b/src/types/inline_keyboard_button.rs index 27ef8896..935a38c4 100644 --- a/src/types/inline_keyboard_button.rs +++ b/src/types/inline_keyboard_button.rs @@ -45,7 +45,7 @@ pub enum InlineKeyboardButtonKind { /// /// Example: /// ``` -/// use async_telegram_bot::types::InlineKeyboardButton; +/// use telebofr::types::InlineKeyboardButton; /// /// let url_button = InlineKeyboardButton::url( /// "Text".to_string(), diff --git a/src/types/inline_keyboard_markup.rs b/src/types/inline_keyboard_markup.rs index 67bd2aa8..2ca92ee4 100644 --- a/src/types/inline_keyboard_markup.rs +++ b/src/types/inline_keyboard_markup.rs @@ -16,9 +16,7 @@ pub struct InlineKeyboardMarkup { /// /// Example: /// ``` -/// use async_telegram_bot::types::{ -/// InlineKeyboardButton, InlineKeyboardMarkup, -/// }; +/// use telebofr::types::{InlineKeyboardButton, InlineKeyboardMarkup}; /// /// let url_button = InlineKeyboardButton::url( /// "text".to_string(), diff --git a/src/types/inline_query_result.rs b/src/types/inline_query_result.rs index f4644b79..098d0dc4 100644 --- a/src/types/inline_query_result.rs +++ b/src/types/inline_query_result.rs @@ -49,9 +49,8 @@ pub enum InlineQueryResult { #[cfg(test)] mod tests { - use crate::types::inline_keyboard_markup::InlineKeyboardMarkup; - use crate::types::parse_mode::ParseMode; use crate::types::{ + inline_keyboard_markup::InlineKeyboardMarkup, parse_mode::ParseMode, InlineQueryResult, InlineQueryResultCachedAudio, InputMessageContent, }; diff --git a/src/types/keyboard_button.rs b/src/types/keyboard_button.rs index 41d1b156..01929838 100644 --- a/src/types/keyboard_button.rs +++ b/src/types/keyboard_button.rs @@ -1,4 +1,4 @@ -/// This object represents one button of the reply keyboard. For simple text +/// This object represents one button of the reply keyboard. For filter text /// buttons String can be used instead of this object to specify text of the /// button. Optional fields are mutually exclusive. #[derive(Debug, Serialize, Deserialize, Hash, PartialEq, Eq, Clone)] diff --git a/src/types/message.rs b/src/types/message.rs index c658dd42..61bebed0 100644 --- a/src/types/message.rs +++ b/src/types/message.rs @@ -1,4 +1,8 @@ -use crate::types::{Animation, Audio, Chat, Contact, Document, Game, InlineKeyboardMarkup, Invoice, Location, MessageEntity, PassportData, PhotoSize, Poll, Sticker, SuccessfulPayment, User, Venue, Video, VideoNote, Voice, True}; +use crate::types::{ + Animation, Audio, Chat, Contact, Document, Game, InlineKeyboardMarkup, + Invoice, Location, MessageEntity, PassportData, PhotoSize, Poll, Sticker, + SuccessfulPayment, True, User, Venue, Video, VideoNote, Voice, +}; #[derive(Debug, Deserialize, PartialEq, Clone)] pub struct Message { @@ -10,7 +14,6 @@ pub struct Message { pub kind: MessageKind, } - #[derive(Debug, Deserialize, PartialEq, Clone)] #[serde(untagged)] pub enum MessageKind { @@ -188,23 +191,23 @@ mod getters { use std::ops::Deref; use crate::types::{ - self, Message, Sender, User, ForwardedFrom, Chat, MessageEntity, - PhotoSize, True, + self, message::{ - MessageKind::{ - Common, NewChatMembers, LeftChatMember, NewChatTitle, - NewChatPhoto, DeleteChatPhoto, GroupChatCreated, - ChannelChatCreated, Migrate, Invoice, SuccessfulPayment, - ConnectedWebsite, PassportData - }, + ForwardKind::{ChannelForward, NonChannelForward, Origin}, MediaKind::{ - Text, Video, Photo, Animation, Audio, Document, Voice, Game, - Sticker, VideoNote, Contact, Location, Poll, Venue + Animation, Audio, Contact, Document, Game, Location, Photo, + Poll, Sticker, Text, Venue, Video, VideoNote, Voice, + }, + MessageKind::{ + ChannelChatCreated, Common, ConnectedWebsite, DeleteChatPhoto, + GroupChatCreated, Invoice, LeftChatMember, Migrate, + NewChatMembers, NewChatPhoto, NewChatTitle, PassportData, + Pinned, SuccessfulPayment, SupergroupChatCreated, }, - ForwardKind::{NonChannelForward, ChannelForward, Origin} }, + Chat, ForwardedFrom, Message, MessageEntity, PhotoSize, Sender, True, + User, }; - use crate::types::message::MessageKind::{SupergroupChatCreated, Pinned}; /// Getters for [Message] fields from [telegram docs]. /// @@ -223,49 +226,67 @@ mod getters { /// `forward_sender_name` pub fn forward_from(&self) -> Option<&ForwardedFrom> { match &self.kind { - Common { forward_kind: NonChannelForward { from, .. }, .. } => - Some(from), + Common { + forward_kind: NonChannelForward { from, .. }, + .. + } => Some(from), _ => None, } } pub fn forward_from_chat(&self) -> Option<&Chat> { match &self.kind { - Common { forward_kind: ChannelForward { chat, .. }, .. } => - Some(chat), + Common { + forward_kind: ChannelForward { chat, .. }, + .. + } => Some(chat), _ => None, } } pub fn forward_from_message_id(&self) -> Option<&i32> { match &self.kind { - Common { forward_kind: ChannelForward { message_id, .. }, .. } => - Some(message_id), + Common { + forward_kind: ChannelForward { message_id, .. }, + .. + } => Some(message_id), _ => None, } } pub fn forward_signature(&self) -> Option<&str> { match &self.kind { - Common { forward_kind: ChannelForward { signature, .. }, .. } => - signature.as_ref().map(Deref::deref), + Common { + forward_kind: ChannelForward { signature, .. }, + .. + } => signature.as_ref().map(Deref::deref), _ => None, } } pub fn forward_date(&self) -> Option<&i32> { match &self.kind { - Common { forward_kind: ChannelForward { date, .. }, .. } | - Common { forward_kind: NonChannelForward { date, .. }, .. } => - Some(date), + Common { + forward_kind: ChannelForward { date, .. }, + .. + } + | Common { + forward_kind: NonChannelForward { date, .. }, + .. + } => Some(date), _ => None, } } pub fn reply_to_message(&self) -> Option<&Message> { match &self.kind { - Common { forward_kind: Origin { reply_to_message, .. }, .. } => - reply_to_message.as_ref().map(Deref::deref), + Common { + forward_kind: + Origin { + reply_to_message, .. + }, + .. + } => reply_to_message.as_ref().map(Deref::deref), _ => None, } } @@ -279,104 +300,172 @@ mod getters { pub fn media_group_id(&self) -> Option<&str> { match &self.kind { - Common { media_kind: Video { media_group_id, .. }, .. } | - Common { media_kind: Photo { media_group_id, .. }, .. } => - media_group_id.as_ref().map(Deref::deref), + Common { + media_kind: Video { media_group_id, .. }, + .. + } + | Common { + media_kind: Photo { media_group_id, .. }, + .. + } => media_group_id.as_ref().map(Deref::deref), _ => None, } } pub fn text(&self) -> Option<&str> { match &self.kind { - Common { media_kind: Text { text, .. }, .. } => Some(text), + Common { + media_kind: Text { text, .. }, + .. + } => Some(text), _ => None, } } pub fn entities(&self) -> Option<&[MessageEntity]> { match &self.kind { - Common { media_kind: Text { entities, .. }, .. } => - Some(entities), + Common { + media_kind: Text { entities, .. }, + .. + } => Some(entities), _ => None, } } pub fn caption_entities(&self) -> Option<&[MessageEntity]> { match &self.kind { - Common { media_kind: Animation { caption_entities, .. }, .. } | - Common { media_kind: Audio { caption_entities, .. }, .. } | - Common { media_kind: Document { caption_entities, .. }, .. } | - Common { media_kind: Photo { caption_entities, .. }, .. } | - Common { media_kind: Video { caption_entities, .. }, .. } | - Common { media_kind: Voice { caption_entities, .. }, .. } => - Some(caption_entities), + Common { + media_kind: + Animation { + caption_entities, .. + }, + .. + } + | Common { + media_kind: + Audio { + caption_entities, .. + }, + .. + } + | Common { + media_kind: + Document { + caption_entities, .. + }, + .. + } + | Common { + media_kind: + Photo { + caption_entities, .. + }, + .. + } + | Common { + media_kind: + Video { + caption_entities, .. + }, + .. + } + | Common { + media_kind: + Voice { + caption_entities, .. + }, + .. + } => Some(caption_entities), _ => None, } } pub fn audio(&self) -> Option<&types::Audio> { match &self.kind { - Common { media_kind: Audio { audio, .. }, .. } => Some(audio), + Common { + media_kind: Audio { audio, .. }, + .. + } => Some(audio), _ => None, } } pub fn document(&self) -> Option<&types::Document> { match &self.kind { - Common { media_kind: Document { document, .. }, .. } => - Some(document), + Common { + media_kind: Document { document, .. }, + .. + } => Some(document), _ => None, } } pub fn animation(&self) -> Option<&types::Animation> { match &self.kind { - Common { media_kind: Animation { animation, .. }, .. } => - Some(animation), + Common { + media_kind: Animation { animation, .. }, + .. + } => Some(animation), _ => None, } } pub fn game(&self) -> Option<&types::Game> { match &self.kind { - Common { media_kind: Game { game, .. }, .. } => Some(game), + Common { + media_kind: Game { game, .. }, + .. + } => Some(game), _ => None, } } pub fn photo(&self) -> Option<&[PhotoSize]> { match &self.kind { - Common { media_kind: Photo { photo, .. }, .. } => Some(photo), + Common { + media_kind: Photo { photo, .. }, + .. + } => Some(photo), _ => None, } } pub fn sticker(&self) -> Option<&types::Sticker> { match &self.kind { - Common { media_kind: Sticker { sticker, .. }, .. } => - Some(sticker), + Common { + media_kind: Sticker { sticker, .. }, + .. + } => Some(sticker), _ => None, } } pub fn video(&self) -> Option<&types::Video> { match &self.kind { - Common { media_kind: Video { video, .. }, .. } => Some(video), + Common { + media_kind: Video { video, .. }, + .. + } => Some(video), _ => None, } } pub fn voice(&self) -> Option<&types::Voice> { match &self.kind { - Common { media_kind: Voice { voice, .. }, .. } => Some(voice), + Common { + media_kind: Voice { voice, .. }, + .. + } => Some(voice), _ => None, } } pub fn video_note(&self) -> Option<&types::VideoNote> { match &self.kind { - Common { media_kind: VideoNote { video_note, .. }, .. } => - Some(video_note), + Common { + media_kind: VideoNote { video_note, .. }, + .. + } => Some(video_note), _ => None, } } @@ -384,12 +473,14 @@ mod getters { pub fn caption(&self) -> Option<&str> { match &self.kind { Common { media_kind, .. } => match media_kind { - Animation { caption, ..} | - Audio { caption, ..} | - Document { caption, ..} | - Photo { caption, ..} | - Video { caption, ..} | - Voice { caption, ..} => caption.as_ref().map(Deref::deref), + Animation { caption, .. } + | Audio { caption, .. } + | Document { caption, .. } + | Photo { caption, .. } + | Video { caption, .. } + | Voice { caption, .. } => { + caption.as_ref().map(Deref::deref) + } _ => None, }, _ => None, @@ -398,29 +489,40 @@ mod getters { pub fn contact(&self) -> Option<&types::Contact> { match &self.kind { - Common { media_kind: Contact { contact }, .. } => Some(contact), + Common { + media_kind: Contact { contact }, + .. + } => Some(contact), _ => None, } } pub fn location(&self) -> Option<&types::Location> { match &self.kind { - Common { media_kind: Location { location, .. }, .. } => - Some(location), + Common { + media_kind: Location { location, .. }, + .. + } => Some(location), _ => None, } } pub fn venue(&self) -> Option<&types::Venue> { match &self.kind { - Common { media_kind: Venue { venue, .. }, .. } => Some(venue), + Common { + media_kind: Venue { venue, .. }, + .. + } => Some(venue), _ => None, } } pub fn poll(&self) -> Option<&types::Poll> { match &self.kind { - Common { media_kind: Poll { poll, .. }, .. } => Some(poll), + Common { + media_kind: Poll { poll, .. }, + .. + } => Some(poll), _ => None, } } @@ -457,47 +559,55 @@ mod getters { // mb smt like `is_delete_chat_photo(&self) -> bool`? pub fn delete_chat_photo(&self) -> Option<True> { match &self.kind { - DeleteChatPhoto { delete_chat_photo } => - Some(*delete_chat_photo), + DeleteChatPhoto { delete_chat_photo } => { + Some(*delete_chat_photo) + } _ => None, } } pub fn group_chat_created(&self) -> Option<True> { match &self.kind { - GroupChatCreated { group_chat_created } => - Some(*group_chat_created), + GroupChatCreated { group_chat_created } => { + Some(*group_chat_created) + } _ => None, } } pub fn super_group_chat_created(&self) -> Option<True> { match &self.kind { - SupergroupChatCreated { supergroup_chat_created } => - Some(*supergroup_chat_created), + SupergroupChatCreated { + supergroup_chat_created, + } => Some(*supergroup_chat_created), _ => None, } } pub fn channel_chat_created(&self) -> Option<True> { match &self.kind { - ChannelChatCreated { channel_chat_created } => - Some(*channel_chat_created), + ChannelChatCreated { + channel_chat_created, + } => Some(*channel_chat_created), _ => None, } } pub fn migrate_to_chat_id(&self) -> Option<&i64> { match &self.kind { - Migrate { migrate_to_chat_id, .. } => Some(migrate_to_chat_id), + Migrate { + migrate_to_chat_id, .. + } => Some(migrate_to_chat_id), _ => None, } } pub fn migrate_from_chat_id(&self) -> Option<&i64> { match &self.kind { - Migrate { migrate_from_chat_id, .. } => - Some(migrate_from_chat_id), + Migrate { + migrate_from_chat_id, + .. + } => Some(migrate_from_chat_id), _ => None, } } @@ -516,25 +626,24 @@ mod getters { } } - pub fn successful_payment(&self) -> Option<&types::SuccessfulPayment> { match &self.kind { - SuccessfulPayment { successful_payment } => - Some(successful_payment), + SuccessfulPayment { successful_payment } => { + Some(successful_payment) + } _ => None, } } - pub fn connected_website(&self) -> Option<&str> { match &self.kind { - ConnectedWebsite { connected_website } => - Some(connected_website), + ConnectedWebsite { connected_website } => { + Some(connected_website) + } _ => None, } } - pub fn passport_data(&self) -> Option<&types::PassportData> { match &self.kind { PassportData { passport_data } => Some(passport_data), @@ -542,7 +651,6 @@ mod getters { } } - pub fn reply_markup(&self) -> Option<&types::InlineKeyboardMarkup> { match &self.kind { Common { reply_markup, .. } => reply_markup.as_ref(), @@ -552,7 +660,6 @@ mod getters { } } - #[cfg(test)] mod tests { use serde_json::from_str; diff --git a/src/types/mod.rs b/src/types/mod.rs index b3268627..075b1dfc 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -1,92 +1,87 @@ -//! Raw API structures. +//! API types. -pub use self::{ - animation::Animation, - audio::Audio, - callback_game::CallbackGame, - callback_query::CallbackQuery, - chat::{Chat, ChatKind, NonPrivateChatKind}, - chat_action::ChatAction, - chat_id::ChatId, - chat_member::{ChatMember, ChatMemberStatus}, - chat_permissions::ChatPermissions, - chat_photo::ChatPhoto, - chosen_inline_result::ChosenInlineResult, - contact::Contact, - document::Document, - encrypted_credintials::EncryptedCredentials, - encrypted_passport_element::{ - EncryptedPassportElement, EncryptedPassportElementKind, - }, - file::File, - force_reply::ForceReply, - game::Game, - game_high_score::GameHighScore, - inline_keyboard_button::{InlineKeyboardButton, InlineKeyboardButtonKind}, - inline_keyboard_markup::InlineKeyboardMarkup, - inline_query::InlineQuery, - inline_query_result::InlineQueryResult, - inline_query_result_article::InlineQueryResultArticle, - inline_query_result_audio::InlineQueryResultAudio, - inline_query_result_cached_audio::InlineQueryResultCachedAudio, - inline_query_result_cached_document::InlineQueryResultCachedDocument, - inline_query_result_cached_gif::InlineQueryResultCachedGif, - inline_query_result_cached_mpeg4_gif::InlineQueryResultCachedMpeg4Gif, - inline_query_result_cached_photo::InlineQueryResultCachedPhoto, - inline_query_result_cached_sticker::InlineQueryResultCachedSticker, - inline_query_result_cached_video::InlineQueryResultCachedVideo, - inline_query_result_cached_voice::InlineQueryResultCachedVoice, - inline_query_result_contact::InlineQueryResultContact, - inline_query_result_document::InlineQueryResultDocument, - inline_query_result_game::InlineQueryResultGame, - inline_query_result_gif::InlineQueryResultGif, - inline_query_result_location::InlineQueryResultLocation, - inline_query_result_mpeg4_gif::InlineQueryResultMpeg4Gif, - inline_query_result_photo::InlineQueryResultPhoto, - inline_query_result_venue::InlineQueryResultVenue, - inline_query_result_video::InlineQueryResultVideo, - inline_query_result_voice::InlineQueryResultVoice, - input_file::InputFile, - input_media::InputMedia, - input_message_content::InputMessageContent, - invoice::Invoice, - keyboard_button::KeyboardButton, - label_price::LabeledPrice, - location::Location, - login_url::LoginUrl, - mask_position::MaskPosition, - message::{ - ForwardKind, ForwardedFrom, MediaKind, Message, MessageKind, Sender, - }, - message_entity::MessageEntity, - order_info::OrderInfo, - parse_mode::ParseMode, - passport_data::PassportData, - passport_file::PassportFile, - photo_size::PhotoSize, - poll::{Poll, PollOption}, - pre_checkout_query::PreCheckoutQuery, - reply_keyboard_markup::ReplyKeyboardMarkup, - reply_keyboard_remove::ReplyKeyboardRemove, - reply_markup::ReplyMarkup, - response_parameters::ResponseParameters, - send_invoice::SendInvoice, - shipping_address::ShippingAddress, - shipping_option::ShippingOption, - shipping_query::ShippingQuery, - sticker::Sticker, - sticker_set::StickerSet, - successful_payment::SuccessfulPayment, - unit_true::True, - update::{Update, UpdateKind}, - user::User, - user_profile_photos::UserProfilePhotos, - venue::Venue, - video::Video, - video_note::VideoNote, - voice::Voice, - webhook_info::WebhookInfo, -}; +pub use animation::*; +pub use audio::*; +pub use callback_game::*; +pub use callback_query::*; +pub use chat::*; +pub use chat_action::*; +pub use chat_id::*; +pub use chat_member::*; +pub use chat_permissions::*; +pub use chat_photo::*; +pub use chosen_inline_result::*; +pub use contact::*; +pub use document::*; +pub use encrypted_credentials::*; +pub use encrypted_passport_element::*; +pub use file::*; +pub use force_reply::*; +pub use game::*; +pub use game_high_score::*; +pub use inline_keyboard_button::*; +pub use inline_keyboard_markup::*; +pub use inline_query::*; +pub use inline_query_result::*; +pub use inline_query_result_article::*; +pub use inline_query_result_audio::*; +pub use inline_query_result_cached_audio::*; +pub use inline_query_result_cached_document::*; +pub use inline_query_result_cached_gif::*; +pub use inline_query_result_cached_mpeg4_gif::*; +pub use inline_query_result_cached_photo::*; +pub use inline_query_result_cached_sticker::*; +pub use inline_query_result_cached_video::*; +pub use inline_query_result_cached_voice::*; +pub use inline_query_result_contact::*; +pub use inline_query_result_document::*; +pub use inline_query_result_game::*; +pub use inline_query_result_gif::*; +pub use inline_query_result_location::*; +pub use inline_query_result_mpeg4_gif::*; +pub use inline_query_result_photo::*; +pub use inline_query_result_venue::*; +pub use inline_query_result_video::*; +pub use inline_query_result_voice::*; +pub use input_file::*; +pub use input_media::*; +pub use input_message_content::*; +pub use invoice::*; +pub use keyboard_button::*; +pub use label_price::*; +pub use location::*; +pub use login_url::*; +pub use mask_position::*; +pub use message::*; +pub use message_entity::*; +pub use order_info::*; +pub use parse_mode::*; +pub use passport_data::*; +pub use passport_file::*; +pub use photo_size::*; +pub use poll::*; +pub use pre_checkout_query::*; +pub use reply_keyboard_markup::*; +pub use reply_keyboard_remove::*; +pub use reply_markup::*; +pub use response_parameters::*; +pub use send_invoice::*; +pub use shipping_address::*; +pub use shipping_option::*; +pub use shipping_query::*; +pub use sticker::*; +pub use sticker_set::*; +pub use successful_payment::*; +pub use unit_false::*; +pub use unit_true::*; +pub use update::*; +pub use user::*; +pub use user_profile_photos::*; +pub use venue::*; +pub use video::*; +pub use video_note::*; +pub use voice::*; +pub use webhook_info::*; mod animation; mod audio; @@ -134,6 +129,7 @@ mod shipping_query; mod sticker; mod sticker_set; mod successful_payment; +mod unit_false; mod unit_true; mod update; mod user; @@ -167,7 +163,7 @@ mod inline_query_result_venue; mod inline_query_result_video; mod inline_query_result_voice; -mod encrypted_credintials; +mod encrypted_credentials; mod encrypted_passport_element; mod passport_data; mod passport_file; diff --git a/src/types/passport_data.rs b/src/types/passport_data.rs index 8ca8e20c..4c938af8 100644 --- a/src/types/passport_data.rs +++ b/src/types/passport_data.rs @@ -1,5 +1,4 @@ -use super::encrypted_credintials::EncryptedCredentials; -use super::encrypted_passport_element::EncryptedPassportElement; +use super::{EncryptedCredentials, EncryptedPassportElement}; #[derive(Debug, Deserialize, Eq, Hash, PartialEq, Clone, Serialize)] pub struct PassportData { diff --git a/src/types/unit_false.rs b/src/types/unit_false.rs new file mode 100644 index 00000000..f5039f8e --- /dev/null +++ b/src/types/unit_false.rs @@ -0,0 +1,80 @@ +use serde::{de::Visitor, Deserialize, Deserializer, Serialize, Serializer}; + +#[derive(Copy, Clone, Debug, Default, Eq, Hash, PartialEq)] +pub struct False; + +impl std::convert::TryFrom<bool> for False { + type Error = (); + + fn try_from(value: bool) -> Result<Self, Self::Error> { + #[allow(clippy::match_bool)] + match value { + true => Err(()), + false => Ok(False), + } + } +} + +impl<'de> Deserialize<'de> for False { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: Deserializer<'de>, + { + deserializer.deserialize_bool(FalseVisitor) + } +} + +struct FalseVisitor; + +impl<'de> Visitor<'de> for FalseVisitor { + type Value = False; + + fn expecting( + &self, + formatter: &mut std::fmt::Formatter, + ) -> std::fmt::Result { + write!(formatter, "bool, equal to `false`") + } + + fn visit_bool<E>(self, value: bool) -> Result<Self::Value, E> + where + E: serde::de::Error, + { + #[allow(clippy::match_bool)] + match value { + true => Err(E::custom("expected `false`, found `true`")), + false => Ok(False), + } + } +} + +impl Serialize for False { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: Serializer, + { + serializer.serialize_bool(false) + } +} + +#[cfg(test)] +mod tests { + use serde_json::{from_str, to_string}; + + use super::False; + + #[test] + fn unit_false_de() { + let json = "false"; + let expected = False; + let actual = from_str(json).unwrap(); + assert_eq!(expected, actual); + } + + #[test] + fn unit_false_se() { + let actual = to_string(&False).unwrap(); + let expected = "false"; + assert_eq!(expected, actual); + } +} diff --git a/src/types/unit_true.rs b/src/types/unit_true.rs index 0efe1b33..ed953d46 100644 --- a/src/types/unit_true.rs +++ b/src/types/unit_true.rs @@ -1,7 +1,9 @@ -use serde::de::{self, Deserialize, Deserializer, Visitor}; -use serde::ser::{Serialize, Serializer}; +use serde::{ + de::{self, Deserialize, Deserializer, Visitor}, + ser::{Serialize, Serializer}, +}; -#[derive(Copy, Clone, Debug, Default, Eq, Hash, PartialEq, PartialOrd, Ord)] +#[derive(Copy, Clone, Debug, Default, Eq, Hash, PartialEq)] pub struct True; impl std::convert::TryFrom<bool> for True {