diff --git a/README.md b/README.md index d3565e12..6e0c581c 100644 --- a/README.md +++ b/README.md @@ -5,3 +5,8 @@ + +## Dependency graph +
+ +
diff --git a/graph.png b/graph.png new file mode 100644 index 00000000..8c66df6f Binary files /dev/null and b/graph.png differ diff --git a/src/bot/mod.rs b/src/bot/mod.rs index d87df13f..74ff89d4 100644 --- a/src/bot/mod.rs +++ b/src/bot/mod.rs @@ -1,7 +1,24 @@ use reqwest::r#async::Client; -use crate::core::requests::{ - get_me::GetMe, send_message::SendMessage, ChatId, RequestContext, +use crate::core::{ + types::{ + InputFile, + InputMedia, + }, + requests::{ + ChatId, + RequestContext, + + get_me::GetMe, + send_message::SendMessage, + edit_message_live_location::EditMessageLiveLocation, + forward_message::ForwardMessage, + send_audio::SendAudio, + send_location::SendLocation, + send_media_group::SendMediaGroup, + send_photo::SendPhoto, + stop_message_live_location::StopMessageLiveLocation, + } }; pub struct Bot { @@ -23,15 +40,19 @@ impl Bot { client, } } + + fn ctx(&self) -> RequestContext { + RequestContext { + token: &self.token, + client: &self.client, + } + } } /// Telegram functions impl Bot { pub fn get_me(&self) -> GetMe { - GetMe::new(RequestContext { - token: &self.token, - client: &self.client, - }) + GetMe::new(self.ctx()) } pub fn send_message(&self, chat_id: C, text: T) -> SendMessage @@ -40,12 +61,105 @@ impl Bot { T: Into, { SendMessage::new( - RequestContext { - token: &self.token, - client: &self.client, - }, + self.ctx(), chat_id.into(), text.into(), ) } + + pub fn edit_message_live_location( + &self, + latitude: Lt, + longitude: Lg, + ) -> EditMessageLiveLocation + where + Lt: Into, + Lg: Into, + { + EditMessageLiveLocation::new( + self.ctx(), + latitude.into(), + longitude.into(), + ) + } + + pub fn forward_message( + &self, + chat_id: C, + from_chat_id: F, + message_id: M + ) -> ForwardMessage + where + C: Into, + F: Into, + M: Into, + { + ForwardMessage::new( + self.ctx(), + chat_id.into(), + from_chat_id.into(), + message_id.into(), + ) + } + + pub fn send_audio(&self, chat_id: C, audio: A) -> SendAudio + where + C: Into, + A: Into, + { + SendAudio::new( + self.ctx(), + chat_id.into(), + audio.into() + ) + } + + pub fn send_location( + &self, + chat_id: C, + latitude: Lt, + longitude: Lg, + ) -> SendLocation + where + C: Into, + Lt: Into, + Lg: Into, + { + SendLocation::new( + self.ctx(), + chat_id.into(), + latitude.into(), + longitude.into(), + ) + } + + pub fn send_media_group(&self, chat_id: C, media: M) -> SendMediaGroup + where + C: Into, + M: Into> + { + SendMediaGroup::new( + self.ctx(), + chat_id.into(), + media.into(), + ) + } + + pub fn send_photo(&self, chat_id: C, photo: P) -> SendPhoto + where + C: Into, + P: Into + { + SendPhoto::new( + self.ctx(), + chat_id.into(), + photo.into(), + ) + } + + pub fn stop_message_live_location(&self) -> StopMessageLiveLocation { + StopMessageLiveLocation::new( + self.ctx() + ) + } } diff --git a/src/core/network/mod.rs b/src/core/network/mod.rs index 8fdde960..244c6d63 100644 --- a/src/core/network/mod.rs +++ b/src/core/network/mod.rs @@ -9,7 +9,6 @@ use reqwest::{ StatusCode, }; use serde::{de::DeserializeOwned, Serialize}; -use serde_json::Value; const TELEGRAM_API_URL: &str = "https://api.telegram.org"; @@ -63,7 +62,7 @@ pub async fn request_multipart( TelegramResponse::Err { description, error_code, - response_parameters, + response_parameters: _, .. } => Err(RequestError::ApiError { description, @@ -95,7 +94,7 @@ pub async fn request_json( TelegramResponse::Err { description, error_code, - response_parameters, + response_parameters: _, .. } => Err(RequestError::ApiError { description, @@ -108,11 +107,17 @@ pub async fn request_json( #[serde(untagged)] enum TelegramResponse { Ok { - ok: bool, // true + /// Dummy field. Used for deserialization. + #[allow(dead_code)] + ok: bool, // TODO: True type + result: R, }, Err { - ok: bool, // false + /// Dummy field. Used for deserialization. + #[allow(dead_code)] + ok: bool, // TODO: False type + description: String, error_code: u16, response_parameters: Option, diff --git a/src/core/requests/answer_pre_checkout_query.rs b/src/core/requests/answer_pre_checkout_query.rs new file mode 100644 index 00000000..fb995ae2 --- /dev/null +++ b/src/core/requests/answer_pre_checkout_query.rs @@ -0,0 +1,83 @@ +use crate::core::{ + requests::{RequestContext, Request, RequestFuture, ResponseResult}, + network +}; + +#[derive(Debug, Serialize, Clone)] +/// Once the user has confirmed their payment and shipping details, the Bot API +/// sends the final confirmation in the form of an [`Update`] with the field +/// 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. +pub struct AnswerPreCheckoutQuery<'a> { + #[serde(skip_serializing)] + ctx: RequestContext<'a>, + + /// Unique identifier for the query to be answered + pub pre_checkout_query_id: String, + + /// Specify True if everything is alright (goods are available, etc.) and + /// the bot is ready to proceed with the order. Use False if there are any + /// problems. + pub ok: bool, + + #[serde(skip_serializing_if = "Option::is_none")] + /// Required if ok is False. Error message in human readable form that + /// explains the reason for failure to proceed with the checkout (e.g. + /// "Sorry, somebody just bought the last of our amazing black T-shirts + /// while you were busy filling out your payment details. Please choose a + /// different color or garment!"). Telegram will display this message to + /// the user. + pub error_message: Option, +} + +impl<'a> Request<'a> for AnswerPreCheckoutQuery<'a> { + type ReturnValue = bool; + + fn send(self) -> RequestFuture<'a, ResponseResult> { + Box::pin(async move { + network::request_json( + &self.ctx.client, + &self.ctx.token, + "answerPreCheckoutQuery", + &self + ).await + }) + } +} + +impl<'a> AnswerPreCheckoutQuery<'a> { + pub(crate) fn new( + ctx: RequestContext<'a>, + pre_checkout_query_id: String, + ok: bool + ) -> Self { + Self { + ctx, + pre_checkout_query_id, + ok, + error_message: None + } + } + + pub fn pre_checkout_query_id(mut self, pre_checkout_query_id: T) -> Self + where T: Into + { + self.pre_checkout_query_id = pre_checkout_query_id.into(); + self + } + + pub fn ok(mut self, ok: T) -> Self + where T: Into + { + self.ok = ok.into(); + self + } + + pub fn error_message(mut self, error_message: T) -> Self + where T: Into + { + self.error_message = Some(error_message.into()); + self + } +} diff --git a/src/core/requests/answer_shipping_query.rs b/src/core/requests/answer_shipping_query.rs new file mode 100644 index 00000000..74728d92 --- /dev/null +++ b/src/core/requests/answer_shipping_query.rs @@ -0,0 +1,91 @@ +use crate::core::types::ShippingOption; +use crate::core::requests::{RequestContext, Request, RequestFuture, ResponseResult}; +use crate::core::network; + +#[derive(Debug, Clone, Serialize)] +/// If you sent an invoice requesting a shipping address and the parameter +/// 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. +pub struct AnswerShippingQuery<'a> { + #[serde(skip_serializing)] + ctx: RequestContext<'a>, + + /// Unique identifier for the query to be answered + pub shipping_query_id: String, + /// Specify True if delivery to the specified address is possible and False + /// if there are any problems (for example, if delivery to the specified + /// address is not possible) + pub ok: bool, + + #[serde(skip_serializing_if = "Option::is_none")] + /// Required if ok is True. A JSON-serialized array of available shipping + /// options. + pub shipping_options: Option>, + + #[serde(skip_serializing_if = "Option::is_none")] + /// Required if ok is False. Error message in human readable form that + /// explains why it is impossible to complete the order (e.g. "Sorry, + /// delivery to your desired address is unavailable'). Telegram will + /// display this message to the user. + pub error_message: Option, +} + +impl<'a> Request<'a> for AnswerShippingQuery<'a> { + type ReturnValue = bool; + + fn send(self) -> RequestFuture<'a, ResponseResult> { + Box::pin(async move { + network::request_json( + &self.ctx.client, + &self.ctx.token, + "answerShippingQuery", + &self + ).await + }) + } +} + +impl<'a> AnswerShippingQuery<'a> { + pub(crate) fn new( + ctx: RequestContext<'a>, + shipping_query_id: String, + ok: bool + ) -> Self { + Self { + ctx, + shipping_query_id, + ok, + shipping_options: None, + error_message: None + } + } + + pub fn shipping_query_id(mut self, shipping_query_id: T) -> Self + where T: Into + { + self.shipping_query_id = shipping_query_id.into(); + self + } + + pub fn ok(mut self, ok: T) -> Self + where T: Into + { + self.ok = ok.into(); + self + } + + pub fn shipping_options(mut self, shipping_options: T) -> Self + where T: Into> + { + self.shipping_options = Some(shipping_options.into()); + self + } + + pub fn error_message(mut self, error_message: T) -> Self + where T: Into + { + self.error_message = Some(error_message.into()); + self + } +} diff --git a/src/core/requests/form_builder.rs b/src/core/requests/form_builder.rs index e266c837..d9bd8b69 100644 --- a/src/core/requests/form_builder.rs +++ b/src/core/requests/form_builder.rs @@ -6,7 +6,6 @@ use crate::core::{ }; use reqwest::r#async::multipart::Form; -use serde::Serialize; /// This is a convenient struct that builds `reqwest::r#async::multipart::Form` /// from scratch. diff --git a/src/core/requests/forward_message.rs b/src/core/requests/forward_message.rs index 8e61bce6..dc751bfd 100644 --- a/src/core/requests/forward_message.rs +++ b/src/core/requests/forward_message.rs @@ -1,8 +1,11 @@ use crate::core::{ network, requests::{ - form_builder::FormBuilder, ChatId, Request, RequestContext, - RequestFuture, ResponseResult, + ChatId, + Request, + RequestFuture, + RequestContext, + ResponseResult, }, types::Message, }; diff --git a/src/core/requests/mod.rs b/src/core/requests/mod.rs index 05e69f7f..a1dfa82f 100644 --- a/src/core/requests/mod.rs +++ b/src/core/requests/mod.rs @@ -89,6 +89,8 @@ mod tests { } } +pub mod answer_pre_checkout_query; +pub mod answer_shipping_query; pub mod edit_message_live_location; pub mod forward_message; pub mod get_file; diff --git a/src/core/requests/send_chat_action.rs b/src/core/requests/send_chat_action.rs index ad686836..f8a1142c 100644 --- a/src/core/requests/send_chat_action.rs +++ b/src/core/requests/send_chat_action.rs @@ -1,8 +1,83 @@ -use crate::core::requests::RequestContext; -//TODO:: need implementation +use crate::core::network; +use crate::core::requests::{ChatId, Request, RequestContext, RequestFuture, ResponseResult}; +use crate::core::types::Message; +///Use this method when you need to tell the user that something is happening on the bot's side. +///The status is set for 5 seconds or less (when a message arrives from your bot, Telegram clients clear its typing status). +///Returns True on success. #[derive(Debug, Clone, Serialize)] struct SendChatAction<'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, + /// Type of action to broadcast. Choose one, depending on what the user is + /// about to receive: typing for text messages, upload_photo for photos, + /// record_video or upload_video for videos, record_audio or upload_audio + /// for audio files, upload_document for general files, find_location for + /// location data, record_video_note or upload_video_note for video notes. + pub action: ChatAction, } + +#[derive(Debug, Serialize, From, Clone)] +#[serde(rename_all = "snake_case")] +enum ChatAction { + Typing, + UploadPhoto, + RecordVideo, + UploadVideo, + RecordAudio, + UploadAudio, + UploadDocument, + FindLocation, + RecordVideoNote, + UploadVideoNote, +} + +impl<'a> Request<'a> for SendChatAction<'a> { + type ReturnValue = bool; + + fn send(self) -> RequestFuture<'a, ResponseResult> { + Box::pin(async move { + network::request_json( + &self.ctx.client, + &self.ctx.token, + "sendChatAction", + &self, + ) + .await + }) + } +} + +impl<'a> SendChatAction<'a> { + pub(crate) fn new( + ctx: RequestContext<'a>, + chat_id: ChatId, + action: ChatAction, + ) -> Self { + Self { + ctx, + chat_id, + action, + } + } + + pub fn chat_id(mut self, chat_id: T) -> Self + where + T: Into, + { + self.chat_id = chat_id.into(); + self + } + + + pub fn action(mut self, action: T) -> Self + where + T: Into, + { + self.action = action.into(); + self + } +} \ No newline at end of file diff --git a/src/core/requests/send_contact.rs b/src/core/requests/send_contact.rs index 6ffd1f2a..16cbec0e 100644 --- a/src/core/requests/send_contact.rs +++ b/src/core/requests/send_contact.rs @@ -1,7 +1,142 @@ -use crate::core::requests::RequestContext; -//TODO:: need implementation +use crate::core::network; +use crate::core::requests::{ + ChatId, Request, RequestContext, RequestFuture, ResponseResult, +}; +use crate::core::types::{Message, ReplyMarkup}; + +/// Use this method to send phone contacts. +/// returned. #[derive(Debug, Clone, Serialize)] struct SendContact<'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, + /// Contact's phone number + pub phone_number: String, + /// Contact's first name + pub first_name: String, + /// Contact's last name + #[serde(skip_serializing_if = "Option::is_none")] + pub last_name: Option, + /// Additional data about the contact in the form of a + /// vCard, 0-2048 bytes + #[serde(skip_serializing_if = "Option::is_none")] + pub vcard: 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, + /// InlineKeyboardMarkup or ReplyKeyboardMarkup or ReplyKeyboardRemove + /// or ForceReply Optional Additional interface options. A JSON-serialized + /// object for an inline keyboard, custom reply keyboard, instructions to + /// remove keyboard or to force a reply from the user. + #[serde(skip_serializing_if = "Option::is_none")] + pub reply_markup: Option, +} + +impl<'a> Request<'a> for SendContact<'a> { + type ReturnValue = Message; + + fn send(self) -> RequestFuture<'a, ResponseResult> { + Box::pin(async move { + network::request_json( + &self.ctx.client, + &self.ctx.token, + "sendContact", + &self, + ) + .await + }) + } +} + +impl<'a> SendContact<'a> { + pub(crate) fn new( + ctx: RequestContext<'a>, + chat_id: ChatId, + phone_number: String, + first_name: String, + ) -> Self { + Self { + ctx, + chat_id, + phone_number, + first_name, + last_name: None, + vcard: None, + disable_notification: None, + reply_to_message_id: None, + reply_markup: None, + } + } + + pub fn chat_id(mut self, chat_id: T) -> Self + where + T: Into, + { + self.chat_id = chat_id.into(); + self + } + + pub fn phone_number(mut self, phone_number: T) -> Self + where + T: Into, + { + self.phone_number = phone_number.into(); + self + } + + pub fn first_name(mut self, first_name: T) -> Self + where + T: Into, + { + self.first_name = first_name.into(); + self + } + + pub fn last_name(mut self, last_name: T) -> Self + where + T: Into, + { + self.last_name = Some(last_name.into()); + self + } + + pub fn vcard(mut self, vcard: T) -> Self + where + T: Into, + { + self.vcard = Some(vcard.into()); + self + } + + pub fn disable_notification(mut self, disable_notification: T) -> Self + where + T: Into, + { + self.disable_notification = Some(disable_notification.into()); + self + } + + pub fn reply_to_message_id(mut self, reply_to_message_id: T) -> Self + where + T: Into, + { + self.reply_to_message_id = Some(reply_to_message_id.into()); + self + } + + pub fn reply_markup(mut self, reply_markup: T) -> Self + where + T: Into, + { + self.reply_markup = Some(reply_markup.into()); + self + } } diff --git a/src/core/requests/send_message.rs b/src/core/requests/send_message.rs index 889bcb14..d3bf2421 100644 --- a/src/core/requests/send_message.rs +++ b/src/core/requests/send_message.rs @@ -1,8 +1,11 @@ use crate::core::{ network, requests::{ - form_builder::FormBuilder, ChatId, Request, RequestContext, - RequestFuture, ResponseResult, + ChatId, + Request, + RequestFuture, + RequestContext, + ResponseResult, }, types::{Message, ParseMode, ReplyMarkup}, }; diff --git a/src/core/requests/send_photo.rs b/src/core/requests/send_photo.rs index 0d4e40e3..9d2a07b5 100644 --- a/src/core/requests/send_photo.rs +++ b/src/core/requests/send_photo.rs @@ -1,5 +1,3 @@ -use std::path::Path; - use crate::core::{ network, requests::{ diff --git a/src/core/requests/send_poll.rs b/src/core/requests/send_poll.rs index 8341bb39..861c98e7 100644 --- a/src/core/requests/send_poll.rs +++ b/src/core/requests/send_poll.rs @@ -1,8 +1,114 @@ -use crate::core::requests::RequestContext; -//TODO:: need implementation +use crate::core::network; +use crate::core::requests::{ + ChatId, Request, RequestContext, RequestFuture, ResponseResult, +}; +use crate::core::types::{Message, ReplyMarkup}; +/// Use this method to send a native poll. A native poll can't be sent to a +/// private chat. On success, the sent Message is returned. #[derive(Debug, Clone, Serialize)] struct SendPoll<'a> { #[serde(skip_serializing)] ctx: RequestContext<'a>, + /// identifier for the target chat or username of the target channel (in + /// the format @channelusername). A native poll can't be sent to a private + /// chat. + chat_id: ChatId, + /// Poll question, 1-255 characters + question: String, + /// List of answer options, 2-10 strings 1-100 characters each + options: Vec, + /// Sends the message silently. Users will receive a notification with no + /// sound. + disable_notification: Option, + /// If the message is a reply, ID of the original message + reply_to_message_id: Option, + /// InlineKeyboardMarkup or ReplyKeyboardMarkup or ReplyKeyboardRemove + /// or ForceReply Optional 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. + reply_markup: Option, +} + +impl<'a> Request<'a> for SendPoll<'a> { + type ReturnValue = Message; + + fn send(self) -> RequestFuture<'a, ResponseResult> { + Box::pin(async move { + network::request_json( + &self.ctx.client, + &self.ctx.token, + "sendPoll", + &self, + ) + .await + }) + } +} + +impl<'a> SendPoll<'a> { + pub(crate) fn new( + ctx: RequestContext<'a>, + chat_id: ChatId, + question: String, + options: Vec, + ) -> Self { + Self { + ctx, + chat_id, + question, + options, + disable_notification: None, + reply_to_message_id: None, + reply_markup: None, + } + } + + pub fn chat_id(mut self, chat_id: T) -> Self + where + T: Into, + { + self.chat_id = chat_id.into(); + self + } + + pub fn question(mut self, question: T) -> Self + where + T: Into, + { + self.question = question.into(); + self + } + + pub fn options(mut self, options: T) -> Self + where + T: Into>, + { + self.options = options.into(); + self + } + + pub fn disable_notification(mut self, disable_notification: T) -> Self + where + T: Into, + { + self.disable_notification = Some(disable_notification.into()); + self + } + + pub fn reply_to_message_id(mut self, reply_to_message_id: T) -> Self + where + T: Into, + { + self.reply_to_message_id = Some(reply_to_message_id.into()); + self + } + + pub fn reply_markup(mut self, reply_markup: T) -> Self + where + T: Into, + { + self.reply_markup = Some(reply_markup.into()); + self + } } diff --git a/src/core/requests/send_venue.rs b/src/core/requests/send_venue.rs index deac9b61..85fee6d6 100644 --- a/src/core/requests/send_venue.rs +++ b/src/core/requests/send_venue.rs @@ -1,43 +1,158 @@ -use crate::core::requests::{ChatId, RequestContext}; +use crate::core::network; +use crate::core::requests::{ + ChatId, Request, RequestContext, RequestFuture, ResponseResult, +}; +use crate::core::types::{Message, ReplyMarkup}; -//TODO:: need implementation -///Use this method to send information about a venue. On success, the sent +/// Use this method to send information about a venue. /// Message is returned. #[derive(Debug, Clone, Serialize)] struct SendVenue<'a> { #[serde(skip_serializing)] ctx: RequestContext<'a>, - /// Integer or String Yes Unique identifier for the target chat or + /// Unique identifier for the target chat or /// username of the target channel (in the format @channelusername) - chat_id: ChatId, - /// Float number Yes Latitude of the venue - latitude: f64, - ///Float number Yes Longitude of the venue - longitude: f64, - /// Yes Name of the venue - title: String, - ///String Yes Address of the venue - address: String, - /// String Optional Foursquare identifier of the venue + pub chat_id: ChatId, + /// Latitude of the venue + pub latitude: f64, + /// Longitude of the venue + pub longitude: f64, + /// Name of the venue + pub title: String, + /// Address of the venue + pub address: String, + /// Foursquare identifier of the venue #[serde(skip_serializing_if = "Option::is_none")] - foursquare_id: Option, - /// String Optional Foursquare type of the venue, if known. (For + pub foursquare_id: Option, + /// Foursquare type of the venue, if known. (For /// example, “arts_entertainment/default”, “arts_entertainment/aquarium” or /// “food/icecream”.) #[serde(skip_serializing_if = "Option::is_none")] - foursquare_type: Option, - /// Boolean Optional Sends the message silently. Users will receive a + pub foursquare_type: Option, + /// Sends the message silently. Users will receive a /// notification with no sound. #[serde(skip_serializing_if = "Option::is_none")] - disable_notification: Option, - /// Integer Optional If the message is a reply, ID of the original + pub disable_notification: Option, + /// If the message is a reply, ID of the original /// message #[serde(skip_serializing_if = "Option::is_none")] - reply_to_message_id: Option, + pub reply_to_message_id: Option, /// InlineKeyboardMarkup or ReplyKeyboardMarkup or ReplyKeyboardRemove or /// ForceReply Optional 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")] - reply_markup: Option<()>, //TODO: need concrete type + pub reply_markup: Option, +} + +impl<'a> Request<'a> for SendVenue<'a> { + type ReturnValue = Message; + + fn send(self) -> RequestFuture<'a, ResponseResult> { + Box::pin(async move { + network::request_json( + &self.ctx.client, + &self.ctx.token, + "sendVenue", + &self, + ) + .await + }) + } +} + +impl<'a> SendVenue<'a> { + pub fn new( + ctx: RequestContext<'a>, + chat_id: ChatId, + latitude: f64, + longitude: f64, + title: String, + address: String, + ) -> Self { + Self { + ctx, + chat_id, + latitude, + longitude, + title, + address, + foursquare_id: None, + foursquare_type: None, + disable_notification: None, + reply_to_message_id: None, + reply_markup: None, + } + } + pub fn chat_id(mut self, chat_id: T) -> Self + where + T: Into, + { + self.chat_id = chat_id.into(); + self + } + + pub fn longitude(mut self, longitude: T) -> Self + where + T: Into, + { + self.longitude = longitude.into(); + self + } + + pub fn latitude(mut self, latitude: T) -> Self + where + T: Into, + { + self.latitude = latitude.into(); + self + } + + pub fn title(mut self, title: T) -> Self + where + T: Into, + { + self.title = title.into(); + self + } + + pub fn address(mut self, address: T) -> Self + where + T: Into, + { + self.address = address.into(); + self + } + + pub fn foursquare_id(mut self, foursquare_id: T) -> Self + where + T: Into, + { + self.foursquare_id = Some(foursquare_id.into()); + self + } + + pub fn disable_notification(mut self, disable_notification: T) -> Self + where + T: Into, + { + self.disable_notification = Some(disable_notification.into()); + self + } + + pub fn foursquare_type(mut self, foursquare_type: T) -> Self + where + T: Into, + { + self.foursquare_type = Some(foursquare_type.into()); + self + } + + pub fn reply_markup(mut self, reply_markup: T) -> Self + where + T: Into, + { + self.reply_markup = Some(reply_markup.into()); + self + } } diff --git a/src/core/requests/stop_message_live_location.rs b/src/core/requests/stop_message_live_location.rs index 2598f4bf..c86ee1ed 100644 --- a/src/core/requests/stop_message_live_location.rs +++ b/src/core/requests/stop_message_live_location.rs @@ -1,19 +1,20 @@ -use std::path::Path; - use crate::core::{ network, requests::{ - form_builder::FormBuilder, ChatId, Request, RequestContext, - RequestFuture, ResponseResult, + ChatId, + Request, + RequestFuture, + RequestContext, + ResponseResult, }, - types::{InlineKeyboardMarkup, Message, ParseMode}, + types::{InlineKeyboardMarkup, Message}, }; /// Use this method to stop updating a live location message before live_period /// expires. On success, if the message was sent by the bot, the sent Message is /// returned, otherwise True is returned. #[derive(Debug, Clone, Serialize)] -struct StopMessageLiveLocation<'a> { +pub struct StopMessageLiveLocation<'a> { #[serde(skip_serializing)] ctx: RequestContext<'a>, /// Required if inline_message_id is not specified. Unique identifier for @@ -52,7 +53,7 @@ impl<'a> Request<'a> for StopMessageLiveLocation<'a> { } impl<'a> StopMessageLiveLocation<'a> { - fn new(ctx: RequestContext<'a>) -> Self { + pub(crate) fn new(ctx: RequestContext<'a>) -> Self { Self { ctx, chat_id: None, diff --git a/src/core/requests/utils.rs b/src/core/requests/utils.rs index 44231ef2..193bad0c 100644 --- a/src/core/requests/utils.rs +++ b/src/core/requests/utils.rs @@ -1,9 +1,12 @@ +use std::path::PathBuf; + use bytes::{Bytes, BytesMut}; use reqwest::r#async::multipart::Part; -use std::fs::File; -use std::path::PathBuf; -use tokio::codec::FramedRead; -use tokio::prelude::*; +use tokio::{ + prelude::*, + codec::FramedRead, +}; + struct FileDecoder; diff --git a/src/core/types/animation.rs b/src/core/types/animation.rs index 09fccf40..4a60e4c5 100644 --- a/src/core/types/animation.rs +++ b/src/core/types/animation.rs @@ -1,13 +1,63 @@ use crate::core::types::PhotoSize; #[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] +/// This object represents an animation file (GIF or H.264/MPEG-4 AVC video +/// without sound). pub struct Animation { + /// Identifier for this file pub file_id: String, + /// Video width as defined by sender pub width: u32, + /// Video height as defined by sender pub height: u32, + /// Duration of the video in seconds as defined by sender pub duration: u32, - pub thumb: PhotoSize, + /// Optional. Animation thumbnail as defined by sender + pub thumb: Option, + /// Optional. Original animation filename as defined by sender pub file_name: Option, + /// Optional. MIME type of the file as defined by sender pub mime_type: Option, + /// Optional. File size pub file_size: Option } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn deserialize() { + let json = r#"{ + "file_id":"id", + "width":320, + "height":320, + "duration":59, + "thumb":{ + "file_id":"id", + "width":320, + "height":320, + "file_size":3452 + }, + "file_name":"some", + "mime_type":"gif", + "file_size":6500}"#; + let expected = Animation { + file_id: "id".to_string(), + width: 320, + height: 320, + duration: 59, + thumb: Some(PhotoSize { + file_id: "id".to_string(), + width: 320, + height: 320, + file_size: Some(3452) + }), + file_name: Some("some".to_string()), + mime_type: Some("gif".to_string()), + file_size: Some(6500) + }; + let actual = serde_json::from_str::(json).unwrap(); + assert_eq!(actual, expected) + } +} diff --git a/src/core/types/answer_pre_checkout_query.rs b/src/core/types/answer_pre_checkout_query.rs deleted file mode 100644 index 07d1c758..00000000 --- a/src/core/types/answer_pre_checkout_query.rs +++ /dev/null @@ -1,7 +0,0 @@ -#[derive(Debug, Deserialize, Hash, PartialEq, Eq, Clone)] -pub struct AnswerPreCheckoutQuery { - pub pre_checkout_query_id: String, - pub ok: bool, - #[serde(skip_serializing_if = "Option::is_none")] - pub error_message: Option, -} diff --git a/src/core/types/answer_shipping_query.rs b/src/core/types/answer_shipping_query.rs deleted file mode 100644 index c04c016b..00000000 --- a/src/core/types/answer_shipping_query.rs +++ /dev/null @@ -1,11 +0,0 @@ -use crate::core::types::ShippingOption; - -#[derive(Debug, Deserialize, Hash, PartialEq, Eq, Clone)] -pub struct AnswerShippingQuery { - pub shipping_query_id: String, - pub ok: bool, - #[serde(skip_serializing_if = "Option::is_none")] - pub shipping_options: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub error_message: Option, -} diff --git a/src/core/types/inline_keyboard_button.rs b/src/core/types/inline_keyboard_button.rs index 73389ae6..ba2bcc4f 100644 --- a/src/core/types/inline_keyboard_button.rs +++ b/src/core/types/inline_keyboard_button.rs @@ -40,3 +40,54 @@ pub enum InlineKeyboardButtonKind { /* CallbackGame(CallbackGame), TODO: разобраться, что с этим делать * TODO: add LoginUrl, pay */ } + +/// Build buttons +/// +/// Example: +/// ```edition2018 +/// use async_telegram_bot::core::types::InlineKeyboardButton; +/// +/// fn main() { +/// let url_button = InlineKeyboardButton::url( +/// "Text".to_string(), +/// "http://url.com".to_string(), +/// ); +/// } +/// ``` +impl InlineKeyboardButton { + pub fn url(text: String, url: String) -> InlineKeyboardButton { + InlineKeyboardButton { + text, + kind: InlineKeyboardButtonKind::Url(url), + } + } + + pub fn callback(text: String, callback_data: String) + -> InlineKeyboardButton { + InlineKeyboardButton { + text, + kind: InlineKeyboardButtonKind::CallbackData(callback_data), + } + } + + pub fn switch_inline_query(text: String, switch_inline_query: String) + -> InlineKeyboardButton { + InlineKeyboardButton { + text, + kind: InlineKeyboardButtonKind::SwitchInlineQuery(switch_inline_query) + } + } + + pub fn switch_inline_query_current_chat( + text: String, + switch_inline_query_current_chat: String + ) -> InlineKeyboardButton { + + InlineKeyboardButton { + text, + kind: InlineKeyboardButtonKind::SwitchInlineQueryCurrentChat( + switch_inline_query_current_chat + ) + } + } +} diff --git a/src/core/types/inline_keyboard_markup.rs b/src/core/types/inline_keyboard_markup.rs index 77f8eee7..5dd68af3 100644 --- a/src/core/types/inline_keyboard_markup.rs +++ b/src/core/types/inline_keyboard_markup.rs @@ -11,3 +11,111 @@ pub struct InlineKeyboardMarkup { /// [`InlineKeyboardButton`] objects pub inline_keyboard: Vec>, } + +/// Build Markup +/// +/// Example: +/// ```edition2018 +/// use async_telegram_bot::core::types::{ +/// InlineKeyboardMarkup, +/// InlineKeyboardButton +/// }; +/// +/// fn main() { +/// let url_button = InlineKeyboardButton::url( +/// "text".to_string(), +/// "http://url.com".to_string() +/// ); +/// let keyboard = InlineKeyboardMarkup::new() +/// .row(vec![url_button]); +/// } +/// ``` +impl InlineKeyboardMarkup { + pub fn new() -> Self { + Self { + inline_keyboard: vec![] + } + } + + pub fn append_row(mut self, buttons: Vec) -> Self { + self.inline_keyboard.push(buttons); + self + } + + pub fn append_to_row(mut self, button: InlineKeyboardButton, index: usize) + -> Self { + match self.inline_keyboard.get_mut(index) { + Some(buttons) => buttons.push(button), + None => self.inline_keyboard.push(vec![button]) + }; + self + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn append_row() { + let button1 = InlineKeyboardButton::url( + "text 1".to_string(), + "url 1".to_string(), + ); + let button2 = InlineKeyboardButton::url( + "text 2".to_string(), + "url 2".to_string(), + ); + let markup = InlineKeyboardMarkup::new() + .append_row(vec![button1.clone(), button2.clone()]); + let expected = InlineKeyboardMarkup { + inline_keyboard: vec![ + vec![button1.clone(), button2.clone()] + ] + }; + assert_eq!(markup, expected); + } + + #[test] + fn append_to_row__existent_row() { + let button1 = InlineKeyboardButton::url( + "text 1".to_string(), + "url 1".to_string(), + ); + let button2 = InlineKeyboardButton::url( + "text 2".to_string(), + "url 2".to_string(), + ); + let markup = InlineKeyboardMarkup::new() + .append_row(vec![button1.clone()]) + .append_to_row(button2.clone(), 0); + let expected = InlineKeyboardMarkup { + inline_keyboard: vec![ + vec![button1.clone(), button2.clone()] + ] + }; + assert_eq!(markup, expected); + } + + #[test] + fn append_to_row__nonexistent_row() { + let button1 = InlineKeyboardButton::url( + "text 1".to_string(), + "url 1".to_string(), + ); + let button2 = InlineKeyboardButton::url( + "text 2".to_string(), + "url 2".to_string(), + ); + let markup = InlineKeyboardMarkup::new() + .append_row(vec![button1.clone()]) + .append_to_row(button2.clone(), 1); + let expected = InlineKeyboardMarkup { + inline_keyboard: vec![ + vec![button1.clone()], + vec![button2.clone()] + ] + }; + assert_eq!(markup, expected); + } +} diff --git a/src/core/types/label_price.rs b/src/core/types/label_price.rs index a9ced54e..d7816ef3 100644 --- a/src/core/types/label_price.rs +++ b/src/core/types/label_price.rs @@ -1,5 +1,29 @@ -#[derive(Debug, Deserialize, Hash, PartialEq, Eq, Clone)] +#[derive(Debug, Hash, PartialEq, Eq, Clone, Serialize)] +/// This object represents a portion of the price for goods or services. pub struct LabeledPrice { + /// Portion label pub label: String, + /// Price of the product in the smallest units of the + /// [currency](https://core.telegram.org/bots/payments#supported-currencies) + /// (integer, not float/double). For example, for a price of US$ 1.45 pass + /// amount = 145. See the exp parameter in [`currencies.json`](https://core.telegram.org/bots/payments/currencies.json), + /// it shows the number of digits past the decimal point for each currency + /// (2 for the majority of currencies). pub amount: i64, } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn serialize() { + let labeled_price = LabeledPrice { + label: "Label".to_string(), + amount: 60 + }; + let expected = r#"{"label":"Label","amount":60}"#; + let actual = serde_json::to_string(&labeled_price).unwrap(); + assert_eq!(actual, expected); + } +} diff --git a/src/core/types/mod.rs b/src/core/types/mod.rs index 7e6d7e08..3c1cca89 100644 --- a/src/core/types/mod.rs +++ b/src/core/types/mod.rs @@ -1,7 +1,5 @@ use self::not_implemented_types::*; pub use self::{ - answer_pre_checkout_query::AnswerPreCheckoutQuery, - answer_shipping_query::AnswerShippingQuery, animation::Animation, audio::Audio, callback_query::CallbackQuery, @@ -51,8 +49,6 @@ pub use self::{ }; mod animation; -mod answer_pre_checkout_query; -mod answer_shipping_query; mod audio; mod callback_query; mod chat; diff --git a/src/core/types/photo_size.rs b/src/core/types/photo_size.rs index 9f8d6cf3..8df34596 100644 --- a/src/core/types/photo_size.rs +++ b/src/core/types/photo_size.rs @@ -1,7 +1,32 @@ #[derive(Debug, Deserialize, Eq, Hash, PartialEq, Serialize, Clone)] +/// This object represents one size of a photo or a [`Document`] / +/// [`Sticker`] thumbnail. pub struct PhotoSize { + /// Identifier for this file pub file_id: String, + /// Photo width pub width: i32, + /// Photo height pub height: i32, + /// Optional. File size pub file_size: Option, } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn deserialize() { + let json = r#"{"file_id":"id","width":320,"height":320, + "file_size":3452}"#; + let expected = PhotoSize { + file_id: "id".to_string(), + width: 320, + height: 320, + file_size: Some(3452) + }; + let actual = serde_json::from_str::(json).unwrap(); + assert_eq!(actual, expected); + } +} diff --git a/src/core/types/send_invoice.rs b/src/core/types/send_invoice.rs index 87c26524..07698819 100644 --- a/src/core/types/send_invoice.rs +++ b/src/core/types/send_invoice.rs @@ -1,6 +1,6 @@ use crate::core::types::{InlineKeyboardMarkup, LabeledPrice}; -#[derive(Debug, Deserialize, Hash, PartialEq, Eq, Clone)] +#[derive(Debug, Hash, PartialEq, Eq, Clone)] pub struct SendInvoice { pub chat_id: i64, pub title: String, diff --git a/src/core/types/shipping_option.rs b/src/core/types/shipping_option.rs index 4dc1835c..9a6e4e8e 100644 --- a/src/core/types/shipping_option.rs +++ b/src/core/types/shipping_option.rs @@ -1,8 +1,31 @@ use crate::core::types::LabeledPrice; -#[derive(Debug, Deserialize, Hash, PartialEq, Eq, Clone)] +#[derive(Debug, Hash, PartialEq, Eq, Clone, Serialize)] +/// This object represents one shipping option. pub struct ShippingOption { - pub id: i64, + /// Shipping option identifier + pub id: String, + /// Option title pub title: String, + /// List of price portions pub prices: Vec, } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn serialize() { + let shipping_option = ShippingOption { + id: "0".to_string(), + title: "Option".to_string(), + prices: vec![ + LabeledPrice { label: "Label".to_string(), amount: 60 } + ] + }; + let expected = r#"{"id":"0","title":"Option","prices":[{"label":"Label","amount":60}]}"#; + let actual = serde_json::to_string(&shipping_option).unwrap(); + assert_eq!(actual, expected); + } +} diff --git a/src/keyboards/inline_keyboard_button.rs b/src/keyboards/inline_keyboard_button.rs deleted file mode 100644 index 9cc43188..00000000 --- a/src/keyboards/inline_keyboard_button.rs +++ /dev/null @@ -1,55 +0,0 @@ -use crate::core::types::{InlineKeyboardButton, InlineKeyboardButtonKind}; - -pub struct InlineKeyboardButtonBuilder; - -/// Build buttons -/// -/// Example: -/// ```edition2018 -/// use async_telegram_bot::keyboards::InlineKeyboardButtonBuilder; -/// -/// fn main() { -/// let url_button = InlineKeyboardButtonBuilder::url( -/// "Text".to_string(), -/// "http://url.com".to_string(), -/// ); -/// } -/// ``` -impl InlineKeyboardButtonBuilder { - pub fn url(text: String, url: String) -> InlineKeyboardButton { - InlineKeyboardButton { - text, - kind: InlineKeyboardButtonKind::Url(url), - } - } - - pub fn callback(text: String, callback_data: String) - -> InlineKeyboardButton { - InlineKeyboardButton { - text, - kind: InlineKeyboardButtonKind::CallbackData(callback_data), - } - } - - pub fn switch_inline_query(text: String, switch_inline_query: String) - -> InlineKeyboardButton { - InlineKeyboardButton { - text, - kind: InlineKeyboardButtonKind::SwitchInlineQuery(switch_inline_query) - } - } - - pub fn switch_inline_query_current_chat( - text: String, - switch_inline_query_current_chat: String - ) -> InlineKeyboardButton { - - InlineKeyboardButton { - text, - kind: InlineKeyboardButtonKind::SwitchInlineQueryCurrentChat( - switch_inline_query_current_chat - ) - } - } -} - diff --git a/src/keyboards/inline_keyboard_markup.rs b/src/keyboards/inline_keyboard_markup.rs deleted file mode 100644 index e0ff0663..00000000 --- a/src/keyboards/inline_keyboard_markup.rs +++ /dev/null @@ -1,70 +0,0 @@ -use crate::core::types::{InlineKeyboardMarkup, InlineKeyboardButton}; - -pub struct InlineKeyboardMarkupBuilder { - keyboard: InlineKeyboardMarkup, -} - -/// Builder for [`InlineKeyboardMarkup`] -/// -/// Example: -/// ```edition2018 -/// use async_telegram_bot::keyboards; -/// -/// fn main() { -/// let url_button = keyboards::InlineKeyboardButtonBuilder::url( -/// "text".to_string(), -/// "http://url.com".to_string() -/// ); -/// let keyboard = keyboards::InlineKeyboardMarkupBuilder::new() -/// .row(vec![url_button]) -/// .build(); -/// } -/// ``` -impl InlineKeyboardMarkupBuilder { - pub fn new() -> Self { - Self { - keyboard: InlineKeyboardMarkup { - inline_keyboard: vec![] - } - } - } - - pub fn row(mut self, buttons: Vec) -> Self { - self.keyboard.inline_keyboard.push(buttons); - self - } - - pub fn append_to_row(mut self, button: InlineKeyboardButton, index: usize) - -> Self { - match self.keyboard.inline_keyboard.get_mut(index) { - Some(buttons) => buttons.push(button), - None => self.keyboard.inline_keyboard.push(vec![button]) - }; - self - } - - pub fn build(self) -> InlineKeyboardMarkup { - self.keyboard - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::keyboards::InlineKeyboardButtonBuilder; - - #[test] - fn test_row() { - let btn = InlineKeyboardButtonBuilder::url( - "text".to_string(), - "http://url".to_string(), - ); - let kb = InlineKeyboardMarkupBuilder::new() - .row(vec![btn.clone()]) - .build(); - let expected = InlineKeyboardMarkup { - inline_keyboard: vec![vec![btn.clone()]], - }; - assert_eq!(kb, expected); - } -} diff --git a/src/keyboards/mod.rs b/src/keyboards/mod.rs deleted file mode 100644 index 63fac65a..00000000 --- a/src/keyboards/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -mod inline_keyboard_button; -mod inline_keyboard_markup; - -pub use self::{ - inline_keyboard_button::InlineKeyboardButtonBuilder, - inline_keyboard_markup::InlineKeyboardMarkupBuilder, -}; diff --git a/src/lib.rs b/src/lib.rs index 081b06a5..4f8dd9a7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,5 +5,4 @@ extern crate serde; pub mod bot; pub mod core; -pub mod keyboards; pub mod dispatcher;