diff --git a/Cargo.toml b/Cargo.toml index f44dab04..ae9588a7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,3 +16,4 @@ tokio = "0.2.0-alpha.4" bytes = "0.4.12" futures-preview = "0.3.0-alpha.18" async-trait = "0.1.13" +libc = "0.2.62" diff --git a/src/bot/api.rs b/src/bot/api.rs index 37efa01f..d10d83b1 100644 --- a/src/bot/api.rs +++ b/src/bot/api.rs @@ -1,12 +1,9 @@ use crate::{ bot::Bot, requests::{ - edit_message_live_location::EditMessageLiveLocation, - forward_message::ForwardMessage, get_file::GetFile, get_me::GetMe, - send_audio::SendAudio, send_location::SendLocation, - send_media_group::SendMediaGroup, send_message::SendMessage, - send_photo::SendPhoto, - stop_message_live_location::StopMessageLiveLocation, ChatId, + ChatId, EditMessageLiveLocation, ForwardMessage, GetFile, GetMe, + SendAudio, SendLocation, SendMediaGroup, SendMessage, SendPhoto, + StopMessageLiveLocation, }, types::{InputFile, InputMedia}, }; diff --git a/src/lib.rs b/src/lib.rs index d35d2e98..34962237 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,12 +1,14 @@ +#![feature(termination_trait_lib)] + #[macro_use] extern crate derive_more; #[macro_use] extern crate serde; mod network; +mod errors; pub mod bot; -pub mod errors; pub mod requests; pub mod types; diff --git a/src/network/download.rs b/src/network/download.rs index a57d3fff..96bc646e 100644 --- a/src/network/download.rs +++ b/src/network/download.rs @@ -1,10 +1,10 @@ +use bytes::Buf; use futures::StreamExt; use reqwest::r#async::{Chunk, Client}; use tokio::{ io::{AsyncWrite, AsyncWriteExt}, stream::Stream, }; -use bytes::Buf; use crate::{ network::{file_url, TELEGRAM_API_URL}, @@ -23,7 +23,8 @@ where let mut stream = download_file_stream(client, token, path).await?; while let Some(chunk) = stream.next().await { - destination.write_all(chunk?.bytes()).await?; + let chunk = chunk?; + destination.write_all(chunk.bytes()).await?; } Ok(()) diff --git a/src/network/request.rs b/src/network/request.rs index 0faf04c0..b068a5a0 100644 --- a/src/network/request.rs +++ b/src/network/request.rs @@ -3,7 +3,7 @@ use reqwest::r#async::{multipart::Form, Client, Response}; use serde::{de::DeserializeOwned, Serialize}; use crate::{ - network::{method_url, TELEGRAM_API_URL, TelegramResponse}, + network::{method_url, TelegramResponse, TELEGRAM_API_URL}, requests::ResponseResult, RequestError, }; diff --git a/src/requests/answer_pre_checkout_query.rs b/src/requests/answer_pre_checkout_query.rs index f345bd9f..e203bf5b 100644 --- a/src/requests/answer_pre_checkout_query.rs +++ b/src/requests/answer_pre_checkout_query.rs @@ -3,6 +3,7 @@ use async_trait::async_trait; use crate::{ network, requests::{Request, RequestContext, ResponseResult}, + types::True }; #[derive(Debug, Serialize, Clone)] @@ -35,7 +36,7 @@ pub struct AnswerPreCheckoutQuery<'a> { #[async_trait] impl Request for AnswerPreCheckoutQuery<'_> { - type ReturnValue = bool; + type ReturnValue = True; async fn send_boxed(self) -> ResponseResult { self.send().await @@ -43,7 +44,7 @@ impl Request for AnswerPreCheckoutQuery<'_> { } impl AnswerPreCheckoutQuery<'_> { - pub async fn send(self) -> ResponseResult { + pub async fn send(self) -> ResponseResult { network::request_json( &self.ctx.client, &self.ctx.token, diff --git a/src/requests/answer_shipping_query.rs b/src/requests/answer_shipping_query.rs index 6a7e65f8..11898d06 100644 --- a/src/requests/answer_shipping_query.rs +++ b/src/requests/answer_shipping_query.rs @@ -3,7 +3,7 @@ use async_trait::async_trait; use crate::{ network, requests::{Request, RequestContext, ResponseResult}, - types::ShippingOption, + types::{ShippingOption, True}, }; #[derive(Debug, Clone, Serialize)] @@ -37,7 +37,7 @@ pub struct AnswerShippingQuery<'a> { #[async_trait] impl Request for AnswerShippingQuery<'_> { - type ReturnValue = bool; + type ReturnValue = True; async fn send_boxed(self) -> ResponseResult { self.send().await diff --git a/src/requests/get_user_profile_photos.rs b/src/requests/get_user_profile_photos.rs index 7123151b..722b919a 100644 --- a/src/requests/get_user_profile_photos.rs +++ b/src/requests/get_user_profile_photos.rs @@ -1,19 +1,73 @@ -use crate::requests::RequestContext; +use crate::network; +use crate::requests::{Request, RequestContext, RequestFuture, ResponseResult}; +use crate::types::UserProfilePhotos; -//TODO: complete implementation after user_profile_fotos will be added to -// types/mod.rs ///Use this method to get a list of profile pictures for a user. Returns a /// UserProfilePhotos object. #[derive(Debug, Clone, Serialize)] -struct GetUserProfilePhotos<'a> { +pub struct GetUserProfilePhotos<'a> { #[serde(skip_serializing)] ctx: RequestContext<'a>, /// Unique identifier of the target user - user_id: i32, + pub user_id: i32, /// Sequential number of the first photo to be returned. By default, all /// photos are returned. - offset: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub offset: Option, ///Limits the number of photos to be retrieved. Values between 1—100 are /// accepted. Defaults to 100. - limit: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub limit: Option, +} + +impl<'a> Request<'a> for GetUserProfilePhotos<'a> { + type ReturnValue = UserProfilePhotos; + + fn send(self) -> RequestFuture<'a, ResponseResult> { + Box::pin(async move { + network::request_json( + &self.ctx.client, + &self.ctx.token, + "getUserProfilePhotos", + &self, + ) + .await + }) + } +} + +impl<'a> GetUserProfilePhotos<'a> { + pub fn new(ctx: RequestContext<'a>, user_id: i32) -> Self { + Self { + ctx, + user_id, + offset: None, + limit: None, + } + } + + pub fn user_id(mut self, user_id: T) -> Self + where + T: Into, + { + self.user_id = user_id.into(); + self + } + + pub fn offset(mut self, offset: T) -> Self + where + T: Into, + { + self.offset = Some(offset.into()); + self + } + + pub fn limit(mut self, limit: T) -> Self + where + T: Into, + { + self.limit = Some(limit.into()); + self + } + } diff --git a/src/requests/kick_chat_member.rs b/src/requests/kick_chat_member.rs index b33616ce..ae417f90 100644 --- a/src/requests/kick_chat_member.rs +++ b/src/requests/kick_chat_member.rs @@ -1,12 +1,72 @@ -use crate::requests::RequestContext; -//TODO:: need implementation +use crate::network; +use crate::requests::{ + ChatId, Request, RequestContext, RequestFuture, ResponseResult, +}; +use crate::types::True; + /// Use this method to kick a user from a group, a supergroup or a channel. In /// the case of supergroups and channels, the user will not be able to return to /// the group on their own using invite links, etc., unless unbanned first. The /// bot must be an administrator in the chat for this to work and must have the /// appropriate admin rights. Returns True on success. #[derive(Debug, Clone, Serialize)] -struct KickChatMember<'a> { +pub struct KickChatMember<'a> { #[serde(skip_serializing)] ctx: RequestContext<'a>, + ///Unique identifier for the target group or username of the target + /// supergroup or channel (in the format @channelusername) + pub chat_id: ChatId, + /// Unique identifier of the target user + pub user_id: i32, + ///Date when the user will be unbanned, unix time. If user is banned for + /// more than 366 days or less than 30 seconds from the current time they + /// are considered to be banned forever + #[serde(skip_serializing_if = "Option::is_none")] + pub until_date: Option, +} + +impl<'a> Request<'a> for KickChatMember<'a> { + type ReturnValue = True; + + fn send(self) -> RequestFuture<'a, ResponseResult> { + Box::pin(async move { + network::request_json( + self.ctx.client, + self.ctx.token, + "kickChatMember", + &self, + ) + .await + }) + } +} + +impl<'a> KickChatMember<'a> { + pub(crate) fn new( + ctx: RequestContext<'a>, + chat_id: ChatId, + user_id: i32, + ) -> Self { + Self { + ctx, + chat_id, + user_id, + until_date: None, + } + } + + pub fn chat_id>(mut self, chat_id: T) -> Self { + self.chat_id = chat_id.into(); + self + } + + pub fn user_id>(mut self, user_id: T) -> Self { + self.user_id = user_id.into(); + self + } + + pub fn until_date>(mut self, until_date: T) -> Self { + self.until_date = Some(until_date.into()); + self + } } diff --git a/src/requests/mod.rs b/src/requests/mod.rs index 5d31f915..67361d23 100644 --- a/src/requests/mod.rs +++ b/src/requests/mod.rs @@ -7,6 +7,23 @@ use serde::de::DeserializeOwned; use crate::RequestError; +pub use self::{ + answer_pre_checkout_query::AnswerPreCheckoutQuery, + answer_shipping_query::AnswerShippingQuery, + edit_message_live_location::EditMessageLiveLocation, + forward_message::ForwardMessage, get_chat::GetChat, get_file::GetFile, + get_me::GetMe, get_updates::GetUpdates, + get_user_profile_photos::GetUserProfilePhotos, + kick_chat_member::KickChatMember, pin_chat_message::PinChatMessage, + restrict_chat_member::RestrictChatMember, + send_audio::SendAudio, send_chat_action::SendChatAction, + send_contact::SendContact, send_location::SendLocation, + send_media_group::SendMediaGroup, send_message::SendMessage, + send_photo::SendPhoto, send_poll::SendPoll, send_venue::SendVenue, + stop_message_live_location::StopMessageLiveLocation, + unban_chat_member::UnbanChatMember, +}; + pub type ResponseResult = Result; /// Request that can be sent to telegram. @@ -62,25 +79,26 @@ 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_chat; -pub mod get_file; -pub mod get_me; -pub mod get_updates; -pub mod get_user_profile_photos; -pub mod kick_chat_member; -pub mod restrict_chat_member; -pub mod send_audio; -pub mod send_chat_action; -pub mod send_contact; -pub mod send_location; -pub mod send_media_group; -pub mod send_message; -pub mod send_photo; -pub mod send_poll; -pub mod send_venue; -pub mod stop_message_live_location; -pub mod unban_chat_member; +mod answer_pre_checkout_query; +mod answer_shipping_query; +mod edit_message_live_location; +mod forward_message; +mod get_chat; +mod get_file; +mod get_me; +mod get_updates; +mod get_user_profile_photos; +mod kick_chat_member; +mod pin_chat_message; +mod restrict_chat_member; +mod send_audio; +mod send_chat_action; +mod send_contact; +mod send_location; +mod send_media_group; +mod send_message; +mod send_photo; +mod send_poll; +mod send_venue; +mod stop_message_live_location; +mod unban_chat_member; diff --git a/src/requests/pin_chat_message.rs b/src/requests/pin_chat_message.rs new file mode 100644 index 00000000..d6b9f8ef --- /dev/null +++ b/src/requests/pin_chat_message.rs @@ -0,0 +1,50 @@ +use crate::{ + requests::{ChatId, RequestContext, RequestFuture, ResponseResult, Request}, + types::True +}; +use crate::network; + +/// Use this method to get up to date information about the chat +/// (current name of the user for one-on-one conversations, +/// current username of a user, group or channel, etc.). +/// Returns a Chat object on success. +#[derive(Debug, Clone, Serialize)] +pub struct PinChatMessage<'a> { + #[serde(skip_serializing)] + ctx: RequestContext<'a>, + /// Unique identifier for the target chat or username + /// of the target supergroup or channel (in the format @channelusername) + pub chat_id: ChatId, + pub message_id: i32, + pub disable_notification: Option +} + +impl<'a> PinChatMessage<'a> { + pub(crate) fn new( + ctx: RequestContext<'a>, chat_id: ChatId, message_id: i32 + ) -> Self { + Self { ctx, chat_id, message_id, disable_notification: None } + } + + pub fn disable_notification(mut self, val: T) -> Self + where T: Into + { + self.disable_notification = Some(val.into()); + self + } +} + +impl<'a> Request<'a> for PinChatMessage<'a> { + type ReturnValue = True; + + fn send(self) -> RequestFuture<'a, ResponseResult> { + Box::pin(async move { + network::request_json( + &self.ctx.client, + &self.ctx.token, + "pinChatMessage", + &self, + ).await + }) + } +} diff --git a/src/requests/restrict_chat_member.rs b/src/requests/restrict_chat_member.rs index 6957922b..697dcdeb 100644 --- a/src/requests/restrict_chat_member.rs +++ b/src/requests/restrict_chat_member.rs @@ -1,8 +1,92 @@ -use crate::requests::RequestContext; -//TODO:: need implementation +use crate::network; +use crate::requests::{ + ChatId, Request, RequestContext, RequestFuture, ResponseResult, +}; +use crate::types::{ChatPermissions, True}; +/// 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 +/// appropriate admin rights. Pass True for all permissions to lift restrictions +/// from a user. Returns True on success. #[derive(Debug, Clone, Serialize)] -struct RestrictChatMember<'a> { +pub struct RestrictChatMember<'a> { #[serde(skip_serializing)] ctx: RequestContext<'a>, + ///Unique identifier for the target chat or username of the target + /// supergroup (in the format @supergroupusername) + pub chat_id: ChatId, + ///Unique identifier of the target user + pub user_id: i32, + ///New user permissions + pub permissions: ChatPermissions, + ///Date when restrictions will be lifted for the user, unix time. If user + /// is restricted for more than 366 days or less than 30 seconds from the + /// current time, they are considered to be restricted forever + #[serde(skip_serializing_if = "Option::is_none")] + pub until_date: Option, +} + +impl<'a> Request<'a> for RestrictChatMember<'a> { + type ReturnValue = True; + + fn send(self) -> RequestFuture<'a, ResponseResult> { + Box::pin(async move { + network::request_json( + &self.ctx.client, + &self.ctx.token, + "restrictChatMember", + &self, + ) + .await + }) + } +} + +impl<'a> RestrictChatMember<'a> { + pub(crate) fn new( + ctx: RequestContext<'a>, + chat_id: ChatId, + user_id: i32, + permissions: ChatPermissions, + ) -> Self { + Self { + ctx, + chat_id, + user_id, + permissions, + until_date: None, + } + } + + pub fn chat_id(mut self, chat_id: T) -> Self + where + T: Into, + { + self.chat_id = chat_id.into(); + self + } + + pub fn user_id(mut self, user_id: T) -> Self + where + T: Into, + { + self.user_id = user_id.into(); + self + } + + pub fn permissions(mut self, permissions: T) -> Self + where + T: Into, + { + self.permissions = permissions.into(); + self + } + + pub fn until_date(mut self, until_date: T) -> Self + where + T: Into, + { + self.until_date = Some(until_date.into()); + self + } } diff --git a/src/requests/send_chat_action.rs b/src/requests/send_chat_action.rs index 6510b620..093db60b 100644 --- a/src/requests/send_chat_action.rs +++ b/src/requests/send_chat_action.rs @@ -3,6 +3,7 @@ use async_trait::async_trait; use crate::{ network, requests::{ChatId, Request, RequestContext, ResponseResult}, + types::True }; ///Use this method when you need to tell the user that something is happening @@ -41,7 +42,7 @@ pub enum ChatAction { #[async_trait] impl Request for SendChatAction<'_> { - type ReturnValue = bool; + type ReturnValue = True; async fn send_boxed(self) -> ResponseResult { self.send().await diff --git a/src/requests/unban_chat_member.rs b/src/requests/unban_chat_member.rs index d7a9b436..ffaace92 100644 --- a/src/requests/unban_chat_member.rs +++ b/src/requests/unban_chat_member.rs @@ -2,7 +2,7 @@ use crate::requests::RequestContext; //TODO:: need implementation #[derive(Debug, Clone, Serialize)] -struct UnbanChatMember<'a> { +pub struct UnbanChatMember<'a> { #[serde(skip_serializing)] ctx: RequestContext<'a>, } diff --git a/src/types/encrypted_credintials.rs b/src/types/encrypted_credintials.rs new file mode 100644 index 00000000..184993cf --- /dev/null +++ b/src/types/encrypted_credintials.rs @@ -0,0 +1,35 @@ +#[derive(Debug, Deserialize, Eq, Hash, PartialEq, Clone, Serialize)] +pub struct EncryptedCredentials { + // TODO: check base64 type + pub data: String, + pub hash: String, + pub secret: String, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn must_serialize_encrypted_credentials_to_json() { + // given + let expected_json = r#" + { + "data":"someData", + "hash":"1122", + "secret":"secret" + }"# + .replace("\n", "") + .replace(" ", ""); + let encrypted_credentials = EncryptedCredentials { + data: "someData".to_string(), + hash: "1122".to_string(), + secret: "secret".to_string(), + }; + // when + let actual_json = + serde_json::to_string(&encrypted_credentials).unwrap(); + //then + assert_eq!(actual_json, expected_json) + } +} diff --git a/src/types/encrypted_passport_element.rs b/src/types/encrypted_passport_element.rs new file mode 100644 index 00000000..61b1fa47 --- /dev/null +++ b/src/types/encrypted_passport_element.rs @@ -0,0 +1,67 @@ +use super::passport_file::PassportFile; + +#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] +pub struct EncryptedPassportElement { + pub hash: String, + #[serde(flatten)] + pub kind: EncryptedPassportElementKind +} + +#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum EncryptedPassportElementKind { + PersonalDetails { + data: String + }, + Passport { + data: String, + front_side: PassportFile, + selfie: PassportFile, + translation: Option> + }, + DriverLicense { + data: String, + front_side: PassportFile, + reverse_side: PassportFile, + selfie: PassportFile, + translation: Option> + }, + IdentityCard { + data: String, + front_side: PassportFile, + reverse_side: PassportFile, + selfie: PassportFile, + translation: Option> + }, + InternalPassport { + data: String, + front_side: PassportFile, + selfie: PassportFile, + translation: Option> + }, + Address { + data: String + }, + UtilityBill { + files: Vec, + translation: Option> + }, + BankStatement { + files: Vec, + translation: Option> + }, + RentalAgreement { + files: Vec, + translation: Option> + }, + PassportRegistration { + files: Vec, + translation: Option> + }, + TemporaryRegistration { + files: Vec, + translation: Option> + }, + PhoneNumber { phone_number: String }, + Email { email: String } +} diff --git a/src/types/message.rs b/src/types/message.rs index 905d79d4..2bc8a102 100644 --- a/src/types/message.rs +++ b/src/types/message.rs @@ -15,7 +15,7 @@ pub struct Message { } impl Message { - fn text(&self) -> Option<&str> { + pub fn text(&self) -> Option<&str> { if let MessageKind::Common { media_kind: MediaKind::Text { ref text, .. }, .. diff --git a/src/types/mod.rs b/src/types/mod.rs index 1533e3bb..041001cd 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -11,6 +11,8 @@ pub use self::{ chosen_inline_result::ChosenInlineResult, contact::Contact, document::Document, + encrypted_credintials::EncryptedCredentials, + encrypted_passport_element::{EncryptedPassportElement, EncryptedPassportElementKind}, file::File, force_reply::ForceReply, game::Game, @@ -54,6 +56,8 @@ pub use self::{ message_entity::MessageEntity, order_info::OrderInfo, parse_mode::ParseMode, + passport_data::PassportData, + passport_file::PassportFile, photo_size::PhotoSize, poll::{Poll, PollOption}, pre_checkout_query::PreCheckoutQuery, @@ -68,6 +72,7 @@ pub use self::{ sticker::Sticker, sticker_set::StickerSet, successful_payment::SuccessfulPayment, + unit_true::True, update::{Update, UpdateKind}, user::User, user_profile_photos::UserProfilePhotos, @@ -123,6 +128,7 @@ mod shipping_query; mod sticker; mod sticker_set; mod successful_payment; +mod unit_true; mod update; mod user; mod user_profile_photos; @@ -154,3 +160,8 @@ mod inline_query_result_photo; mod inline_query_result_venue; mod inline_query_result_video; mod inline_query_result_voice; + +mod encrypted_credintials; +mod encrypted_passport_element; +mod passport_data; +mod passport_file; diff --git a/src/types/passport_data.rs b/src/types/passport_data.rs new file mode 100644 index 00000000..8ca8e20c --- /dev/null +++ b/src/types/passport_data.rs @@ -0,0 +1,8 @@ +use super::encrypted_credintials::EncryptedCredentials; +use super::encrypted_passport_element::EncryptedPassportElement; + +#[derive(Debug, Deserialize, Eq, Hash, PartialEq, Clone, Serialize)] +pub struct PassportData { + pub data: Vec, + pub credentials: EncryptedCredentials, +} diff --git a/src/types/passport_file.rs b/src/types/passport_file.rs new file mode 100644 index 00000000..1aee85a7 --- /dev/null +++ b/src/types/passport_file.rs @@ -0,0 +1,6 @@ +#[derive(Debug, Deserialize, Eq, Hash, PartialEq, Clone, Serialize)] +pub struct PassportFile { + pub file_id: String, + pub file_size: u64, + pub file_date: u64, +} diff --git a/src/types/unit_true.rs b/src/types/unit_true.rs new file mode 100644 index 00000000..85db581a --- /dev/null +++ b/src/types/unit_true.rs @@ -0,0 +1,81 @@ +use serde::de::{self, Deserialize, Deserializer, Visitor}; +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 { + match value { + true => Ok(True), + false => Err(()) + } + } +} + +impl<'de> Deserialize<'de> for True { + fn deserialize(des: D) -> Result + where + D: Deserializer<'de> + { + des.deserialize_bool(TrueVisitor) + } +} + +struct TrueVisitor; + +impl<'de> Visitor<'de> for TrueVisitor { + type Value = True; + + fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "bool, equal to `true`") + } + + fn visit_bool(self, value: bool) -> Result + where + E: de::Error + { + match value { + true => Ok(True), + false => Err(E::custom("expected `true`, found `false`")) + } + } +} + +impl Serialize for True { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_bool(true) + } +} + +#[cfg(test)] +mod tests { + use super::True; + use serde_json::{from_str, to_string}; + + #[test] + fn unit_true_de() { + let json = "true"; + let expected = True; + let actual = from_str(json).unwrap(); + assert_eq!(expected, actual); + } + + #[test] + fn unit_true_se() { + let actual = to_string(&True).unwrap(); + let expected = "true"; + assert_eq!(expected, actual); + } +}