diff --git a/Cargo.toml b/Cargo.toml index e40bdb28..85ab027a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,16 +6,20 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -reqwest = { git = "https://github.com/seanmonstar/reqwest", rev = "ba7b2a754eab0d79817ea8551d0803806ae8af7d" } -serde_json = "1.0.39" -serde = {version = "1.0.92", features = ["derive"] } -lazy_static = "1.3" +reqwest = { version = "0.10.0-alpha.1", features = ["json", "unstable-stream"] } +serde_json = "1.0.41" +serde = { version = "1.0.101", features = ["derive"] } apply = "0.2.2" derive_more = "0.15.0" -tokio = "0.2.0-alpha.4" +tokio = "0.2.0-alpha.6" bytes = "0.4.12" log = "0.4.8" -futures-preview = "0.3.0-alpha.18" pin-project = "0.4.0-alpha.7" +futures-preview = "0.3.0-alpha.19" async-trait = "0.1.13" -libc = "0.2.62" +thiserror = "1.0.2" + +[features] +default = [] + +unstable-stream = [] # add streams to public API \ No newline at end of file diff --git a/ICON.jpg b/ICON.jpg new file mode 100644 index 00000000..64d69180 Binary files /dev/null and b/ICON.jpg differ diff --git a/README.md b/README.md index 6e0c581c..384dc706 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,30 @@

async-telegram-bot

+ + + + -
- -## Dependency graph -
- + + + + + + + +
+ +
+ + Lorem ipsum dolor sit amet consectetur adipiscing elit nibh cubilia, nostra ultricies class torquent integer scelerisque netus euismod vehicula, metus aliquam morbi leo risus cursus massa potenti. + +

+ Pulse · + Stargazers · + Releases · + Contributing +

diff --git a/graph.png b/graph.png deleted file mode 100644 index 8c66df6f..00000000 Binary files a/graph.png and /dev/null differ diff --git a/src/bot/api.rs b/src/bot/api.rs index 0472d84d..a75328c1 100644 --- a/src/bot/api.rs +++ b/src/bot/api.rs @@ -1,11 +1,14 @@ use crate::{ bot::Bot, requests::{ - ChatId, EditMessageLiveLocation, ForwardMessage, GetFile, GetMe, - SendAudio, SendLocation, SendMediaGroup, SendMessage, SendPhoto, - StopMessageLiveLocation, GetUpdates + 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 }, - types::{InputFile, InputMedia}, + types::{ChatAction, ChatId, ChatPermissions, InputFile, InputMedia}, }; /// Telegram functions @@ -23,7 +26,7 @@ impl Bot { C: Into, T: Into, { - SendMessage::new(self.ctx(), chat_id.into(), text.into()) + SendMessage::new(self.ctx(), chat_id, text) } pub fn edit_message_live_location( @@ -35,11 +38,7 @@ impl Bot { Lt: Into, Lg: Into, { - EditMessageLiveLocation::new( - self.ctx(), - latitude.into(), - longitude.into(), - ) + EditMessageLiveLocation::new(self.ctx(), latitude, longitude) } pub fn forward_message( @@ -53,12 +52,7 @@ impl Bot { F: Into, M: Into, { - ForwardMessage::new( - self.ctx(), - chat_id.into(), - from_chat_id.into(), - message_id.into(), - ) + ForwardMessage::new(self.ctx(), chat_id, from_chat_id, message_id) } pub fn send_audio(&self, chat_id: C, audio: A) -> SendAudio @@ -66,7 +60,7 @@ impl Bot { C: Into, A: Into, { - SendAudio::new(self.ctx(), chat_id.into(), audio.into()) + SendAudio::new(self.ctx(), chat_id, audio) } pub fn send_location( @@ -80,12 +74,7 @@ impl Bot { Lt: Into, Lg: Into, { - SendLocation::new( - self.ctx(), - chat_id.into(), - latitude.into(), - longitude.into(), - ) + SendLocation::new(self.ctx(), chat_id, latitude, longitude) } pub fn send_media_group(&self, chat_id: C, media: M) -> SendMediaGroup @@ -93,7 +82,7 @@ impl Bot { C: Into, M: Into>, { - SendMediaGroup::new(self.ctx(), chat_id.into(), media.into()) + SendMediaGroup::new(self.ctx(), chat_id, media) } pub fn send_photo(&self, chat_id: C, photo: P) -> SendPhoto @@ -101,7 +90,7 @@ impl Bot { C: Into, P: Into, { - SendPhoto::new(self.ctx(), chat_id.into(), photo.into()) + SendPhoto::new(self.ctx(), chat_id, photo) } pub fn stop_message_live_location(&self) -> StopMessageLiveLocation { @@ -112,6 +101,177 @@ impl Bot { where F: Into, { - GetFile::new(self.ctx(), file_id.into()) + GetFile::new(self.ctx(), file_id) + } + + pub fn answer_pre_checkout_query( + &self, + pre_checkout_query_id: I, + ok: O, + ) -> AnswerPreCheckoutQuery + where + I: Into, + O: Into, + { + AnswerPreCheckoutQuery::new(self.ctx(), pre_checkout_query_id, ok) + } + + pub fn answer_shipping_query( + &self, + shipping_query_id: I, + ok: O, + ) -> AnswerShippingQuery + where + I: Into, + O: Into, + { + AnswerShippingQuery::new(self.ctx(), shipping_query_id, ok) + } + + pub fn kick_chat_member( + &self, + chat_id: C, + user_id: U, + ) -> KickChatMember + where + C: Into, + U: Into, + { + KickChatMember::new(self.ctx(), chat_id, user_id) + } + + pub fn pin_chat_message( + &self, + chat_id: C, + message_id: M, + ) -> PinChatMessage + where + C: Into, + M: Into, + { + PinChatMessage::new(self.ctx(), chat_id, message_id) + } + + pub fn promote_chat_member( + &self, + chat_id: C, + user_id: U, + ) -> PromoteChatMember + where + C: Into, + U: Into, + { + PromoteChatMember::new(self.ctx(), chat_id, user_id) + } + + pub fn restrict_chat_member( + &self, + chat_id: C, + user_id: U, + permissions: P, + ) -> RestrictChatMember + where + C: Into, + U: Into, + P: Into, + { + RestrictChatMember::new(self.ctx(), chat_id, user_id, permissions) + } + + pub fn send_chat_action( + &self, + chat_id: C, + action: A, + ) -> SendChatAction + where + C: Into, + A: Into, + { + SendChatAction::new(self.ctx(), chat_id, action) + } + + pub fn send_contact( + &self, + chat_id: C, + phone_number: P, + first_name: F, + ) -> SendContact + where + C: Into, + P: Into, + F: Into, + { + SendContact::new(self.ctx(), chat_id, phone_number, first_name) + } + + pub fn send_poll( + &self, + chat_id: C, + question: Q, + options: O, + ) -> SendPoll + where + C: Into, + Q: Into, + O: Into>, + { + SendPoll::new(self.ctx(), chat_id, question, options) + } + + pub fn send_venue( + &self, + chat_id: C, + latitude: Lt, + longitude: Lg, + title: T, + address: A, + ) -> SendVenue + where + C: Into, + Lt: Into, + Lg: Into, + T: Into, + A: Into, + { + SendVenue::new(self.ctx(), chat_id, latitude, longitude, title, address) + } + + pub fn send_video_note( + &self, + chat_id: C, + video_note: V, + ) -> SendVideoNote + where + C: Into, + V: Into, // TODO: InputFile + { + SendVideoNote::new(self.ctx(), chat_id, video_note) + } + + pub fn send_voice(&self, chat_id: C, voice: V) -> SendVoice + where + C: Into, + V: Into, // TODO: InputFile + { + SendVoice::new(self.ctx(), chat_id, voice) + } + + pub fn unban_chat_member( + &self, + chat_id: C, + user_id: U, + ) -> UnbanChatMember + where + C: Into, + U: Into, + { + UnbanChatMember::new(self.ctx(), chat_id, user_id) + } + + pub fn unpin_chat_message(&self, chat_id: C) -> UnpinChatMessage + where + C: Into, + { + UnpinChatMessage::new(self.ctx(), chat_id) } } diff --git a/src/bot/download.rs b/src/bot/download.rs index d3c6ff69..d711f151 100644 --- a/src/bot/download.rs +++ b/src/bot/download.rs @@ -1,11 +1,11 @@ -use reqwest::r#async::Chunk; -use tokio::{io::AsyncWrite, stream::Stream}; +use tokio::io::AsyncWrite; -use crate::{ - bot::Bot, - network::{download_file, download_file_stream}, - DownloadError, -}; +#[cfg(feature = "unstable-stream")] +use ::{bytes::Bytes, tokio::stream::Stream}; + +#[cfg(feature = "unstable-stream")] +use crate::network::download_file_stream; +use crate::{bot::Bot, network::download_file, DownloadError}; impl Bot { /// Download file from telegram into `destination`. @@ -26,8 +26,7 @@ impl Bot { /// let bot = Bot::new("TOKEN"); /// let mut file = File::create("/home/waffle/Pictures/test.png").await?; /// - /// let TgFile { file_path, .. } = - /// bot.get_file("*file_id*").send_boxed().await?; + /// let TgFile { file_path, .. } = bot.get_file("*file_id*").send().await?; /// bot.download_file(&file_path, &mut file).await?; /// # Ok(()) } /// ``` @@ -56,10 +55,11 @@ impl Bot { /// [`AsyncWrite`]: tokio::io::AsyncWrite /// [`tokio::fs::File`]: tokio::fs::File /// [`download_file`]: crate::bot::Bot::download_file + #[cfg(feature = "unstable-stream")] pub async fn download_file_stream( &self, path: &str, - ) -> Result>, reqwest::Error> + ) -> Result>, reqwest::Error> { download_file_stream(&self.client, &self.token, path).await } diff --git a/src/bot/mod.rs b/src/bot/mod.rs index eacf2e3a..4f370663 100644 --- a/src/bot/mod.rs +++ b/src/bot/mod.rs @@ -1,4 +1,4 @@ -use reqwest::r#async::Client; +use reqwest::Client; use crate::requests::RequestContext; diff --git a/src/errors.rs b/src/errors.rs index 6482793d..e8c1ae21 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -1,29 +1,21 @@ use reqwest::StatusCode; // -#[derive(Debug, Display, From)] +#[derive(Debug, Error, From)] pub enum DownloadError { - #[display(fmt = "Network error: {err}", err = _0)] - NetworkError(reqwest::Error), + #[error("A network error: {0}")] + NetworkError(#[source] reqwest::Error), - #[display(fmt = "IO Error: {err}", err = _0)] - Io(std::io::Error), + #[error("An I/O error: {0}")] + Io(#[source] std::io::Error), } -impl std::error::Error for DownloadError { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - match self { - DownloadError::NetworkError(err) => Some(err), - DownloadError::Io(err) => Some(err), - } - } -} // // -#[derive(Debug, Display)] +#[derive(Debug, Error)] pub enum RequestError { - #[display(fmt = "Telegram error #{}: {}", status_code, description)] + #[error("A Telegram's error #{status_code}: {description}")] ApiError { status_code: StatusCode, description: String, @@ -31,30 +23,19 @@ pub enum RequestError { /// The group has been migrated to a supergroup with the specified /// identifier. - #[display(fmt = "The group has been migrated to a supergroup with id {id}", id = _0)] + #[error("The group has been migrated to a supergroup with ID #{0}")] MigrateToChatId(i64), /// In case of exceeding flood control, the number of seconds left to wait /// before the request can be repeated - #[display(fmt = "Retry after {secs} seconds", secs = _0)] + #[error("Retry after {0} seconds")] RetryAfter(i32), - #[display(fmt = "Network error: {err}", err = _0)] - NetworkError(reqwest::Error), + #[error("A network error: {0}")] + NetworkError(#[source] reqwest::Error), - #[display(fmt = "InvalidJson error caused by: {err}", err = _0)] - InvalidJson(serde_json::Error), + #[error("An error while parsing JSON: {0}")] + InvalidJson(#[source] serde_json::Error), } -impl std::error::Error for RequestError { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - match self { - RequestError::ApiError { .. } => None, - RequestError::MigrateToChatId(_) => None, - RequestError::RetryAfter(_) => None, - RequestError::NetworkError(err) => Some(err), - RequestError::InvalidJson(err) => Some(err), - } - } -} // diff --git a/src/lib.rs b/src/lib.rs index 28221d58..8d244ff6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,9 +1,9 @@ -#![feature(termination_trait_lib)] - #[macro_use] extern crate derive_more; #[macro_use] extern crate serde; +#[macro_use] +extern crate thiserror; pub use errors::{DownloadError, RequestError}; diff --git a/src/network/download.rs b/src/network/download.rs index 82ff18ea..47061b6f 100644 --- a/src/network/download.rs +++ b/src/network/download.rs @@ -1,10 +1,8 @@ -use bytes::Buf; -use futures::StreamExt; -use reqwest::r#async::{Chunk, Client}; -use tokio::{ - io::{AsyncWrite, AsyncWriteExt}, - stream::Stream, -}; +use reqwest::Client; +use tokio::io::{AsyncWrite, AsyncWriteExt}; + +#[cfg(feature = "unstable-stream")] +use ::{bytes::Bytes, tokio::stream::Stream}; use crate::DownloadError; @@ -19,25 +17,38 @@ pub async fn download_file( where D: AsyncWrite + Unpin, { - let mut stream = download_file_stream(client, token, path).await?; + let mut res = client + .get(&super::file_url(TELEGRAM_API_URL, token, path)) + .send() + .await? + .error_for_status()?; - while let Some(chunk) = stream.next().await { - let chunk = chunk?; - destination.write_all(chunk.bytes()).await?; + while let Some(chunk) = res.chunk().await? { + destination.write_all(&chunk).await?; } Ok(()) } +#[cfg(feature = "unstable-stream")] pub async fn download_file_stream( client: &Client, token: &str, path: &str, -) -> Result>, reqwest::Error> { - Ok(client +) -> Result>, reqwest::Error> { + let res = client .get(&super::file_url(TELEGRAM_API_URL, token, path)) .send() .await? - .error_for_status()? - .into_body()) + .error_for_status()?; + + Ok(futures::stream::unfold(res, |mut res| { + async { + match res.chunk().await { + Err(err) => Some((Err(err), res)), + Ok(Some(c)) => Some((Ok(c), res)), + Ok(None) => None, + } + } + })) } diff --git a/src/network/mod.rs b/src/network/mod.rs index 625b28e3..1461304f 100644 --- a/src/network/mod.rs +++ b/src/network/mod.rs @@ -1,6 +1,11 @@ -pub use download::{download_file, download_file_stream}; -pub use request::{request_json, request_multipart}; -pub use telegram_response::TelegramResponse; +#[cfg(feature = "unstable-stream")] +pub use download::download_file_stream; + +pub use self::{ + download::download_file, + request::{request_json, request_multipart}, + telegram_response::TelegramResponse, +}; mod download; mod request; @@ -22,7 +27,7 @@ fn method_url(base: &str, token: &str, method_name: &str) -> String { /// Creates URL for downloading a file. See the [Telegram documentation]. /// -/// [Telegram documentation] (https://core.telegram.org/bots/api#file) +/// [Telegram documentation]: https://core.telegram.org/bots/api#file fn file_url(base: &str, token: &str, file_path: &str) -> String { format!( "{url}/file/bot{token}/{file}", diff --git a/src/network/request.rs b/src/network/request.rs index 139b6f74..ac97c612 100644 --- a/src/network/request.rs +++ b/src/network/request.rs @@ -1,5 +1,5 @@ use apply::Apply; -use reqwest::r#async::{multipart::Form, Client, Response}; +use reqwest::{multipart::Form, Client, Response}; use serde::{de::DeserializeOwned, Serialize}; use crate::{requests::ResponseResult, RequestError}; @@ -29,12 +29,16 @@ where .await } -pub async fn request_json( +pub async fn request_json( client: &Client, token: &str, method_name: &str, params: &P, -) -> ResponseResult { +) -> ResponseResult +where + T: DeserializeOwned, + P: Serialize, +{ process_response( client .post(&super::method_url(TELEGRAM_API_URL, token, method_name)) @@ -46,9 +50,10 @@ pub async fn request_json( .await } -async fn process_response( - mut response: Response, -) -> ResponseResult { +async fn process_response(response: Response) -> ResponseResult +where + T: DeserializeOwned, +{ serde_json::from_str::>( &response.text().await.map_err(RequestError::NetworkError)?, ) diff --git a/src/requests/answer_pre_checkout_query.rs b/src/requests/answer_pre_checkout_query.rs index 9caee1e9..df5940e7 100644 --- a/src/requests/answer_pre_checkout_query.rs +++ b/src/requests/answer_pre_checkout_query.rs @@ -12,6 +12,8 @@ use crate::{ /// pre_checkout_query. Use this method to respond to such pre-checkout queries. /// On success, True is returned. Note: The Bot API must receive an answer /// within 10 seconds after the pre-checkout query was sent. +/// +/// [`Update`]: crate::types::Update pub struct AnswerPreCheckoutQuery<'a> { #[serde(skip_serializing)] ctx: RequestContext<'a>, @@ -56,40 +58,44 @@ impl AnswerPreCheckoutQuery<'_> { } impl<'a> AnswerPreCheckoutQuery<'a> { - pub(crate) fn new( + pub(crate) fn new( ctx: RequestContext<'a>, - pre_checkout_query_id: String, - ok: bool, - ) -> Self { + pre_checkout_query_id: S, + ok: B, + ) -> Self + where + S: Into, + B: Into, + { Self { ctx, - pre_checkout_query_id, - ok, + pre_checkout_query_id: pre_checkout_query_id.into(), + ok: ok.into(), error_message: None, } } - pub fn pre_checkout_query_id(mut self, pre_checkout_query_id: T) -> Self + pub fn pre_checkout_query_id(mut self, value: T) -> Self where T: Into, { - self.pre_checkout_query_id = pre_checkout_query_id.into(); + self.pre_checkout_query_id = value.into(); self } - pub fn ok(mut self, ok: T) -> Self + pub fn ok(mut self, value: B) -> Self where - T: Into, + B: Into, { - self.ok = ok.into(); + self.ok = value.into(); self } - pub fn error_message(mut self, error_message: T) -> Self + pub fn error_message(mut self, value: S) -> Self where - T: Into, + S: Into, { - self.error_message = Some(error_message.into()); + self.error_message = Some(value.into()); self } } diff --git a/src/requests/answer_shipping_query.rs b/src/requests/answer_shipping_query.rs index 6961f5dc..cb58135c 100644 --- a/src/requests/answer_shipping_query.rs +++ b/src/requests/answer_shipping_query.rs @@ -11,6 +11,8 @@ use crate::{ /// is_flexible was specified, the Bot API will send an [`Update`] with a /// shipping_query field to the bot. Use this method to reply to shipping /// queries. On success, True is returned. +/// +/// [`Update`]: crate::types::Update pub struct AnswerShippingQuery<'a> { #[serde(skip_serializing)] ctx: RequestContext<'a>, @@ -57,49 +59,53 @@ impl AnswerShippingQuery<'_> { } impl<'a> AnswerShippingQuery<'a> { - pub(crate) fn new( + pub(crate) fn new( ctx: RequestContext<'a>, - shipping_query_id: String, - ok: bool, - ) -> Self { + shipping_query_id: S, + ok: B, + ) -> Self + where + S: Into, + B: Into, + { Self { ctx, - shipping_query_id, - ok, + shipping_query_id: shipping_query_id.into(), + ok: ok.into(), shipping_options: None, error_message: None, } } - pub fn shipping_query_id(mut self, shipping_query_id: T) -> Self + pub fn shipping_query_id(mut self, value: S) -> Self where - T: Into, + S: Into, { - self.shipping_query_id = shipping_query_id.into(); + self.shipping_query_id = value.into(); self } - pub fn ok(mut self, ok: T) -> Self + pub fn ok(mut self, value: B) -> Self where - T: Into, + B: Into, { - self.ok = ok.into(); + self.ok = value.into(); self } - pub fn shipping_options(mut self, shipping_options: T) -> Self + pub fn shipping_options(mut self, value: T) -> Self where T: Into>, { - self.shipping_options = Some(shipping_options.into()); + self.shipping_options = Some(value.into()); self } - pub fn error_message(mut self, error_message: T) -> Self + pub fn error_message(mut self, value: S) -> Self where - T: Into, + S: Into, { - self.error_message = Some(error_message.into()); + self.error_message = Some(value.into()); self } } diff --git a/src/requests/edit_message_live_location.rs b/src/requests/edit_message_live_location.rs index 192a1e2a..db584c28 100644 --- a/src/requests/edit_message_live_location.rs +++ b/src/requests/edit_message_live_location.rs @@ -2,16 +2,19 @@ use async_trait::async_trait; use crate::{ network, - requests::{ChatId, Request, RequestContext, ResponseResult}, - types::{Message, ReplyMarkup}, + requests::{Request, RequestContext, ResponseResult}, + types::{ChatId, Message, ReplyMarkup}, }; #[derive(Debug, Clone, Serialize)] /// Use this method to edit live location messages. A location can be edited /// until its live_period expires or editing is explicitly disabled by a -/// call to [`stopMessageLiveLocation`]. On success, if the edited message +/// call to [`StopMessageLiveLocation`]. On success, if the edited message /// was sent by the bot, the edited [`Message`] is returned, otherwise True /// is returned. +/// +/// [`StopMessageLiveLocation`]: crate::requests::StopMessageLiveLocation +/// [`Message`]: crate::types::Message pub struct EditMessageLiveLocation<'a> { #[serde(skip_serializing)] ctx: RequestContext<'a>, @@ -60,47 +63,63 @@ impl EditMessageLiveLocation<'_> { } impl<'a> EditMessageLiveLocation<'a> { - pub(crate) fn new( + pub(crate) fn new( ctx: RequestContext<'a>, - latitude: f64, - longitude: f64, - ) -> Self { + latitude: Lt, + longitude: Lg, + ) -> Self + where + Lt: Into, + Lg: Into, + { Self { ctx, chat_id: None, message_id: None, inline_message_id: None, - latitude, - longitude, + latitude: latitude.into(), + longitude: longitude.into(), reply_markup: None, } } - pub fn chat_id>(mut self, chat_id: T) -> Self { - self.chat_id = Some(chat_id.into()); - self - } - - pub fn message_id>(mut self, message_id: T) -> Self { - self.message_id = Some(message_id.into()); - self - } - - pub fn inline_message_id(mut self, inline_message_id: T) -> Self + pub fn chat_id(mut self, value: T) -> Self where - T: Into, + T: Into, { - self.inline_message_id = Some(inline_message_id.into()); + self.chat_id = Some(value.into()); self } - pub fn latitude>(mut self, latitude: T) -> Self { - self.latitude = latitude.into(); + pub fn message_id(mut self, value: T) -> Self + where + T: Into, + { + self.message_id = Some(value.into()); self } - pub fn longitude>(mut self, longitude: T) -> Self { - self.longitude = longitude.into(); + pub fn inline_message_id(mut self, value: S) -> Self + where + S: Into, + { + self.inline_message_id = Some(value.into()); + self + } + + pub fn latitude(mut self, value: Lt) -> Self + where + Lt: Into, + { + self.latitude = value.into(); + self + } + + pub fn longitude(mut self, value: Lg) -> Self + where + Lg: Into, + { + self.longitude = value.into(); self } } diff --git a/src/requests/form_builder.rs b/src/requests/form_builder.rs index d5ee4169..72a013e7 100644 --- a/src/requests/form_builder.rs +++ b/src/requests/form_builder.rs @@ -1,13 +1,13 @@ use std::path::PathBuf; -use reqwest::r#async::multipart::Form; +use reqwest::multipart::Form; use crate::{ - requests::{utils, ChatId}, - types::{InputMedia, ParseMode}, + requests::utils, + types::{ChatId, InputMedia, ParseMode}, }; -/// This is a convenient struct that builds `reqwest::r#async::multipart::Form` +/// This is a convenient struct that builds `reqwest::multipart::Form` /// from scratch. pub struct FormBuilder { form: Form, diff --git a/src/requests/forward_message.rs b/src/requests/forward_message.rs index 90dd5765..6681faf0 100644 --- a/src/requests/forward_message.rs +++ b/src/requests/forward_message.rs @@ -2,8 +2,8 @@ use async_trait::async_trait; use crate::{ network, - requests::{ChatId, Request, RequestContext, ResponseResult}, - types::Message, + requests::{Request, RequestContext, ResponseResult}, + types::{ChatId, Message}, }; #[derive(Debug, Clone, Serialize)] @@ -50,38 +50,56 @@ impl ForwardMessage<'_> { } impl<'a> ForwardMessage<'a> { - pub(crate) fn new( + pub(crate) fn new( ctx: RequestContext<'a>, - chat_id: ChatId, - from_chat_id: ChatId, - message_id: i32, - ) -> Self { + chat_id: C, + from_chat_id: Fc, + message_id: M, + ) -> Self + where + C: Into, + Fc: Into, + M: Into, + { Self { ctx, - chat_id, - from_chat_id, - message_id, + chat_id: chat_id.into(), + from_chat_id: from_chat_id.into(), + message_id: message_id.into(), disable_notification: None, } } - pub fn chat_id>(mut self, val: T) -> Self { - self.chat_id = val.into(); + pub fn chat_id(mut self, value: C) -> Self + where + C: Into, + { + self.chat_id = value.into(); self } - pub fn from_chat_id>(mut self, val: T) -> Self { - self.from_chat_id = val.into(); + #[allow(clippy::wrong_self_convention)] + pub fn from_chat_id(mut self, value: C) -> Self + where + C: Into, + { + self.from_chat_id = value.into(); self } - pub fn message_id>(mut self, val: T) -> Self { - self.message_id = val.into(); + pub fn message_id(mut self, value: M) -> Self + where + M: Into, + { + self.message_id = value.into(); self } - pub fn disable_notification>(mut self, val: T) -> Self { - self.disable_notification = Some(val.into()); + pub fn disable_notification(mut self, value: B) -> Self + where + B: Into, + { + self.disable_notification = Some(value.into()); self } } diff --git a/src/requests/get_chat.rs b/src/requests/get_chat.rs index 4a71a107..9de16585 100644 --- a/src/requests/get_chat.rs +++ b/src/requests/get_chat.rs @@ -2,8 +2,8 @@ use async_trait::async_trait; use crate::{ network, - requests::{ChatId, Request, RequestContext, ResponseResult}, - types::Chat, + requests::{Request, RequestContext, ResponseResult}, + types::{Chat, ChatId}, }; /// Use this method to get up to date information about the chat @@ -41,11 +41,11 @@ impl GetChat<'_> { } impl<'a> GetChat<'a> { - pub fn chat_id(mut self, chat_id: T) -> Self + pub fn chat_id(mut self, value: C) -> Self where - T: Into, + C: Into, { - self.chat_id = chat_id.into(); + self.chat_id = value.into(); self } } diff --git a/src/requests/get_file.rs b/src/requests/get_file.rs index abfc8cd5..bb303e92 100644 --- a/src/requests/get_file.rs +++ b/src/requests/get_file.rs @@ -43,15 +43,21 @@ impl GetFile<'_> { } impl<'a> GetFile<'a> { - pub(crate) fn new(ctx: RequestContext<'a>, file_id: String) -> Self { - Self { ctx, file_id } + pub(crate) fn new(ctx: RequestContext<'a>, value: F) -> Self + where + F: Into, + { + Self { + ctx, + file_id: value.into(), + } } - pub fn file_id(mut self, file_id: T) -> Self + pub fn file_id(mut self, value: F) -> Self where - T: Into, + F: Into, { - self.file_id = file_id.into(); + self.file_id = value.into(); self } } diff --git a/src/requests/get_updates.rs b/src/requests/get_updates.rs index 596eaac1..5925c114 100644 --- a/src/requests/get_updates.rs +++ b/src/requests/get_updates.rs @@ -61,35 +61,35 @@ impl<'a> GetUpdates<'a> { } } - pub fn offset(mut self, offset: T) -> Self + pub fn offset(mut self, value: T) -> Self where T: Into, { - self.offset = Some(offset.into()); + self.offset = Some(value.into()); self } - pub fn limit(mut self, limit: T) -> Self + pub fn limit(mut self, value: T) -> Self where T: Into, { - self.limit = Some(limit.into()); + self.limit = Some(value.into()); self } - pub fn timeout(mut self, timeout: T) -> Self + pub fn timeout(mut self, value: T) -> Self where T: Into, { - self.timeout = Some(timeout.into()); + self.timeout = Some(value.into()); self } - pub fn allowed_updates(mut self, allowed_updates: T) -> Self + pub fn allowed_updates(mut self, value: T) -> Self where T: Into>, { - self.allowed_updates = Some(allowed_updates.into()); + self.allowed_updates = Some(value.into()); self } } diff --git a/src/requests/get_user_profile_photos.rs b/src/requests/get_user_profile_photos.rs index 28e6b4be..37246478 100644 --- a/src/requests/get_user_profile_photos.rs +++ b/src/requests/get_user_profile_photos.rs @@ -46,36 +46,39 @@ impl GetUserProfilePhotos<'_> { } impl<'a> GetUserProfilePhotos<'a> { - pub fn new(ctx: RequestContext<'a>, user_id: i32) -> Self { + pub fn new(ctx: RequestContext<'a>, user_id: U) -> Self + where + U: Into, + { Self { ctx, - user_id, + user_id: user_id.into(), offset: None, limit: None, } } - pub fn user_id(mut self, user_id: T) -> Self + pub fn user_id(mut self, value: T) -> Self where T: Into, { - self.user_id = user_id.into(); + self.user_id = value.into(); self } - pub fn offset(mut self, offset: T) -> Self + pub fn offset(mut self, value: T) -> Self where T: Into, { - self.offset = Some(offset.into()); + self.offset = Some(value.into()); self } - pub fn limit(mut self, limit: T) -> Self + pub fn limit(mut self, value: T) -> Self where T: Into, { - self.limit = Some(limit.into()); + self.limit = Some(value.into()); self } } diff --git a/src/requests/kick_chat_member.rs b/src/requests/kick_chat_member.rs index e30716d3..08956d2a 100644 --- a/src/requests/kick_chat_member.rs +++ b/src/requests/kick_chat_member.rs @@ -2,8 +2,8 @@ use async_trait::async_trait; use crate::{ network, - requests::{ChatId, Request, RequestContext, ResponseResult}, - types::True, + requests::{Request, RequestContext, ResponseResult}, + types::{ChatId, True}, }; /// Use this method to kick a user from a group, a supergroup or a channel. In @@ -28,7 +28,7 @@ pub struct KickChatMember<'a> { } #[async_trait] -impl<'a> Request for KickChatMember<'a> { +impl Request for KickChatMember<'_> { type ReturnValue = True; async fn send_boxed(self) -> ResponseResult { @@ -49,31 +49,44 @@ impl KickChatMember<'_> { } impl<'a> KickChatMember<'a> { - pub(crate) fn new( + pub(crate) fn new( ctx: RequestContext<'a>, - chat_id: ChatId, - user_id: i32, - ) -> Self { + chat_id: C, + user_id: U, + ) -> Self + where + C: Into, + U: Into, + { Self { ctx, - chat_id, - user_id, + chat_id: chat_id.into(), + user_id: user_id.into(), until_date: None, } } - pub fn chat_id>(mut self, chat_id: T) -> Self { - self.chat_id = chat_id.into(); + pub fn chat_id(mut self, value: C) -> Self + where + C: Into, + { + self.chat_id = value.into(); self } - pub fn user_id>(mut self, user_id: T) -> Self { - self.user_id = user_id.into(); + pub fn user_id(mut self, value: U) -> Self + where + U: Into, + { + self.user_id = value.into(); self } - pub fn until_date>(mut self, until_date: T) -> Self { - self.until_date = Some(until_date.into()); + pub fn until_date(mut self, value: T) -> Self + where + T: Into, + { + self.until_date = Some(value.into()); self } } diff --git a/src/requests/mod.rs b/src/requests/mod.rs index afd9c47d..f6708ca1 100644 --- a/src/requests/mod.rs +++ b/src/requests/mod.rs @@ -1,7 +1,8 @@ -use async_trait::async_trait; -use reqwest::r#async::Client; +use reqwest::Client; use serde::de::DeserializeOwned; +use async_trait::async_trait; + use crate::RequestError; pub use self::{ @@ -27,61 +28,6 @@ pub use self::{ mod form_builder; mod utils; -pub type ResponseResult = Result; - -/// Request that can be sent to telegram. -/// `ReturnValue` - a type that will be returned from Telegram. -#[async_trait] -pub trait Request { - type ReturnValue: DeserializeOwned; - - /// Send request to telegram - async fn send_boxed(self) -> ResponseResult; -} - -#[derive(Debug, Clone)] -pub struct RequestContext<'a> { - pub client: &'a Client, - pub token: &'a str, -} - -/// Unique identifier for the target chat or username of the target channel (in -/// the format @channelusername) -#[derive(Debug, Display, Serialize, From, PartialEq, Eq, Clone)] -#[serde(untagged)] -pub enum ChatId { - /// chat identifier - #[display(fmt = "{}", _0)] - Id(i64), - /// _channel_ username (in the format @channelusername) - #[display(fmt = "{}", _0)] - ChannelUsername(String), -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn chat_id_id_serialization() { - let expected_json = String::from(r#"123456"#); - let actual_json = serde_json::to_string(&ChatId::Id(123456)).unwrap(); - - assert_eq!(expected_json, actual_json) - } - - #[test] - fn chat_id_channel_username_serialization() { - let expected_json = String::from(r#""@username""#); - let actual_json = serde_json::to_string(&ChatId::ChannelUsername( - String::from("@username"), - )) - .unwrap(); - - assert_eq!(expected_json, actual_json) - } -} - mod answer_pre_checkout_query; mod answer_shipping_query; mod edit_message_live_location; @@ -112,3 +58,21 @@ mod send_voice; mod stop_message_live_location; mod unban_chat_member; mod unpin_chat_message; + +pub type ResponseResult = Result; + +/// A request that can be sent to Telegram. +#[async_trait] +pub trait Request { + /// A type of response. + type ReturnValue: DeserializeOwned; + + /// Send this request. + async fn send_boxed(self) -> ResponseResult; +} + +#[derive(Debug, Clone)] +pub struct RequestContext<'a> { + pub client: &'a Client, + pub token: &'a str, +} diff --git a/src/requests/pin_chat_message.rs b/src/requests/pin_chat_message.rs index d8129b59..4f71d4a6 100644 --- a/src/requests/pin_chat_message.rs +++ b/src/requests/pin_chat_message.rs @@ -2,8 +2,8 @@ use async_trait::async_trait; use crate::{ network, - requests::{ChatId, Request, RequestContext, ResponseResult}, - types::True, + requests::{Request, RequestContext, ResponseResult}, + types::{ChatId, True}, }; /// Use this method to get up to date information about the chat @@ -22,30 +22,34 @@ pub struct PinChatMessage<'a> { } impl<'a> PinChatMessage<'a> { - pub(crate) fn new( + pub(crate) fn new( ctx: RequestContext<'a>, - chat_id: ChatId, - message_id: i32, - ) -> Self { + chat_id: C, + message_id: M, + ) -> Self + where + C: Into, + M: Into, + { Self { ctx, - chat_id, - message_id, + chat_id: chat_id.into(), + message_id: message_id.into(), disable_notification: None, } } - pub fn disable_notification(mut self, val: T) -> Self + pub fn disable_notification(mut self, value: B) -> Self where - T: Into, + B: Into, { - self.disable_notification = Some(val.into()); + self.disable_notification = Some(value.into()); self } } #[async_trait] -impl<'a> Request for PinChatMessage<'a> { +impl Request for PinChatMessage<'_> { type ReturnValue = True; async fn send_boxed(self) -> ResponseResult { self.send().await diff --git a/src/requests/promote_chat_member.rs b/src/requests/promote_chat_member.rs index 9685adc9..596d56c6 100644 --- a/src/requests/promote_chat_member.rs +++ b/src/requests/promote_chat_member.rs @@ -1,9 +1,11 @@ -use crate::network; -use crate::requests::{ChatId, Request, RequestContext, ResponseResult}; -use crate::types::True; - use async_trait::async_trait; +use crate::{ + network, + requests::{Request, RequestContext, ResponseResult}, + types::{ChatId, True}, +}; + ///Use this method to promote or demote a user in a supergroup or a channel. /// The bot must be an administrator in the chat for this to work and must have /// the appropriate admin rights. Pass False for all boolean parameters to @@ -47,6 +49,7 @@ pub struct PromoteChatMember<'a> { #[serde(skip_serializing_if = "Option::is_none")] pub can_promote_members: Option, } + #[async_trait] impl Request for PromoteChatMember<'_> { type ReturnValue = True; @@ -67,16 +70,21 @@ impl PromoteChatMember<'_> { .await } } + impl<'a> PromoteChatMember<'a> { - pub(crate) fn new( + pub(crate) fn new( ctx: RequestContext<'a>, - chat_id: ChatId, - user_id: i32, - ) -> Self { + chat_id: C, + user_id: U, + ) -> Self + where + C: Into, + U: Into, + { Self { ctx, - chat_id, - user_id, + chat_id: chat_id.into(), + user_id: user_id.into(), can_change_info: None, can_post_messages: None, can_edit_messages: None, @@ -88,83 +96,83 @@ impl<'a> PromoteChatMember<'a> { } } - pub fn chat_id(mut self, chat_id: T) -> Self + pub fn chat_id(mut self, value: C) -> Self where - T: Into, + C: Into, { - self.chat_id = chat_id.into(); + self.chat_id = value.into(); self } - pub fn user_id(mut self, user_id: T) -> Self + pub fn user_id(mut self, value: U) -> Self where - T: Into, + U: Into, { - self.user_id = user_id.into(); + self.user_id = value.into(); self } - pub fn can_change_info(mut self, can_change_info: T) -> Self + pub fn can_change_info(mut self, value: B) -> Self where - T: Into, + B: Into, { - self.can_change_info = Some(can_change_info.into()); + self.can_change_info = Some(value.into()); self } - pub fn can_post_messages(mut self, can_post_messages: T) -> Self + pub fn can_post_messages(mut self, value: B) -> Self where - T: Into, + B: Into, { - self.can_post_messages = Some(can_post_messages.into()); + self.can_post_messages = Some(value.into()); self } - pub fn can_edit_messages(mut self, can_edit_messages: T) -> Self + pub fn can_edit_messages(mut self, value: B) -> Self where - T: Into, + B: Into, { - self.can_edit_messages = Some(can_edit_messages.into()); + self.can_edit_messages = Some(value.into()); self } - pub fn can_delete_messages(mut self, can_delete_messages: T) -> Self + pub fn can_delete_messages(mut self, value: B) -> Self where - T: Into, + B: Into, { - self.can_delete_messages = Some(can_delete_messages.into()); + self.can_delete_messages = Some(value.into()); self } - pub fn can_invite_users(mut self, can_invite_users: T) -> Self + pub fn can_invite_users(mut self, value: B) -> Self where - T: Into, + B: Into, { - self.can_invite_users = Some(can_invite_users.into()); + self.can_invite_users = Some(value.into()); self } - pub fn can_restrict_members(mut self, can_restrict_members: T) -> Self + pub fn can_restrict_members(mut self, value: B) -> Self where - T: Into, + B: Into, { - self.can_restrict_members = Some(can_restrict_members.into()); + self.can_restrict_members = Some(value.into()); self } - pub fn can_pin_messages(mut self, can_pin_messages: T) -> Self + pub fn can_pin_messages(mut self, value: B) -> Self where - T: Into, + B: Into, { - self.can_pin_messages = Some(can_pin_messages.into()); + self.can_pin_messages = Some(value.into()); self } - pub fn can_promote_members(mut self, can_promote_members: T) -> Self + pub fn can_promote_members(mut self, value: B) -> Self where - T: Into, + B: Into, { - self.can_promote_members = Some(can_promote_members.into()); + self.can_promote_members = Some(value.into()); self } } diff --git a/src/requests/restrict_chat_member.rs b/src/requests/restrict_chat_member.rs index a8522816..8720e0cc 100644 --- a/src/requests/restrict_chat_member.rs +++ b/src/requests/restrict_chat_member.rs @@ -1,9 +1,10 @@ +use async_trait::async_trait; + use crate::{ network, - requests::{ChatId, Request, RequestContext, ResponseResult}, - types::{ChatPermissions, True}, + requests::{Request, RequestContext, ResponseResult}, + types::{ChatId, ChatPermissions, True}, }; -use async_trait::async_trait; /// Use this method to restrict a user in a supergroup. The bot must be an /// administrator in the supergroup for this to work and must have the @@ -28,7 +29,7 @@ pub struct RestrictChatMember<'a> { } #[async_trait] -impl<'a> Request for RestrictChatMember<'a> { +impl Request for RestrictChatMember<'_> { type ReturnValue = True; async fn send_boxed(self) -> ResponseResult { @@ -49,50 +50,55 @@ impl RestrictChatMember<'_> { } impl<'a> RestrictChatMember<'a> { - pub(crate) fn new( + pub(crate) fn new( ctx: RequestContext<'a>, - chat_id: ChatId, - user_id: i32, - permissions: ChatPermissions, - ) -> Self { + chat_id: C, + user_id: U, + permissions: P, + ) -> Self + where + C: Into, + U: Into, + P: Into, + { Self { ctx, - chat_id, - user_id, - permissions, + chat_id: chat_id.into(), + user_id: user_id.into(), + permissions: permissions.into(), until_date: None, } } - pub fn chat_id(mut self, chat_id: T) -> Self + pub fn chat_id(mut self, value: C) -> Self where - T: Into, + C: Into, { - self.chat_id = chat_id.into(); + self.chat_id = value.into(); self } - pub fn user_id(mut self, user_id: T) -> Self + pub fn user_id(mut self, value: U) -> Self where - T: Into, + U: Into, { - self.user_id = user_id.into(); + self.user_id = value.into(); self } - pub fn permissions(mut self, permissions: T) -> Self + pub fn permissions

(mut self, value: P) -> Self where - T: Into, + P: Into, { - self.permissions = permissions.into(); + self.permissions = value.into(); self } - pub fn until_date(mut self, until_date: T) -> Self + pub fn until_date(mut self, value: T) -> Self where T: Into, { - self.until_date = Some(until_date.into()); + self.until_date = Some(value.into()); self } } diff --git a/src/requests/send_animation.rs b/src/requests/send_animation.rs index 25e39cb5..6e24b063 100644 --- a/src/requests/send_animation.rs +++ b/src/requests/send_animation.rs @@ -1,8 +1,186 @@ -use crate::requests::RequestContext; +use async_trait::async_trait; -///TODO: add implementation +use crate::network; +use crate::requests::{Request, RequestContext, ResponseResult}; +use crate::types::{ChatId, 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 +/// the future. #[derive(Debug, Clone, Serialize)] pub struct SendAnimation<'a> { #[serde(skip_serializing)] ctx: RequestContext<'a>, + ///Unique identifier for the target chat or username of the target channel + /// (in the format @channelusername) + pub chat_id: ChatId, + ///Animation to send. Pass a file_id as String to send an animation that + /// 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 + ///Duration of sent animation in seconds + #[serde(skip_serializing_if = "Option::is_none")] + pub duration: Option, + ///Animation width + #[serde(skip_serializing_if = "Option::is_none")] + pub width: Option, + ///Animation height + #[serde(skip_serializing_if = "Option::is_none")] + pub height: Option, + ///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 + /// should not exceed 320. Ignored if the file is not uploaded using + /// multipart/form-data. Thumbnails can’t be reused and can be only + /// uploaded as a new file, so you can pass “attach://” + /// if the thumbnail was uploaded using multipart/form-data under + /// » + #[serde(skip_serializing_if = "Option::is_none")] + pub thumb: Option, + // InputFile or String Optional + ///Animation caption (may also be used when resending animation by + /// file_id), 0-1024 characters + #[serde(skip_serializing_if = "Option::is_none")] + pub caption: Option, + /// Send Markdown or HTML, if you want Telegram apps to show bold, italic, + /// fixed-width text or inline URLs in the media caption. + #[serde(skip_serializing_if = "Option::is_none")] + pub parse_mode: Option, + ///Sends the message silently. Users will receive a notification with no + /// sound. + #[serde(skip_serializing_if = "Option::is_none")] + pub disable_notification: Option, + ///If the message is a reply, ID of the original message + #[serde(skip_serializing_if = "Option::is_none")] + pub reply_to_message_id: Option, + ///Additional interface options. A JSON-serialized object for an inline + /// keyboard, custom reply keyboard, instructions to remove reply keyboard + /// or to force a reply from the user. + #[serde(skip_serializing_if = "Option::is_none")] + pub reply_markup: Option, +} + +#[async_trait] +impl Request for SendAnimation<'_> { + type ReturnValue = Message; + + async fn send_boxed(self) -> ResponseResult { + self.send().await + } +} + +impl SendAnimation<'_> { + async fn send(self) -> ResponseResult { + network::request_json( + &self.ctx.client, + &self.ctx.token, + "sendAnimation", + &self, + ) + .await + } +} + +impl<'a> SendAnimation<'a> { + pub(crate) fn new( + ctx: RequestContext<'a>, + chat_id: C, + animation: S, + ) -> Self + where + C: Into, + S: Into, + { + Self { + ctx, + chat_id: chat_id.into(), + animation: animation.into(), + duration: None, + width: None, + height: None, + thumb: None, + caption: None, + parse_mode: None, + disable_notification: None, + reply_to_message_id: None, + reply_markup: None, + } + } + + pub fn chat_id(mut self, value: T) -> Self + where + T: Into, + { + self.chat_id = value.into(); + self + } + + pub fn duration(mut self, value: T) -> Self + where + T: Into, + { + self.duration = Some(value.into()); + self + } + + pub fn width(mut self, value: T) -> Self + where + T: Into, + { + self.width = Some(value.into()); + self + } + pub fn height(mut self, value: T) -> Self + where + T: Into, + { + self.height = Some(value.into()); + self + } + pub fn thumb(mut self, value: T) -> Self + where + T: Into, + { + self.thumb = Some(value.into()); + self + } + pub fn caption(mut self, value: T) -> Self + where + T: Into, + { + self.caption = Some(value.into()); + self + } + pub fn parse_mode(mut self, value: T) -> Self + where + T: Into, + { + self.parse_mode = Some(value.into()); + self + } + pub fn disable_notification(mut self, value: T) -> Self + where + T: Into, + { + self.disable_notification = Some(value.into()); + self + } + pub fn reply_to_message_id(mut self, value: T) -> Self + where + T: Into, + { + self.reply_to_message_id = Some(value.into()); + self + } + pub fn reply_markup(mut self, value: T) -> Self + where + T: Into, + { + self.reply_markup = Some(value.into()); + self + } } diff --git a/src/requests/send_audio.rs b/src/requests/send_audio.rs index a8130ee2..7a62891e 100644 --- a/src/requests/send_audio.rs +++ b/src/requests/send_audio.rs @@ -3,10 +3,9 @@ use async_trait::async_trait; use crate::{ network, requests::{ - form_builder::FormBuilder, ChatId, Request, RequestContext, - ResponseResult, + form_builder::FormBuilder, Request, RequestContext, ResponseResult, }, - types::{InputFile, Message, ParseMode, ReplyMarkup}, + types::{ChatId, InputFile, Message, ParseMode, ReplyMarkup}, }; /// Use this method to send audio files, if you want Telegram clients to display @@ -15,6 +14,9 @@ use crate::{ /// to 50 MB in size, this limit may be changed in the future. /// /// For sending voice messages, use the [`SendVoice`] method instead. +/// +/// [`Message`]: crate::types::Message +/// [`SendVoice`]: crate::requests::SendVoice pub struct SendAudio<'a> { ctx: RequestContext<'a>, @@ -35,7 +37,7 @@ pub struct SendAudio<'a> { /// or inline URLs] in the media caption. /// /// [Markdown]: crate::types::ParseMode::Markdown - /// [Html]: crate::types::ParseMode::Html + /// [HTML]: crate::types::ParseMode::HTML /// [bold, italic, fixed-width text or inline URLs]: /// crate::types::ParseMode pub parse_mode: Option, @@ -87,11 +89,13 @@ impl SendAudio<'_> { "reply_to_message_id", self.reply_to_message_id.as_ref(), ); + params = match self.audio { InputFile::File(file) => params.add_file("audio", &file), InputFile::Url(url) => params.add("audio", &url), InputFile::FileId(file_id) => params.add("audio", &file_id), }; + if let Some(thumb) = self.thumb { params = match thumb { InputFile::File(file) => params.add_file("thumb", &file), @@ -99,28 +103,31 @@ impl SendAudio<'_> { InputFile::FileId(file_id) => params.add("thumb", &file_id), } } - let params = params.build(); network::request_multipart( &self.ctx.client, &self.ctx.token, "sendAudio", - Some(params), + Some(params.build()), ) .await } } impl<'a> SendAudio<'a> { - pub(crate) fn new( + pub(crate) fn new( ctx: RequestContext<'a>, - chat_id: ChatId, - audio: InputFile, - ) -> Self { + chat_id: C, + audio: A, + ) -> Self + where + C: Into, + A: Into, + { Self { ctx, - chat_id, - audio, + chat_id: chat_id.into(), + audio: audio.into(), caption: None, parse_mode: None, duration: None, @@ -133,59 +140,83 @@ impl<'a> SendAudio<'a> { } } - pub fn chat_id>(mut self, chat_id: T) -> Self { - self.chat_id = chat_id.into(); + pub fn chat_id(mut self, value: T) -> Self + where + T: Into, + { + self.chat_id = value.into(); self } - pub fn audio>(mut self, audio: T) -> Self { - self.audio = audio.into(); + pub fn audio(mut self, value: T) -> Self + where + T: Into, + { + self.audio = value.into(); self } - pub fn caption>(mut self, caption: T) -> Self { - self.caption = Some(caption.into()); + pub fn caption(mut self, value: T) -> Self + where + T: Into, + { + self.caption = Some(value.into()); self } - pub fn parse_mode>(mut self, parse_mode: T) -> Self { - self.parse_mode = Some(parse_mode.into()); + pub fn parse_mode(mut self, value: T) -> Self + where + T: Into, + { + self.parse_mode = Some(value.into()); self } - pub fn duration>(mut self, duration: T) -> Self { - self.duration = Some(duration.into()); + pub fn duration(mut self, value: T) -> Self + where + T: Into, + { + self.duration = Some(value.into()); self } - pub fn performer>(mut self, performer: T) -> Self { - self.performer = Some(performer.into()); + pub fn performer(mut self, value: T) -> Self + where + T: Into, + { + self.performer = Some(value.into()); self } - pub fn title>(mut self, title: T) -> Self { - self.title = Some(title.into()); + pub fn title(mut self, value: T) -> Self + where + T: Into, + { + self.title = Some(value.into()); self } - pub fn thumb>(mut self, thumb: T) -> Self { - self.thumb = Some(thumb.into()); + pub fn thumb(mut self, value: T) -> Self + where + T: Into, + { + self.thumb = Some(value.into()); self } - pub fn disable_notification>( - mut self, - disable_notification: T, - ) -> Self { - self.disable_notification = Some(disable_notification.into()); + pub fn disable_notification(mut self, value: T) -> Self + where + T: Into, + { + self.disable_notification = Some(value.into()); self } - pub fn reply_to_message_id>( - mut self, - reply_to_message_id: T, - ) -> Self { - self.reply_to_message_id = Some(reply_to_message_id.into()); + pub fn reply_to_message_id(mut self, value: T) -> Self + where + T: Into, + { + self.reply_to_message_id = Some(value.into()); self } } diff --git a/src/requests/send_chat_action.rs b/src/requests/send_chat_action.rs index d65d7905..7b9c8400 100644 --- a/src/requests/send_chat_action.rs +++ b/src/requests/send_chat_action.rs @@ -2,8 +2,8 @@ use async_trait::async_trait; use crate::{ network, - requests::{ChatId, Request, RequestContext, ResponseResult}, - types::True, + requests::{Request, RequestContext, ResponseResult}, + types::{ChatAction, ChatId, True}, }; ///Use this method when you need to tell the user that something is happening @@ -25,21 +25,6 @@ pub struct SendChatAction<'a> { pub action: ChatAction, } -#[derive(Debug, Serialize, From, Clone)] -#[serde(rename_all = "snake_case")] -pub enum ChatAction { - Typing, - UploadPhoto, - RecordVideo, - UploadVideo, - RecordAudio, - UploadAudio, - UploadDocument, - FindLocation, - RecordVideoNote, - UploadVideoNote, -} - #[async_trait] impl Request for SendChatAction<'_> { type ReturnValue = True; @@ -62,31 +47,35 @@ impl SendChatAction<'_> { } impl<'a> SendChatAction<'a> { - pub(crate) fn new( + pub(crate) fn new( ctx: RequestContext<'a>, - chat_id: ChatId, - action: ChatAction, - ) -> Self { + chat_id: Cid, + action: Ca, + ) -> Self + where + Cid: Into, + Ca: Into, + { Self { ctx, - chat_id, - action, + chat_id: chat_id.into(), + action: action.into(), } } - pub fn chat_id(mut self, chat_id: T) -> Self + pub fn chat_id(mut self, value: T) -> Self where T: Into, { - self.chat_id = chat_id.into(); + self.chat_id = value.into(); self } - pub fn action(mut self, action: T) -> Self + pub fn action(mut self, value: T) -> Self where T: Into, { - self.action = action.into(); + self.action = value.into(); self } } diff --git a/src/requests/send_contact.rs b/src/requests/send_contact.rs index 621fe72a..d93ee60f 100644 --- a/src/requests/send_contact.rs +++ b/src/requests/send_contact.rs @@ -2,8 +2,8 @@ use async_trait::async_trait; use crate::{ network, - requests::{ChatId, Request, RequestContext, ResponseResult}, - types::{Message, ReplyMarkup}, + requests::{Request, RequestContext, ResponseResult}, + types::{ChatId, Message, ReplyMarkup}, }; /// Use this method to send phone contacts. @@ -64,17 +64,22 @@ impl SendContact<'_> { } impl<'a> SendContact<'a> { - pub(crate) fn new( + pub(crate) fn new( ctx: RequestContext<'a>, - chat_id: ChatId, - phone_number: String, - first_name: String, - ) -> Self { + chat_id: C, + phone_number: P, + first_name: F, + ) -> Self + where + C: Into, + P: Into, + F: Into, + { Self { ctx, - chat_id, - phone_number, - first_name, + chat_id: chat_id.into(), + phone_number: phone_number.into(), + first_name: first_name.into(), last_name: None, vcard: None, disable_notification: None, @@ -83,67 +88,67 @@ impl<'a> SendContact<'a> { } } - pub fn chat_id(mut self, chat_id: T) -> Self + pub fn chat_id(mut self, value: T) -> Self where T: Into, { - self.chat_id = chat_id.into(); + self.chat_id = value.into(); self } - pub fn phone_number(mut self, phone_number: T) -> Self + pub fn phone_number(mut self, value: T) -> Self where T: Into, { - self.phone_number = phone_number.into(); + self.phone_number = value.into(); self } - pub fn first_name(mut self, first_name: T) -> Self + pub fn first_name(mut self, value: T) -> Self where T: Into, { - self.first_name = first_name.into(); + self.first_name = value.into(); self } - pub fn last_name(mut self, last_name: T) -> Self + pub fn last_name(mut self, value: T) -> Self where T: Into, { - self.last_name = Some(last_name.into()); + self.last_name = Some(value.into()); self } - pub fn vcard(mut self, vcard: T) -> Self + pub fn vcard(mut self, value: T) -> Self where T: Into, { - self.vcard = Some(vcard.into()); + self.vcard = Some(value.into()); self } - pub fn disable_notification(mut self, disable_notification: T) -> Self + pub fn disable_notification(mut self, value: T) -> Self where T: Into, { - self.disable_notification = Some(disable_notification.into()); + self.disable_notification = Some(value.into()); self } - pub fn reply_to_message_id(mut self, reply_to_message_id: T) -> Self + pub fn reply_to_message_id(mut self, value: T) -> Self where T: Into, { - self.reply_to_message_id = Some(reply_to_message_id.into()); + self.reply_to_message_id = Some(value.into()); self } - pub fn reply_markup(mut self, reply_markup: T) -> Self + pub fn reply_markup(mut self, value: T) -> Self where T: Into, { - self.reply_markup = Some(reply_markup.into()); + self.reply_markup = Some(value.into()); self } } diff --git a/src/requests/send_document.rs b/src/requests/send_document.rs index cc4de08f..affe1a7a 100644 --- a/src/requests/send_document.rs +++ b/src/requests/send_document.rs @@ -1,8 +1,167 @@ -use crate::requests::RequestContext; +use async_trait::async_trait; -///TODO: add implementation +use crate::{ + network, + requests::{Request, RequestContext, ResponseResult}, + types::{ChatId, 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>, + /// Unique identifier for the target chat or username of the target + /// channel (in the format @channelusername) + pub chat_id: ChatId, + /// File to send. Pass a file_id as String to send a file that exists 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.» + pub document: String, + //InputFile or String + /// 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 + /// should not exceed 320. Ignored if the file is not uploaded using + /// multipart/form-data. Thumbnails can’t be reused and can be only + /// uploaded as a new file, so you can pass “attach://” + /// if the thumbnail was uploaded using multipart/form-data under + /// . More info on Sending Files » + #[serde(skip_serializing_if = "Option::is_none")] + pub thumb: Option, + //InputFile or String + /// Document caption (may also be used when resending documents by + /// file_id), 0-1024 characters + #[serde(skip_serializing_if = "Option::is_none")] + pub caption: Option, + /// Send Markdown or HTML, if you want Telegram apps to show bold, italic, + /// fixed-width text or inline URLs in the media caption. + #[serde(skip_serializing_if = "Option::is_none")] + pub parse_mode: Option, + /// Sends the message silently. Users will receive a notification with + /// no sound. + #[serde(skip_serializing_if = "Option::is_none")] + pub disable_notification: Option, + /// If the message is a reply, ID of the original message + #[serde(skip_serializing_if = "Option::is_none")] + pub reply_to_message_id: Option, + /// Additional interface options. A JSON-serialized + /// object for an inline keyboard, custom reply keyboard, instructions to + /// remove reply keyboard or to force a reply from the user. + #[serde(skip_serializing_if = "Option::is_none")] + pub reply_markup: Option, +} + +#[async_trait] +impl Request for SendDocument<'_> { + type ReturnValue = Message; + + async fn send_boxed(self) -> ResponseResult { + self.send().await + } +} + +impl SendDocument<'_> { + pub async fn send(self) -> ResponseResult { + network::request_json( + &self.ctx.client, + &self.ctx.token, + "sendDocument", + &self, + ) + .await + } +} + +impl<'a> SendDocument<'a> { + pub(crate) fn new( + ctx: RequestContext<'a>, + chat_id: C, + document: D, + ) -> Self + where + C: Into, + D: Into, + { + Self { + ctx, + chat_id: chat_id.into(), + document: document.into(), + thumb: None, + caption: None, + parse_mode: None, + disable_notification: None, + reply_to_message_id: None, + reply_markup: None, + } + } + + pub fn chat_id(mut self, value: T) -> Self + where + T: Into, + { + self.chat_id = value.into(); + self + } + + pub fn document(mut self, value: T) -> Self + where + T: Into, + { + self.document = value.into(); + self + } + + pub fn thumb(mut self, value: T) -> Self + where + T: Into, + { + self.thumb = Some(value.into()); + self + } + + pub fn caption(mut self, value: T) -> Self + where + T: Into, + { + self.caption = Some(value.into()); + self + } + + pub fn parse_mode(mut self, value: T) -> Self + where + T: Into, + { + self.parse_mode = Some(value.into()); + self + } + + pub fn disable_notification(mut self, value: T) -> Self + where + T: Into, + { + self.disable_notification = Some(value.into()); + self + } + + pub fn reply_to_message_id(mut self, value: T) -> Self + where + T: Into, + { + self.reply_to_message_id = Some(value.into()); + self + } + + pub fn reply_markup(mut self, value: T) -> Self + where + T: Into, + { + self.reply_markup = Some(value.into()); + self + } } diff --git a/src/requests/send_location.rs b/src/requests/send_location.rs index d1fc494a..4b4258fe 100644 --- a/src/requests/send_location.rs +++ b/src/requests/send_location.rs @@ -1,10 +1,11 @@ -use async_trait::async_trait; use serde::Serialize; +use async_trait::async_trait; + use crate::{ network, - requests::{ChatId, Request, RequestContext, ResponseResult}, - types::{Message, ReplyMarkup}, + requests::{Request, RequestContext, ResponseResult}, + types::{ChatId, Message, ReplyMarkup}, }; #[derive(Debug, Clone, Serialize)] @@ -59,17 +60,22 @@ impl SendLocation<'_> { } impl<'a> SendLocation<'a> { - pub(crate) fn new( + pub(crate) fn new( ctx: RequestContext<'a>, - chat_id: ChatId, - latitude: f64, - longitude: f64, - ) -> Self { + chat_id: C, + latitude: Lt, + longitude: Lg, + ) -> Self + where + Lt: Into, + Lg: Into, + C: Into, + { Self { ctx, - chat_id, - latitude, - longitude, + chat_id: chat_id.into(), + latitude: latitude.into(), + longitude: longitude.into(), live_period: None, disable_notification: None, reply_to_message_id: None, @@ -77,33 +83,51 @@ impl<'a> SendLocation<'a> { } } - pub fn chat_id>(mut self, chat_id: T) -> Self { - self.chat_id = chat_id.into(); + pub fn chat_id(mut self, value: T) -> Self + where + T: Into, + { + self.chat_id = value.into(); self } - pub fn latitude>(mut self, latitude: T) -> Self { - self.latitude = latitude.into(); + pub fn latitude(mut self, value: Lt) -> Self + where + Lt: Into, + { + self.latitude = value.into(); self } - pub fn longitude>(mut self, longitude: T) -> Self { - self.longitude = longitude.into(); + pub fn longitude(mut self, value: Lg) -> Self + where + Lg: Into, + { + self.longitude = value.into(); self } - pub fn live_period>(mut self, live_period: T) -> Self { - self.live_period = Some(live_period.into()); + pub fn live_period(mut self, value: T) -> Self + where + T: Into, + { + self.live_period = Some(value.into()); self } - pub fn disable_notification>(mut self, val: T) -> Self { - self.disable_notification = Some(val.into()); + pub fn disable_notification(mut self, value: T) -> Self + where + T: Into, + { + self.disable_notification = Some(value.into()); self } - pub fn reply_to_message_id>(mut self, val: T) -> Self { - self.reply_to_message_id = Some(val.into()); + pub fn reply_to_message_id(mut self, value: T) -> Self + where + T: Into, + { + self.reply_to_message_id = Some(value.into()); self } } diff --git a/src/requests/send_media_group.rs b/src/requests/send_media_group.rs index 636c570e..86e5e2d4 100644 --- a/src/requests/send_media_group.rs +++ b/src/requests/send_media_group.rs @@ -1,13 +1,13 @@ use apply::Apply; + use async_trait::async_trait; use crate::{ network::request_multipart, requests::{ - form_builder::FormBuilder, ChatId, Request, RequestContext, - ResponseResult, + form_builder::FormBuilder, Request, RequestContext, ResponseResult, }, - types::{InputFile, InputMedia, Message}, + types::{ChatId, InputFile, InputMedia, Message}, }; /// Use this method to send a group of photos or videos as an album. @@ -71,37 +71,53 @@ impl SendMediaGroup<'_> { } impl<'a> SendMediaGroup<'a> { - pub(crate) fn new( + pub(crate) fn new( ctx: RequestContext<'a>, - chat_id: ChatId, - media: Vec, - ) -> Self { + chat_id: C, + media: M, + ) -> Self + where + C: Into, + M: Into>, + { SendMediaGroup { ctx, - chat_id, - media, + chat_id: chat_id.into(), + media: media.into(), disable_notification: None, reply_to_message_id: None, } } - pub fn chat_id>(mut self, val: T) -> Self { - self.chat_id = val.into(); + pub fn chat_id(mut self, value: T) -> Self + where + T: Into, + { + self.chat_id = value.into(); self } - pub fn media>>(mut self, val: T) -> Self { - self.media = val.into(); + pub fn media(mut self, value: T) -> Self + where + T: Into>, + { + self.media = value.into(); self } - pub fn disable_notification>(mut self, val: T) -> Self { - self.disable_notification = Some(val.into()); + pub fn disable_notification(mut self, value: T) -> Self + where + T: Into, + { + self.disable_notification = Some(value.into()); self } - pub fn reply_to_message_id>(mut self, val: T) -> Self { - self.reply_to_message_id = Some(val.into()); + pub fn reply_to_message_id(mut self, value: T) -> Self + where + T: Into, + { + self.reply_to_message_id = Some(value.into()); self } } diff --git a/src/requests/send_message.rs b/src/requests/send_message.rs index dd35bdcb..cc416356 100644 --- a/src/requests/send_message.rs +++ b/src/requests/send_message.rs @@ -2,8 +2,8 @@ use async_trait::async_trait; use crate::{ network, - requests::{ChatId, Request, RequestContext, ResponseResult}, - types::{Message, ParseMode, ReplyMarkup}, + requests::{Request, RequestContext, ResponseResult}, + types::{ChatId, Message, ParseMode, ReplyMarkup}, }; #[derive(Debug, Clone, Serialize)] @@ -24,7 +24,7 @@ pub struct SendMessage<'a> { /// or inline URLs] in the media caption. /// /// [Markdown]: crate::types::ParseMode::Markdown - /// [Html]: crate::types::ParseMode::Html + /// [HTML]: crate::types::ParseMode::HTML /// [bold, italic, fixed-width text or inline URLs]: /// crate::types::ParseMode #[serde(skip_serializing_if = "Option::is_none")] @@ -65,15 +65,19 @@ impl SendMessage<'_> { } impl<'a> SendMessage<'a> { - pub(crate) fn new( + pub(crate) fn new( ctx: RequestContext<'a>, - chat_id: ChatId, - text: String, - ) -> Self { + chat_id: C, + text: S, + ) -> Self + where + C: Into, + S: Into, + { SendMessage { ctx, - chat_id, - text, + chat_id: chat_id.into(), + text: text.into(), parse_mode: None, disable_web_page_preview: None, disable_notification: None, @@ -82,38 +86,59 @@ impl<'a> SendMessage<'a> { } } - pub fn chat_id>(mut self, val: T) -> Self { - self.chat_id = val.into(); + pub fn chat_id(mut self, value: T) -> Self + where + T: Into, + { + self.chat_id = value.into(); self } - pub fn text>(mut self, val: T) -> Self { - self.text = val.into(); + pub fn text(mut self, value: T) -> Self + where + T: Into, + { + self.text = value.into(); self } - pub fn parse_mode>(mut self, val: T) -> Self { - self.parse_mode = Some(val.into()); + pub fn parse_mode(mut self, value: T) -> Self + where + T: Into, + { + self.parse_mode = Some(value.into()); self } - pub fn disable_web_page_preview>(mut self, val: T) -> Self { - self.disable_web_page_preview = Some(val.into()); + pub fn disable_web_page_preview(mut self, value: T) -> Self + where + T: Into, + { + self.disable_web_page_preview = Some(value.into()); self } - pub fn disable_notification>(mut self, val: T) -> Self { - self.disable_notification = Some(val.into()); + pub fn disable_notification(mut self, value: T) -> Self + where + T: Into, + { + self.disable_notification = Some(value.into()); self } - pub fn reply_to_message_id>(mut self, val: T) -> Self { - self.reply_to_message_id = Some(val.into()); + pub fn reply_to_message_id(mut self, value: T) -> Self + where + T: Into, + { + self.reply_to_message_id = Some(value.into()); self } - pub fn reply_markup>(mut self, val: T) -> Self { - self.reply_markup = Some(val.into()); + pub fn reply_markup(mut self, value: T) -> Self + where + T: Into, + { + self.reply_markup = Some(value.into()); self } } diff --git a/src/requests/send_photo.rs b/src/requests/send_photo.rs index 20b5e4b6..ba9a4b11 100644 --- a/src/requests/send_photo.rs +++ b/src/requests/send_photo.rs @@ -3,10 +3,9 @@ use async_trait::async_trait; use crate::{ network, requests::{ - form_builder::FormBuilder, ChatId, Request, RequestContext, - ResponseResult, + form_builder::FormBuilder, Request, RequestContext, ResponseResult, }, - types::{InputFile, Message, ParseMode, ReplyMarkup}, + types::{ChatId, InputFile, Message, ParseMode, ReplyMarkup}, }; #[derive(Debug, Clone)] @@ -33,7 +32,7 @@ pub struct SendPhoto<'a> { /// or inline URLs] in the media caption. /// /// [Markdown]: crate::types::ParseMode::Markdown - /// [Html]: crate::types::ParseMode::Html + /// [HTML]: crate::types::ParseMode::HTML /// [bold, italic, fixed-width text or inline URLs]: /// crate::types::ParseMode pub parse_mode: Option, @@ -74,28 +73,31 @@ impl SendPhoto<'_> { InputFile::Url(url) => params.add("photo", &url), InputFile::FileId(file_id) => params.add("photo", &file_id), }; - let params = params.build(); network::request_multipart( &self.ctx.client, &self.ctx.token, "sendPhoto", - Some(params), + Some(params.build()), ) .await } } impl<'a> SendPhoto<'a> { - pub(crate) fn new( + pub(crate) fn new( ctx: RequestContext<'a>, - chat_id: ChatId, - photo: InputFile, - ) -> Self { + chat_id: C, + photo: P, + ) -> Self + where + C: Into, + P: Into, + { Self { ctx, - chat_id, - photo, + chat_id: chat_id.into(), + photo: photo.into(), caption: None, parse_mode: None, disable_notification: None, @@ -104,39 +106,51 @@ impl<'a> SendPhoto<'a> { } } - pub fn chat_id>(mut self, chat_id: T) -> Self { - self.chat_id = chat_id.into(); + pub fn chat_id(mut self, value: T) -> Self + where + T: Into, + { + self.chat_id = value.into(); self } - pub fn photo>(mut self, photo: T) -> Self { - self.photo = photo.into(); + pub fn photo(mut self, value: T) -> Self + where + T: Into, + { + self.photo = value.into(); self } - pub fn caption>(mut self, caption: T) -> Self { - self.caption = Some(caption.into()); + pub fn caption(mut self, value: T) -> Self + where + T: Into, + { + self.caption = Some(value.into()); self } - pub fn parse_mode>(mut self, parse_mode: T) -> Self { - self.parse_mode = Some(parse_mode.into()); + pub fn parse_mode(mut self, value: T) -> Self + where + T: Into, + { + self.parse_mode = Some(value.into()); self } - pub fn disable_notification>( - mut self, - disable_notification: T, - ) -> Self { - self.disable_notification = Some(disable_notification.into()); + pub fn disable_notification(mut self, value: T) -> Self + where + T: Into, + { + self.disable_notification = Some(value.into()); self } - pub fn reply_to_message_id>( - mut self, - reply_to_message_id: T, - ) -> Self { - self.reply_to_message_id = Some(reply_to_message_id.into()); + pub fn reply_to_message_id(mut self, value: T) -> Self + where + T: Into, + { + self.reply_to_message_id = Some(value.into()); self } } diff --git a/src/requests/send_poll.rs b/src/requests/send_poll.rs index 003c46e8..02381a84 100644 --- a/src/requests/send_poll.rs +++ b/src/requests/send_poll.rs @@ -2,8 +2,8 @@ use async_trait::async_trait; use crate::{ network, - requests::{ChatId, Request, RequestContext, ResponseResult}, - types::{Message, ReplyMarkup}, + requests::{Request, RequestContext, ResponseResult}, + types::{ChatId, Message, ReplyMarkup}, }; /// Use this method to send a native poll. A native poll can't be sent to a @@ -54,68 +54,73 @@ impl SendPoll<'_> { } impl<'a> SendPoll<'a> { - pub(crate) fn new( + pub(crate) fn new( ctx: RequestContext<'a>, - chat_id: ChatId, - question: String, - options: Vec, - ) -> Self { + chat_id: C, + question: Q, + options: O, + ) -> Self + where + C: Into, + Q: Into, + O: Into>, + { Self { ctx, - chat_id, - question, - options, + chat_id: chat_id.into(), + question: question.into(), + options: options.into(), disable_notification: None, reply_to_message_id: None, reply_markup: None, } } - pub fn chat_id(mut self, chat_id: T) -> Self + pub fn chat_id(mut self, value: T) -> Self where T: Into, { - self.chat_id = chat_id.into(); + self.chat_id = value.into(); self } - pub fn question(mut self, question: T) -> Self + pub fn question(mut self, value: T) -> Self where T: Into, { - self.question = question.into(); + self.question = value.into(); self } - pub fn options(mut self, options: T) -> Self + pub fn options(mut self, value: T) -> Self where T: Into>, { - self.options = options.into(); + self.options = value.into(); self } - pub fn disable_notification(mut self, disable_notification: T) -> Self + pub fn disable_notification(mut self, value: T) -> Self where T: Into, { - self.disable_notification = Some(disable_notification.into()); + self.disable_notification = Some(value.into()); self } - pub fn reply_to_message_id(mut self, reply_to_message_id: T) -> Self + pub fn reply_to_message_id(mut self, value: T) -> Self where T: Into, { - self.reply_to_message_id = Some(reply_to_message_id.into()); + self.reply_to_message_id = Some(value.into()); self } - pub fn reply_markup(mut self, reply_markup: T) -> Self + pub fn reply_markup(mut self, value: T) -> Self where T: Into, { - self.reply_markup = Some(reply_markup.into()); + self.reply_markup = Some(value.into()); self } } diff --git a/src/requests/send_venue.rs b/src/requests/send_venue.rs index 65f57b61..3913c24c 100644 --- a/src/requests/send_venue.rs +++ b/src/requests/send_venue.rs @@ -2,8 +2,8 @@ use async_trait::async_trait; use crate::{ network, - requests::{ChatId, Request, RequestContext, ResponseResult}, - types::{Message, ReplyMarkup}, + requests::{Request, RequestContext, ResponseResult}, + types::{ChatId, Message, ReplyMarkup}, }; /// Use this method to send information about a venue. @@ -69,21 +69,28 @@ impl SendVenue<'_> { } impl<'a> SendVenue<'a> { - pub(crate) fn new( + pub(crate) fn new( ctx: RequestContext<'a>, - chat_id: ChatId, - latitude: f64, - longitude: f64, - title: String, - address: String, - ) -> Self { + chat_id: C, + latitude: Lt, + longitude: Lg, + title: T, + address: A, + ) -> Self + where + Lt: Into, + Lg: Into, + C: Into, + T: Into, + A: Into, + { Self { ctx, - chat_id, - latitude, - longitude, - title, - address, + chat_id: chat_id.into(), + latitude: latitude.into(), + longitude: longitude.into(), + title: title.into(), + address: address.into(), foursquare_id: None, foursquare_type: None, disable_notification: None, @@ -91,75 +98,75 @@ impl<'a> SendVenue<'a> { reply_markup: None, } } - pub fn chat_id(mut self, chat_id: T) -> Self + pub fn chat_id(mut self, value: T) -> Self where T: Into, { - self.chat_id = chat_id.into(); + self.chat_id = value.into(); self } - pub fn longitude(mut self, longitude: T) -> Self + pub fn longitude(mut self, value: Lg) -> Self where - T: Into, + Lg: Into, { - self.longitude = longitude.into(); + self.longitude = value.into(); self } - pub fn latitude(mut self, latitude: T) -> Self + pub fn latitude(mut self, value: Lt) -> Self where - T: Into, + Lt: Into, { - self.latitude = latitude.into(); + self.latitude = value.into(); self } - pub fn title(mut self, title: T) -> Self + pub fn title(mut self, value: T) -> Self where T: Into, { - self.title = title.into(); + self.title = value.into(); self } - pub fn address(mut self, address: T) -> Self + pub fn address(mut self, value: T) -> Self where T: Into, { - self.address = address.into(); + self.address = value.into(); self } - pub fn foursquare_id(mut self, foursquare_id: T) -> Self + pub fn foursquare_id(mut self, value: T) -> Self where T: Into, { - self.foursquare_id = Some(foursquare_id.into()); + self.foursquare_id = Some(value.into()); self } - pub fn disable_notification(mut self, disable_notification: T) -> Self + pub fn disable_notification(mut self, value: T) -> Self where T: Into, { - self.disable_notification = Some(disable_notification.into()); + self.disable_notification = Some(value.into()); self } - pub fn foursquare_type(mut self, foursquare_type: T) -> Self + pub fn foursquare_type(mut self, value: T) -> Self where T: Into, { - self.foursquare_type = Some(foursquare_type.into()); + self.foursquare_type = Some(value.into()); self } - pub fn reply_markup(mut self, reply_markup: T) -> Self + pub fn reply_markup(mut self, value: T) -> Self where T: Into, { - self.reply_markup = Some(reply_markup.into()); + self.reply_markup = Some(value.into()); self } } diff --git a/src/requests/send_video.rs b/src/requests/send_video.rs index 94981234..811e7783 100644 --- a/src/requests/send_video.rs +++ b/src/requests/send_video.rs @@ -1,8 +1,202 @@ -use crate::requests::RequestContext; +use async_trait::async_trait; -///TODO: add implementation +use crate::network; +use crate::requests::{Request, RequestContext, ResponseResult}; +use crate::types::{ChatId, 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 +/// limit may be changed in the future. #[derive(Debug, Clone, Serialize)] pub struct SendVideo<'a> { #[serde(skip_serializing)] ctx: RequestContext<'a>, + ///Unique identifier for the target chat or username of the target channel + /// (in the format @channelusername) + pub chat_id: ChatId, + ///Video to send. Pass a file_id as String to send a video that exists on + /// 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, + ///Duration of sent video in seconds + #[serde(skip_serializing_if = "Option::is_none")] + pub duration: Option, + ///Video width + #[serde(skip_serializing_if = "Option::is_none")] + pub width: Option, + ///Video height + #[serde(skip_serializing_if = "Option::is_none")] + pub height: Option, + ///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 + /// should not exceed 320. Ignored if the file is not uploaded using + /// multipart/form-data. Thumbnails can’t be reused and can be only + /// uploaded as a new file, so you can pass “attach://” + /// if the thumbnail was uploaded using multipart/form-data under + /// . More info on Sending Files » + #[serde(skip_serializing_if = "Option::is_none")] + pub thumb: Option, + //InputFile or String + ///Video caption (may also be used when resending videos by file_id), + /// 0-1024 characters + #[serde(skip_serializing_if = "Option::is_none")] + pub caption: Option, + ///Send Markdown or HTML, if you want Telegram apps to show bold, italic, + /// fixed-width text or inline URLs in the media caption. + #[serde(skip_serializing_if = "Option::is_none")] + pub parse_mode: Option, + ///Pass True, if the uploaded video is suitable for streaming + #[serde(skip_serializing_if = "Option::is_none")] + pub supports_streaming: Option, + /// Sends the message silently. Users will receive a notification with no + /// sound. + #[serde(skip_serializing_if = "Option::is_none")] + pub disable_notification: Option, + ///If the message is a reply, ID of the original message + #[serde(skip_serializing_if = "Option::is_none")] + pub reply_to_message_id: Option, + ///Additional interface options. A JSON-serialized object for an inline + /// keyboard, custom reply keyboard, instructions to remove reply keyboard + /// or to force a reply from the user. + #[serde(skip_serializing_if = "Option::is_none")] + pub reply_markup: Option, +} + +#[async_trait] +impl Request for SendVideo<'_> { + type ReturnValue = Message; + + async fn send_boxed(self) -> ResponseResult { + self.send().await + } +} + +impl SendVideo<'_> { + async fn send(self) -> ResponseResult { + network::request_json( + &self.ctx.client, + &self.ctx.token, + "sendVideo", + &self, + ) + .await + } +} + +impl<'a> SendVideo<'a> { + pub(crate) fn new( + ctx: RequestContext<'a>, + chat_id: C, + video: V, + ) -> Self + where + C: Into, + V: Into, + { + Self { + ctx, + chat_id: chat_id.into(), + video: video.into(), + duration: None, + width: None, + height: None, + thumb: None, + caption: None, + parse_mode: None, + supports_streaming: None, + disable_notification: None, + reply_to_message_id: None, + reply_markup: None, + } + } + pub fn chat_id(mut self, value: T) -> Self + where + T: Into, + { + self.chat_id = value.into(); + self + } + + pub fn video(mut self, value: T) -> Self + where + T: Into, + { + self.video = value.into(); + self + } + + pub fn duration(mut self, value: T) -> Self + where + T: Into, + { + self.duration = Some(value.into()); + self + } + pub fn width(mut self, value: T) -> Self + where + T: Into, + { + self.width = Some(value.into()); + self + } + pub fn height(mut self, value: T) -> Self + where + T: Into, + { + self.height = Some(value.into()); + self + } + pub fn thumb(mut self, value: T) -> Self + where + T: Into, + { + self.thumb = Some(value.into()); + self + } + pub fn caption(mut self, value: T) -> Self + where + T: Into, + { + self.caption = Some(value.into()); + self + } + pub fn parse_mode(mut self, value: T) -> Self + where + T: Into, + { + self.parse_mode = Some(value.into()); + self + } + pub fn supports_streaming(mut self, value: T) -> Self + where + T: Into, + { + self.supports_streaming = Some(value.into()); + self + } + pub fn disable_notification(mut self, value: T) -> Self + where + T: Into, + { + self.disable_notification = Some(value.into()); + self + } + pub fn reply_to_message_id(mut self, value: T) -> Self + where + T: Into, + { + self.reply_to_message_id = Some(value.into()); + self + } + pub fn reply_markup(mut self, value: T) -> Self + where + T: Into, + { + self.reply_markup = Some(value.into()); + self + } } diff --git a/src/requests/send_video_note.rs b/src/requests/send_video_note.rs index 2a28b07d..6e9a23a8 100644 --- a/src/requests/send_video_note.rs +++ b/src/requests/send_video_note.rs @@ -1,8 +1,11 @@ -use crate::network; -use crate::requests::{ChatId, Request, RequestContext, ResponseResult}; -use crate::types::{Message, ReplyMarkup}; use async_trait::async_trait; +use crate::{ + network, + requests::{Request, RequestContext, ResponseResult}, + types::{ChatId, Message, ReplyMarkup}, +}; + ///As of v.4.0, Telegram clients support rounded square mp4 videos of up to 1 /// minute long. Use this method to send video messages. On success, the sent /// Message is returned. @@ -17,7 +20,8 @@ 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: String, + // InputFile or String ///Duration of sent video in seconds #[serde(skip_serializing_if = "Option::is_none")] pub duration: Option, @@ -33,7 +37,8 @@ pub struct SendVideoNote<'a> { /// if the thumbnail was uploaded using multipart/form-data under /// . More info on Sending Files » #[serde(skip_serializing_if = "Option::is_none")] - pub thumb: Option, // InputFile or String + pub thumb: Option, + // InputFile or String ///Sends the message silently. Users will receive a notification with no /// sound. #[serde(skip_serializing_if = "Option::is_none")] @@ -70,15 +75,19 @@ impl SendVideoNote<'_> { } impl<'a> SendVideoNote<'a> { - pub(crate) fn new( + pub(crate) fn new( ctx: RequestContext<'a>, - chat_id: ChatId, - video_note: String, - ) -> Self { + chat_id: C, + video_note: V, + ) -> Self + where + C: Into, + V: Into, + { Self { ctx, - chat_id, - video_note, + chat_id: chat_id.into(), + video_note: video_note.into(), duration: None, length: None, thumb: None, @@ -88,67 +97,67 @@ impl<'a> SendVideoNote<'a> { } } - pub fn chat_id(mut self, chat_id: T) -> Self + pub fn chat_id(mut self, value: T) -> Self where T: Into, { - self.chat_id = chat_id.into(); + self.chat_id = value.into(); self } - pub fn video_note(mut self, video_note: T) -> Self + pub fn video_note(mut self, value: T) -> Self where T: Into, { - self.video_note = video_note.into(); + self.video_note = value.into(); self } - pub fn duration(mut self, duration: T) -> Self + pub fn duration(mut self, value: T) -> Self where T: Into, { - self.duration = Some(duration.into()); + self.duration = Some(value.into()); self } - pub fn length(mut self, length: T) -> Self + pub fn length(mut self, value: T) -> Self where T: Into, { - self.length = Some(length.into()); + self.length = Some(value.into()); self } - pub fn thumb(mut self, thumb: T) -> Self + pub fn thumb(mut self, value: T) -> Self where T: Into, { - self.thumb = Some(thumb.into()); + self.thumb = Some(value.into()); self } - pub fn disable_notification(mut self, disable_notification: T) -> Self + pub fn disable_notification(mut self, value: T) -> Self where T: Into, { - self.disable_notification = Some(disable_notification.into()); + self.disable_notification = Some(value.into()); self } - pub fn reply_to_message_id(mut self, reply_to_message_id: T) -> Self + pub fn reply_to_message_id(mut self, value: T) -> Self where T: Into, { - self.reply_to_message_id = Some(reply_to_message_id.into()); + self.reply_to_message_id = Some(value.into()); self } - pub fn reply_markup(mut self, reply_markup: T) -> Self + pub fn reply_markup(mut self, value: T) -> Self where T: Into, { - self.reply_markup = Some(reply_markup.into()); + self.reply_markup = Some(value.into()); self } } diff --git a/src/requests/send_voice.rs b/src/requests/send_voice.rs index 7b945fb9..d0d7ad27 100644 --- a/src/requests/send_voice.rs +++ b/src/requests/send_voice.rs @@ -1,8 +1,11 @@ -use crate::network; -use crate::requests::{ChatId, Request, RequestContext, ResponseResult}; -use crate::types::{Message, ReplyMarkup, ParseMode}; use async_trait::async_trait; +use crate::{ + network, + requests::{Request, RequestContext, ResponseResult}, + types::{ChatId, Message, ParseMode, ReplyMarkup}, +}; + ///Use this method to send audio files, if you want Telegram clients to display /// the file as a playable voice message. For this to work, your audio must be /// in an .ogg file encoded with OPUS (other formats may be sent as Audio or @@ -20,7 +23,8 @@ 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: String, + //InputFile or String /// Voice message caption, 0-1024 characters #[serde(skip_serializing_if = "Option::is_none")] pub caption: Option, @@ -68,15 +72,19 @@ impl SendVoice<'_> { } impl<'a> SendVoice<'a> { - pub(crate) fn new( + pub(crate) fn new( ctx: RequestContext<'a>, - chat_id: ChatId, - voice: String, - ) -> Self { + chat_id: C, + voice: V, + ) -> Self + where + C: Into, + V: Into, + { Self { ctx, - chat_id, - voice, + chat_id: chat_id.into(), + voice: voice.into(), caption: None, parse_mode: None, duration: None, @@ -86,70 +94,67 @@ impl<'a> SendVoice<'a> { } } - pub fn chat_id(mut self, chat_id: T) -> Self - where - T: Into, + pub fn chat_id(mut self, value: T) -> Self + where + T: Into, { - self.chat_id = chat_id.into(); + self.chat_id = value.into(); self } - pub fn voice(mut self, voice: T) -> Self - where - T: Into, + pub fn voice(mut self, value: T) -> Self + where + T: Into, { - self.voice = voice.into(); + self.voice = value.into(); self } - pub fn caption(mut self, caption: T) -> Self - where - T: Into, + pub fn caption(mut self, value: T) -> Self + where + T: Into, { - self.caption = Some(caption.into()); + self.caption = Some(value.into()); self } - pub fn parse_mode(mut self, parse_mode: T) -> Self - where - T: Into, + pub fn parse_mode(mut self, value: T) -> Self + where + T: Into, { - self.parse_mode = Some(parse_mode.into()); + self.parse_mode = Some(value.into()); self } - - pub fn duration(mut self, duration: T) -> Self - where - T: Into, + pub fn duration(mut self, value: T) -> Self + where + T: Into, { - self.duration = Some(duration.into()); + self.duration = Some(value.into()); self } - pub fn disable_notification(mut self, disable_notification: T) -> Self - where - T: Into, + pub fn disable_notification(mut self, value: T) -> Self + where + T: Into, { - self.disable_notification = Some(disable_notification.into()); + self.disable_notification = Some(value.into()); self } - pub fn reply_to_message_id(mut self, reply_to_message_id: T) -> Self - where - T: Into, + pub fn reply_to_message_id(mut self, value: T) -> Self + where + T: Into, { - self.reply_to_message_id = Some(reply_to_message_id.into()); + self.reply_to_message_id = Some(value.into()); self } - - pub fn reply_markup(mut self, reply_markup: T) -> Self - where - T: Into, + pub fn reply_markup(mut self, value: T) -> Self + where + T: Into, { - self.reply_markup = Some(reply_markup.into()); + self.reply_markup = Some(value.into()); self } - } diff --git a/src/requests/stop_message_live_location.rs b/src/requests/stop_message_live_location.rs index 23967646..e2c0282f 100644 --- a/src/requests/stop_message_live_location.rs +++ b/src/requests/stop_message_live_location.rs @@ -2,8 +2,8 @@ use async_trait::async_trait; use crate::{ network, - requests::{ChatId, Request, RequestContext, ResponseResult}, - types::{InlineKeyboardMarkup, Message}, + requests::{Request, RequestContext, ResponseResult}, + types::{ChatId, InlineKeyboardMarkup, Message}, }; /// Use this method to stop updating a live location message before live_period @@ -64,35 +64,35 @@ impl<'a> StopMessageLiveLocation<'a> { } } - pub fn chat_id(mut self, chat_id: T) -> Self + pub fn chat_id(mut self, value: T) -> Self where T: Into, { - self.chat_id = Some(chat_id.into()); + self.chat_id = Some(value.into()); self } - pub fn message_id(mut self, message_id: T) -> Self + pub fn message_id(mut self, value: T) -> Self where T: Into, { - self.message_id = Some(message_id.into()); + self.message_id = Some(value.into()); self } - pub fn inline_message_id(mut self, inline_message_id: T) -> Self + pub fn inline_message_id(mut self, value: T) -> Self where T: Into, { - self.inline_message_id = Some(inline_message_id.into()); + self.inline_message_id = Some(value.into()); self } - pub fn reply_markup(mut self, reply_markup: T) -> Self + pub fn reply_markup(mut self, value: T) -> Self where T: Into, { - self.reply_markup = Some(reply_markup.into()); + self.reply_markup = Some(value.into()); self } } diff --git a/src/requests/unban_chat_member.rs b/src/requests/unban_chat_member.rs index bd84adc8..52ab1eaa 100644 --- a/src/requests/unban_chat_member.rs +++ b/src/requests/unban_chat_member.rs @@ -1,7 +1,10 @@ use async_trait::async_trait; -use crate::network; -use crate::requests::{ChatId, Request, RequestContext, ResponseResult}; +use crate::{ + network, + requests::{Request, RequestContext, ResponseResult}, + types::ChatId, +}; /// Use this method to unban a previously kicked user in a supergroup or /// channel. The user will not return to the group or channel automatically, but @@ -40,31 +43,35 @@ impl UnbanChatMember<'_> { } impl<'a> UnbanChatMember<'a> { - pub(crate) fn new( + pub(crate) fn new( ctx: RequestContext<'a>, - chat_id: ChatId, - user_id: i32, - ) -> Self { + chat_id: C, + user_id: U, + ) -> Self + where + C: Into, + U: Into, + { Self { ctx, - chat_id, - user_id, + chat_id: chat_id.into(), + user_id: user_id.into(), } } - pub fn chat_id(mut self, chat_id: T) -> Self + pub fn chat_id(mut self, value: T) -> Self where T: Into, { - self.chat_id = chat_id.into(); + self.chat_id = value.into(); self } - pub fn user_id(mut self, user_id: T) -> Self + pub fn user_id(mut self, value: T) -> Self where T: Into, { - self.user_id = user_id.into(); + self.user_id = value.into(); self } } diff --git a/src/requests/unpin_chat_message.rs b/src/requests/unpin_chat_message.rs index 2158014e..970e46e7 100644 --- a/src/requests/unpin_chat_message.rs +++ b/src/requests/unpin_chat_message.rs @@ -2,8 +2,8 @@ use async_trait::async_trait; use crate::{ network, - requests::{ChatId, Request, RequestContext, ResponseResult}, - types::True, + requests::{Request, RequestContext, ResponseResult}, + types::{ChatId, True}, }; #[derive(Debug, Clone, Serialize)] @@ -36,15 +36,21 @@ impl UnpinChatMessage<'_> { } impl<'a> UnpinChatMessage<'a> { - pub(crate) fn new(ctx: RequestContext<'a>, chat_id: ChatId) -> Self { - Self { ctx, chat_id } + pub(crate) fn new(ctx: RequestContext<'a>, value: C) -> Self + where + C: Into, + { + Self { + ctx, + chat_id: value.into(), + } } - pub fn chat_id(mut self, chat_id: T) -> Self + pub fn chat_id(mut self, value: T) -> Self where T: Into, { - self.chat_id = chat_id.into(); + self.chat_id = value.into(); self } } diff --git a/src/requests/utils.rs b/src/requests/utils.rs index 90f9c409..6ff6b3a5 100644 --- a/src/requests/utils.rs +++ b/src/requests/utils.rs @@ -1,7 +1,7 @@ use std::path::PathBuf; use bytes::{Bytes, BytesMut}; -use reqwest::r#async::multipart::Part; +use reqwest::{multipart::Part, Body}; use tokio::{codec::FramedRead, prelude::*}; struct FileDecoder; @@ -30,12 +30,11 @@ pub fn file_to_part(path_to_file: &PathBuf) -> Part { ) }) .flatten_stream(); - let part = Part::stream(file).file_name( + Part::stream(Body::wrap_stream(file)).file_name( path_to_file .file_name() .unwrap() .to_string_lossy() .into_owned(), - ); - part + ) } diff --git a/src/types/chat_action.rs b/src/types/chat_action.rs new file mode 100644 index 00000000..c58533b0 --- /dev/null +++ b/src/types/chat_action.rs @@ -0,0 +1,14 @@ +#[derive(Debug, Serialize, Clone, Copy, PartialEq, Eq, Hash)] +#[serde(rename_all = "snake_case")] +pub enum ChatAction { + Typing, + UploadPhoto, + RecordVideo, + UploadVideo, + RecordAudio, + UploadAudio, + UploadDocument, + FindLocation, + RecordVideoNote, + UploadVideoNote, +} diff --git a/src/types/chat_id.rs b/src/types/chat_id.rs new file mode 100644 index 00000000..c6e69093 --- /dev/null +++ b/src/types/chat_id.rs @@ -0,0 +1,36 @@ +/// Unique identifier for the target chat or username of the target channel (in +/// the format @channelusername) +#[derive(Debug, Display, Serialize, From, PartialEq, Eq, Clone)] +#[serde(untagged)] +pub enum ChatId { + /// chat identifier + #[display(fmt = "{}", _0)] + Id(i64), + /// _channel_ username (in the format @channelusername) + #[display(fmt = "{}", _0)] + ChannelUsername(String), +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn chat_id_id_serialization() { + let expected_json = String::from(r#"123456"#); + let actual_json = serde_json::to_string(&ChatId::Id(123456)).unwrap(); + + assert_eq!(expected_json, actual_json) + } + + #[test] + fn chat_id_channel_username_serialization() { + let expected_json = String::from(r#""@username""#); + let actual_json = serde_json::to_string(&ChatId::ChannelUsername( + String::from("@username"), + )) + .unwrap(); + + assert_eq!(expected_json, actual_json) + } +} diff --git a/src/types/force_reply.rs b/src/types/force_reply.rs index 4c3b295e..787646b0 100644 --- a/src/types/force_reply.rs +++ b/src/types/force_reply.rs @@ -14,5 +14,7 @@ pub struct ForceReply { /// users only. Targets: 1) users that are @mentioned in the text of the /// [`Message`] object; 2) if the bot's message is a reply /// (has reply_to_message_id), sender of the original message. + /// + /// [`Message`]: crate::types::Message pub selective: Option, } diff --git a/src/types/inline_keyboard_button.rs b/src/types/inline_keyboard_button.rs index 5d5b2ee7..27ef8896 100644 --- a/src/types/inline_keyboard_button.rs +++ b/src/types/inline_keyboard_button.rs @@ -44,15 +44,13 @@ pub enum InlineKeyboardButtonKind { /// Build buttons /// /// Example: -/// ```edition2018 +/// ``` /// use async_telegram_bot::types::InlineKeyboardButton; /// -/// fn main() { -/// let url_button = InlineKeyboardButton::url( -/// "Text".to_string(), -/// "http://url.com".to_string(), -/// ); -/// } +/// let url_button = InlineKeyboardButton::url( +/// "Text".to_string(), +/// "http://url.com".to_string(), +/// ); /// ``` impl InlineKeyboardButton { pub fn url(text: String, url: String) -> InlineKeyboardButton { diff --git a/src/types/inline_keyboard_markup.rs b/src/types/inline_keyboard_markup.rs index 89eb625e..67bd2aa8 100644 --- a/src/types/inline_keyboard_markup.rs +++ b/src/types/inline_keyboard_markup.rs @@ -5,7 +5,7 @@ use crate::types::InlineKeyboardButton; /// /// *Note*: This will only work in Telegram versions released after /// 9 April, 2016. Older clients will display unsupported message. -#[derive(Debug, Serialize, Deserialize, Hash, PartialEq, Eq, Clone)] +#[derive(Debug, Serialize, Deserialize, Hash, PartialEq, Eq, Clone, Default)] pub struct InlineKeyboardMarkup { /// Array of button rows, each represented by an Array of /// [`InlineKeyboardButton`] objects @@ -28,9 +28,7 @@ pub struct InlineKeyboardMarkup { /// ``` impl InlineKeyboardMarkup { pub fn new() -> Self { - Self { - inline_keyboard: vec![], - } + <_>::default() } pub fn append_row(mut self, buttons: Vec) -> Self { diff --git a/src/types/input_media.rs b/src/types/input_media.rs index 5ba0ce46..2fb8fff7 100644 --- a/src/types/input_media.rs +++ b/src/types/input_media.rs @@ -19,7 +19,7 @@ pub enum InputMedia { /// or inline URLs] in the media caption. /// /// [Markdown]: crate::types::ParseMode::Markdown - /// [Html]: crate::types::ParseMode::Html + /// [HTML]: crate::types::ParseMode::HTML /// [bold, italic, fixed-width text or inline URLs]: /// crate::types::ParseMode #[serde(skip_serializing_if = "Option::is_none")] @@ -45,7 +45,7 @@ pub enum InputMedia { /// or inline URLs] in the media caption. /// /// [Markdown]: crate::types::ParseMode::Markdown - /// [Html]: crate::types::ParseMode::Html + /// [HTML]: crate::types::ParseMode::HTML /// [bold, italic, fixed-width text or inline URLs]: /// crate::types::ParseMode #[serde(skip_serializing_if = "Option::is_none")] @@ -85,7 +85,7 @@ pub enum InputMedia { /// or inline URLs] in the media caption. /// /// [Markdown]: crate::types::ParseMode::Markdown - /// [Html]: crate::types::ParseMode::Html + /// [HTML]: crate::types::ParseMode::HTML /// [bold, italic, fixed-width text or inline URLs]: /// crate::types::ParseMode #[serde(skip_serializing_if = "Option::is_none")] @@ -121,7 +121,7 @@ pub enum InputMedia { /// or inline URLs] in the media caption. /// /// [Markdown]: crate::types::ParseMode::Markdown - /// [Html]: crate::types::ParseMode::Html + /// [HTML]: crate::types::ParseMode::HTML /// [bold, italic, fixed-width text or inline URLs]: /// crate::types::ParseMode #[serde(skip_serializing_if = "Option::is_none")] @@ -157,7 +157,7 @@ pub enum InputMedia { /// or inline URLs] in the media caption. /// /// [Markdown]: crate::types::ParseMode::Markdown - /// [Html]: crate::types::ParseMode::Html + /// [HTML]: crate::types::ParseMode::HTML /// [bold, italic, fixed-width text or inline URLs]: /// crate::types::ParseMode #[serde(skip_serializing_if = "Option::is_none")] diff --git a/src/types/input_message_content.rs b/src/types/input_message_content.rs index 60035735..a25d36da 100644 --- a/src/types/input_message_content.rs +++ b/src/types/input_message_content.rs @@ -19,7 +19,7 @@ pub enum InputMessageContent { /// or inline URLs] in the media caption. /// /// [Markdown]: crate::types::ParseMode::Markdown - /// [Html]: crate::types::ParseMode::Html + /// [HTML]: crate::types::ParseMode::HTML /// [bold, italic, fixed-width text or inline URLs]: /// crate::types::ParseMode #[serde(skip_serializing_if = "Option::is_none")] diff --git a/src/types/message.rs b/src/types/message.rs index baa5adeb..c658dd42 100644 --- a/src/types/message.rs +++ b/src/types/message.rs @@ -1,8 +1,4 @@ -use crate::types::{ - Animation, Audio, Chat, Contact, Document, Game, InlineKeyboardMarkup, - Invoice, Location, MessageEntity, PassportData, PhotoSize, Poll, Sticker, - SuccessfulPayment, User, Venue, Video, VideoNote, Voice, -}; +use crate::types::{Animation, Audio, Chat, Contact, Document, Game, InlineKeyboardMarkup, Invoice, Location, MessageEntity, PassportData, PhotoSize, Poll, Sticker, SuccessfulPayment, User, Venue, Video, VideoNote, Voice, True}; #[derive(Debug, Deserialize, PartialEq, Clone)] pub struct Message { @@ -14,19 +10,6 @@ pub struct Message { pub kind: MessageKind, } -impl Message { - pub fn text(&self) -> Option<&str> { - if let MessageKind::Common { - media_kind: MediaKind::Text { ref text, .. }, - .. - } = self.kind - { - Some(text) - } else { - None - } - } -} #[derive(Debug, Deserialize, PartialEq, Clone)] #[serde(untagged)] @@ -54,16 +37,16 @@ pub enum MessageKind { new_chat_photo: Vec, }, DeleteChatPhoto { - delete_chat_photo: bool, + delete_chat_photo: True, }, GroupChatCreated { - group_chat_created: bool, + group_chat_created: True, }, SupergroupChatCreated { - supergroup_chat_created: bool, + supergroup_chat_created: True, }, ChannelChatCreated { - channel_chat_created: bool, + channel_chat_created: True, }, Migrate { migrate_to_chat_id: i64, @@ -201,6 +184,375 @@ pub enum MediaKind { }, } +mod getters { + use std::ops::Deref; + + use crate::types::{ + self, Message, Sender, User, ForwardedFrom, Chat, MessageEntity, + PhotoSize, True, + message::{ + MessageKind::{ + Common, NewChatMembers, LeftChatMember, NewChatTitle, + NewChatPhoto, DeleteChatPhoto, GroupChatCreated, + ChannelChatCreated, Migrate, Invoice, SuccessfulPayment, + ConnectedWebsite, PassportData + }, + MediaKind::{ + Text, Video, Photo, Animation, Audio, Document, Voice, Game, + Sticker, VideoNote, Contact, Location, Poll, Venue + }, + ForwardKind::{NonChannelForward, ChannelForward, Origin} + }, + }; + use crate::types::message::MessageKind::{SupergroupChatCreated, Pinned}; + + /// Getters for [Message] fields from [telegram docs]. + /// + /// [Message]: crate::types::Message + /// [telegram docs]: https://core.telegram.org/bots/api#message + impl Message { + /// NOTE: this is getter for both `from` and `author_signature` + pub fn from(&self) -> Option<&Sender> { + match &self.kind { + Common { from, .. } => Some(from), + _ => None, + } + } + + /// NOTE: this is getter for both `forward_from` and + /// `forward_sender_name` + pub fn forward_from(&self) -> Option<&ForwardedFrom> { + match &self.kind { + 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), + _ => None, + } + } + + pub fn forward_from_message_id(&self) -> Option<&i32> { + match &self.kind { + 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), + _ => None, + } + } + + pub fn forward_date(&self) -> Option<&i32> { + match &self.kind { + 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), + _ => None, + } + } + + pub fn edit_date(&self) -> Option<&i32> { + match &self.kind { + Common { edit_date, .. } => edit_date.as_ref(), + _ => None, + } + } + + 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), + _ => None, + } + } + + pub fn text(&self) -> Option<&str> { + match &self.kind { + Common { media_kind: Text { text, .. }, .. } => Some(text), + _ => None, + } + } + + pub fn entities(&self) -> Option<&[MessageEntity]> { + match &self.kind { + 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), + _ => None, + } + } + + pub fn audio(&self) -> Option<&types::Audio> { + match &self.kind { + 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), + _ => None, + } + } + + pub fn animation(&self) -> Option<&types::Animation> { + match &self.kind { + 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), + _ => None, + } + } + + pub fn photo(&self) -> Option<&[PhotoSize]> { + match &self.kind { + 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), + _ => None, + } + } + + pub fn video(&self) -> Option<&types::Video> { + match &self.kind { + 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), + _ => None, + } + } + + pub fn video_note(&self) -> Option<&types::VideoNote> { + match &self.kind { + Common { media_kind: VideoNote { video_note, .. }, .. } => + Some(video_note), + _ => None, + } + } + + 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), + _ => None, + }, + _ => None, + } + } + + pub fn contact(&self) -> Option<&types::Contact> { + match &self.kind { + 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), + _ => None, + } + } + + pub fn venue(&self) -> Option<&types::Venue> { + match &self.kind { + 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), + _ => None, + } + } + + pub fn new_chat_members(&self) -> Option<&[User]> { + match &self.kind { + NewChatMembers { new_chat_members } => Some(new_chat_members), + _ => None, + } + } + + pub fn left_chat_member(&self) -> Option<&User> { + match &self.kind { + LeftChatMember { left_chat_member } => Some(left_chat_member), + _ => None, + } + } + + pub fn new_chat_title(&self) -> Option<&str> { + match &self.kind { + NewChatTitle { new_chat_title } => Some(new_chat_title), + _ => None, + } + } + + pub fn new_chat_photo(&self) -> Option<&[PhotoSize]> { + match &self.kind { + NewChatPhoto { new_chat_photo } => Some(new_chat_photo), + _ => None, + } + } + + // TODO: OK, `Option` is weird, can we do something with it? + // mb smt like `is_delete_chat_photo(&self) -> bool`? + pub fn delete_chat_photo(&self) -> Option { + match &self.kind { + DeleteChatPhoto { delete_chat_photo } => + Some(*delete_chat_photo), + _ => None, + } + } + + pub fn group_chat_created(&self) -> Option { + match &self.kind { + GroupChatCreated { group_chat_created } => + Some(*group_chat_created), + _ => None, + } + } + + pub fn super_group_chat_created(&self) -> Option { + match &self.kind { + SupergroupChatCreated { supergroup_chat_created } => + Some(*supergroup_chat_created), + _ => None, + } + } + + pub fn channel_chat_created(&self) -> Option { + match &self.kind { + 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), + _ => None, + } + } + + pub fn migrate_from_chat_id(&self) -> Option<&i64> { + match &self.kind { + Migrate { migrate_from_chat_id, .. } => + Some(migrate_from_chat_id), + _ => None, + } + } + + pub fn pinned_message(&self) -> Option<&Message> { + match &self.kind { + Pinned { pinned } => Some(pinned), + _ => None, + } + } + + pub fn invoice(&self) -> Option<&types::Invoice> { + match &self.kind { + Invoice { invoice } => Some(invoice), + _ => None, + } + } + + + pub fn successful_payment(&self) -> Option<&types::SuccessfulPayment> { + match &self.kind { + SuccessfulPayment { successful_payment } => + Some(successful_payment), + _ => None, + } + } + + + pub fn connected_website(&self) -> Option<&str> { + match &self.kind { + ConnectedWebsite { connected_website } => + Some(connected_website), + _ => None, + } + } + + + pub fn passport_data(&self) -> Option<&types::PassportData> { + match &self.kind { + PassportData { passport_data } => Some(passport_data), + _ => None, + } + } + + + pub fn reply_markup(&self) -> Option<&types::InlineKeyboardMarkup> { + match &self.kind { + Common { reply_markup, .. } => reply_markup.as_ref(), + _ => None, + } + } + } +} + + #[cfg(test)] mod tests { use serde_json::from_str; diff --git a/src/types/mod.rs b/src/types/mod.rs index 9846576b..4aafcb55 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -1,10 +1,11 @@ -use self::not_implemented_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, @@ -90,6 +91,8 @@ mod audio; mod callback_game; mod callback_query; mod chat; +mod chat_action; +mod chat_id; mod chat_member; mod chat_permissions; mod chat_photo; @@ -113,7 +116,6 @@ mod login_url; mod mask_position; mod message; mod message_entity; -mod not_implemented_types; mod order_info; mod parse_mode; mod photo_size; diff --git a/src/types/not_implemented_types.rs b/src/types/not_implemented_types.rs deleted file mode 100644 index c1e9d2ba..00000000 --- a/src/types/not_implemented_types.rs +++ /dev/null @@ -1,2 +0,0 @@ -#[derive(Debug, Deserialize, Eq, Hash, PartialEq, Serialize, Clone)] -pub struct PassportData; diff --git a/src/types/parse_mode.rs b/src/types/parse_mode.rs index dc54c1d5..393bf2ea 100644 --- a/src/types/parse_mode.rs +++ b/src/types/parse_mode.rs @@ -28,16 +28,16 @@ use serde::{Deserialize, Serialize}; /// /// Use the following syntax in your message: /// -/// ```ignore +///

 /// *bold text*
 /// _italic text_
 /// [inline URL](http://www.example.com/)
 /// [inline mention of a user](tg://user?id=123456789)
-/// `inline fixed-width code`
+/// `inline fixed-width code`
 /// ```block_language
 /// pre-formatted fixed-width code block
 /// ```
-/// ```
+/// 
/// /// ## HTML style /// To use this mode, pass [HTML] in the `parse_mode` field when using @@ -45,14 +45,14 @@ use serde::{Deserialize, Serialize}; /// /// The following tags are currently supported: /// -/// ```ignore -/// bold, bold -/// italic, italic -/// inline URL -/// inline mention of a user -/// inline fixed-width code -///
pre-formatted fixed-width code block
-/// ``` +///
+/// <b>bold</b>, <strong>bold</strong>
+/// <i>italic</i>, <em>italic</em>
+/// <a href="http://www.example.com/">inline URL</a>
+/// <a href="tg://user?id=123456789">inline mention of a user</a>
+/// <code>inline fixed-width code</code>
+/// <pre>pre-formatted fixed-width code block</pre>
+/// 
/// /// Please note: /// @@ -64,6 +64,10 @@ use serde::{Deserialize, Serialize}; /// - All numerical HTML entities are supported. /// - The API currently supports only the following named HTML entities: `<`, /// `>`, `&` and `"`. +/// +/// [Markdown]: crate::types::ParseMode::Markdown +/// [HTML]: crate::types::ParseMode::HTML +/// [SendMessage]: crate::requests::SendMessage pub enum ParseMode { HTML, Markdown, diff --git a/src/types/photo_size.rs b/src/types/photo_size.rs index 72d110df..530c10cb 100644 --- a/src/types/photo_size.rs +++ b/src/types/photo_size.rs @@ -1,6 +1,9 @@ #[derive(Debug, Deserialize, Eq, Hash, PartialEq, Serialize, Clone)] /// This object represents one size of a photo or a [`Document`] / /// [`Sticker`] thumbnail. +/// +/// [`Document`]: crate::types::Document +/// [`Sticker`]: crate::types::Sticker pub struct PhotoSize { /// Identifier for this file pub file_id: String, diff --git a/src/types/reply_keyboard_markup.rs b/src/types/reply_keyboard_markup.rs index d1a0212c..a80932f0 100644 --- a/src/types/reply_keyboard_markup.rs +++ b/src/types/reply_keyboard_markup.rs @@ -5,6 +5,8 @@ use crate::types::KeyboardButton; pub struct ReplyKeyboardMarkup { /// Array of button rows, each represented by an Array of /// [`KeyboardButton`] objects + /// + /// [`KeyboardButton`]: crate::types::KeyboardButton pub keyboard: Vec>, #[serde(skip_serializing_if = "Option::is_none")] @@ -32,5 +34,7 @@ pub struct ReplyKeyboardMarkup { /// Example: A user requests to change the bot‘s language, bot replies to /// the request with a keyboard to select the new language. Other users in /// the group don’t see the keyboard. + /// + /// [`Message`]: crate::types::Message pub selective: Option, } diff --git a/src/types/reply_keyboard_remove.rs b/src/types/reply_keyboard_remove.rs index 34190703..1ab4d9c2 100644 --- a/src/types/reply_keyboard_remove.rs +++ b/src/types/reply_keyboard_remove.rs @@ -3,6 +3,8 @@ /// By default, custom keyboards are displayed until a new keyboard is sent /// by a bot. An exception is made for one-time keyboards that are hidden /// immediately after the user presses a button (see [`ReplyKeyboardMarkup`]). +/// +/// [`ReplyKeyboardMarkup`]: crate::types::ReplyKeyboardMarkup #[derive(Debug, Serialize, Deserialize, Hash, PartialEq, Eq, Clone)] pub struct ReplyKeyboardRemove { /// equests clients to remove the custom keyboard (user will not be able to @@ -19,5 +21,7 @@ pub struct ReplyKeyboardRemove { /// Example: A user requests to change the bot‘s language, bot replies to /// the request with a keyboard to select the new language. Other users in /// the group don’t see the keyboard. + /// + /// [`Message`]: crate::types::Message pub selective: Option, } diff --git a/src/types/send_invoice.rs b/src/types/send_invoice.rs index 889606a7..63c70b79 100644 --- a/src/types/send_invoice.rs +++ b/src/types/send_invoice.rs @@ -1,5 +1,4 @@ -use crate::requests::ChatId; -use crate::types::{InlineKeyboardMarkup, LabeledPrice}; +use crate::types::{ChatId, InlineKeyboardMarkup, LabeledPrice}; #[derive(Debug, PartialEq, Eq, Clone)] pub struct SendInvoice { diff --git a/src/types/unit_true.rs b/src/types/unit_true.rs index 91a86409..0efe1b33 100644 --- a/src/types/unit_true.rs +++ b/src/types/unit_true.rs @@ -4,16 +4,11 @@ use serde::ser::{Serialize, Serializer}; #[derive(Copy, Clone, Debug, Default, Eq, Hash, PartialEq, PartialOrd, Ord)] pub struct True; -impl std::process::Termination for True { - fn report(self) -> i32 { - libc::EXIT_SUCCESS - } -} - impl std::convert::TryFrom for True { type Error = (); fn try_from(value: bool) -> Result { + #[allow(clippy::match_bool)] match value { true => Ok(True), false => Err(()), @@ -43,6 +38,7 @@ impl<'de> Visitor<'de> for TrueVisitor { where E: de::Error, { + #[allow(clippy::match_bool)] match value { true => Ok(True), false => Err(E::custom("expected `true`, found `false`")),