Merge branch 'dev' of github.com:async-telegram-bot/async-telegram-bot into dev

This commit is contained in:
Waffle 2019-09-12 21:29:04 +03:00
commit 17d8ef43a0
42 changed files with 606 additions and 491 deletions

View file

@ -4,16 +4,12 @@ use crate::core::{
};
use apply::Apply;
use serde_json::Value;
use serde::{
Serialize,
de::DeserializeOwned
};
use reqwest::{
r#async::{multipart::Form, Client},
StatusCode,
r#async::{Client, multipart::Form},
};
use serde::{de::DeserializeOwned, Serialize};
use serde_json::Value;
const TELEGRAM_API_URL: &str = "https://api.telegram.org";
@ -58,10 +54,7 @@ pub async fn request_multipart<T: DeserializeOwned>(
.map_err(RequestError::NetworkError)?;
let response = serde_json::from_str::<TelegramResponse<T>>(
&response
.text()
.await
.map_err(RequestError::NetworkError)?,
&response.text().await.map_err(RequestError::NetworkError)?,
)
.map_err(RequestError::InvalidJson)?;
@ -72,7 +65,10 @@ pub async fn request_multipart<T: DeserializeOwned>(
error_code,
response_parameters,
..
} => Err(RequestError::ApiError { description, status_code: StatusCode::from_u16(error_code).unwrap() })
} => Err(RequestError::ApiError {
description,
status_code: StatusCode::from_u16(error_code).unwrap(),
}),
}
}
@ -90,10 +86,7 @@ pub async fn request_json<T: DeserializeOwned, P: Serialize>(
.map_err(RequestError::NetworkError)?;
let response = serde_json::from_str::<TelegramResponse<T>>(
&response
.text()
.await
.map_err(RequestError::NetworkError)?,
&response.text().await.map_err(RequestError::NetworkError)?,
)
.map_err(RequestError::InvalidJson)?;
@ -104,7 +97,10 @@ pub async fn request_json<T: DeserializeOwned, P: Serialize>(
error_code,
response_parameters,
..
} => Err(RequestError::ApiError { description, status_code: StatusCode::from_u16(error_code).unwrap() })
} => Err(RequestError::ApiError {
description,
status_code: StatusCode::from_u16(error_code).unwrap(),
}),
}
}

View file

@ -1,7 +1,9 @@
use serde::Serialize;
use crate::core::requests::{RequestContext, ChatId, Request, RequestFuture, ResponseResult};
use crate::core::types::{Message, ReplyMarkup};
use crate::core::network;
use crate::core::requests::{
ChatId, Request, RequestContext, RequestFuture, ResponseResult,
};
use crate::core::types::{Message, ReplyMarkup};
use serde::Serialize;
#[derive(Debug, Clone, Serialize)]
/// Use this method to edit live location messages. A location can be edited
@ -13,16 +15,16 @@ pub struct EditMessageLiveLocation<'a> {
#[serde(skip_serializing)]
ctx: RequestContext<'a>,
#[serde(skip_serializing_if="Option::is_none")]
#[serde(skip_serializing_if = "Option::is_none")]
/// Required if inline_message_id is not specified. Unique identifier for
/// the target chat or username of the target channel (in the format
/// @channelusername)
chat_id: Option<ChatId>,
#[serde(skip_serializing_if="Option::is_none")]
#[serde(skip_serializing_if = "Option::is_none")]
/// Required if inline_message_id is not specified. Identifier of the
/// message to edit
message_id: Option<i64>,
#[serde(skip_serializing_if="Option::is_none")]
#[serde(skip_serializing_if = "Option::is_none")]
/// Required if chat_id and message_id are not specified. Identifier of
/// the inline message
inline_message_id: Option<String>,
@ -30,9 +32,9 @@ pub struct EditMessageLiveLocation<'a> {
latitude: f64,
/// Longitude of new location
longitude: f64,
#[serde(skip_serializing_if="Option::is_none")]
#[serde(skip_serializing_if = "Option::is_none")]
/// A JSON-serialized object for a new inline keyboard.
reply_markup: Option<ReplyMarkup>
reply_markup: Option<ReplyMarkup>,
}
impl<'a> Request<'a> for EditMessageLiveLocation<'a> {
@ -44,14 +46,15 @@ impl<'a> Request<'a> for EditMessageLiveLocation<'a> {
&self.ctx.client,
&self.ctx.token,
"editMessageLiveLocation",
&self
).await
&self,
)
.await
})
}
}
impl<'a> EditMessageLiveLocation<'a> {
pub(crate) fn new (
pub(crate) fn new(
ctx: RequestContext<'a>,
latitude: f64,
longitude: f64,
@ -63,7 +66,7 @@ impl<'a> EditMessageLiveLocation<'a> {
inline_message_id: None,
latitude,
longitude,
reply_markup: None
reply_markup: None,
}
}
@ -78,7 +81,8 @@ impl<'a> EditMessageLiveLocation<'a> {
}
pub fn inline_message_id<T>(mut self, inline_message_id: T) -> Self
where T: Into<String>
where
T: Into<String>,
{
self.inline_message_id = Some(inline_message_id.into());
self

View file

@ -1,14 +1,13 @@
use std::path::PathBuf;
use crate::core::{
types::{ParseMode, InputMedia},
requests::{ChatId, utils},
requests::{utils, ChatId},
types::{InputMedia, ParseMode},
};
use reqwest::r#async::multipart::Form;
use serde::Serialize;
/// This is a convenient struct that builds `reqwest::r#async::multipart::Form`
/// from scratch.
pub struct FormBuilder {
@ -22,20 +21,19 @@ impl FormBuilder {
/// Add the supplied key-value pair to this `FormBuilder`.
pub fn add<T>(self, name: &str, value: &T) -> Self
where T: ToFormValue + ?Sized
where
T: ToFormValue + ?Sized,
{
Self {
form: self.form.text(
name.to_owned(),
value.to_form_value()
)
form: self.form.text(name.to_owned(), value.to_form_value()),
}
}
/// Adds a key-value pair to the supplied `FormBuilder` if `value` is some.
/// Don't forget to implement `serde::Serialize` for `T`!
pub fn add_if_some<T>(self, name: &str, value: Option<&T>) -> Self
where T: ToFormValue + ?Sized
where
T: ToFormValue + ?Sized,
{
match value {
None => Self { form: self.form },
@ -45,7 +43,9 @@ impl FormBuilder {
pub fn add_file(self, name: &str, path_to_file: &PathBuf) -> Self {
Self {
form: self.form.part(name.to_owned(), utils::file_to_part(path_to_file))
form: self
.form
.part(name.to_owned(), utils::file_to_part(path_to_file)),
}
}
@ -70,9 +70,7 @@ macro_rules! impl_for_struct {
};
}
impl_for_struct!(
bool, i32, i64, Vec<InputMedia>
);
impl_for_struct!(bool, i32, i64, Vec<InputMedia>);
impl ToFormValue for str {
fn to_form_value(&self) -> String {

View file

@ -1,17 +1,12 @@
use crate::core::{
network,
types::Message,
requests::{
ChatId,
Request,
RequestFuture,
RequestContext,
ResponseResult,
form_builder::FormBuilder,
form_builder::FormBuilder, ChatId, Request, RequestContext,
RequestFuture, ResponseResult,
},
types::Message,
};
#[derive(Debug, Clone, Serialize)]
/// Use this method to forward messages of any kind. On success, the sent
/// [`Message`] is returned.
@ -28,8 +23,9 @@ pub struct ForwardMessage<'a> {
/// Message identifier in the chat specified in from_chat_id
pub message_id: i64,
/// Sends the message silently. Users will receive a notification with no sound.
#[serde(skip_serializing_if="Option::is_none")]
/// Sends the message silently. Users will receive a notification with no
/// sound.
#[serde(skip_serializing_if = "Option::is_none")]
pub disable_notification: Option<bool>,
}
@ -43,22 +39,25 @@ impl<'a> Request<'a> for ForwardMessage<'a> {
self.ctx.token,
"forwardMessage",
&self,
).await
)
.await
})
}
}
impl<'a> ForwardMessage<'a> {
pub(crate) fn new(ctx: RequestContext<'a>,
pub(crate) fn new(
ctx: RequestContext<'a>,
chat_id: ChatId,
from_chat_id: ChatId,
message_id: i64) -> Self {
message_id: i64,
) -> Self {
Self {
ctx,
chat_id,
from_chat_id,
message_id,
disable_notification: None
disable_notification: None,
}
}

View file

@ -0,0 +1,8 @@
use crate::core::requests::RequestContext;
//TODO:: need implementation
#[derive(Debug, Clone, Serialize)]
struct GetFile<'a> {
#[serde(skip_serializing)]
ctx: RequestContext<'a>,
}

View file

@ -1,15 +1,9 @@
use crate::core::{
network,
requests::{Request, RequestContext, RequestFuture, ResponseResult},
types::User,
requests::{
Request,
RequestFuture,
RequestContext,
ResponseResult
},
};
#[derive(Debug, Clone)]
/// A simple method for testing your bot's auth token. Requires no parameters.
/// Returns basic information about the bot in form of a [`User`] object.
@ -21,14 +15,12 @@ impl<'a> Request<'a> for GetMe<'a> {
type ReturnValue = User;
fn send(self) -> RequestFuture<'a, ResponseResult<Self::ReturnValue>> {
Box::pin(
network::request_multipart(
Box::pin(network::request_multipart(
self.ctx.client,
self.ctx.token,
"getMe",
None
)
)
None,
))
}
}

View file

@ -0,0 +1,8 @@
use crate::core::requests::RequestContext;
//TODO:: need implementation
#[derive(Debug, Clone, Serialize)]
struct GetUserProfilePhotos<'a> {
#[serde(skip_serializing)]
ctx: RequestContext<'a>,
}

View file

@ -0,0 +1,8 @@
use crate::core::requests::RequestContext;
//TODO:: need implementation
#[derive(Debug, Clone, Serialize)]
struct KickChatMember<'a> {
#[serde(skip_serializing)]
ctx: RequestContext<'a>,
}

View file

@ -1,19 +1,17 @@
use std::pin::Pin;
use std::future::Future;
use std::pin::Pin;
use reqwest::{
r#async::Client, StatusCode
};
use reqwest::{r#async::Client, StatusCode};
use serde::de::DeserializeOwned;
mod form_builder;
mod utils;
#[derive(Debug, Display)]
pub enum RequestError {
#[display(fmt = "Telegram error #{}: {}", status_code, description)]
ApiError { // TODO: add response parameters
ApiError {
// TODO: add response parameters
status_code: StatusCode,
description: String,
},
@ -82,18 +80,30 @@ mod tests {
#[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();
let actual_json = serde_json::to_string(&ChatId::ChannelUsername(
String::from("@username"),
))
.unwrap();
assert_eq!(expected_json, actual_json)
}
}
pub mod get_me;
pub mod send_message;
pub mod forward_message;
pub mod send_photo;
pub mod send_media_group;
pub mod send_audio;
pub mod send_location;
pub mod edit_message_live_location;
pub mod forward_message;
pub mod get_file;
pub mod get_me;
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;

View file

@ -0,0 +1,8 @@
use crate::core::requests::RequestContext;
//TODO:: need implementation
#[derive(Debug, Clone, Serialize)]
struct RestrictChatMember<'a> {
#[serde(skip_serializing)]
ctx: RequestContext<'a>,
}

View file

@ -1,8 +1,10 @@
use crate::core::{
network,
requests::{ChatId, Request, RequestFuture, ResponseResult, RequestContext},
requests::form_builder::FormBuilder,
types::{InputFile, ParseMode, Message, ReplyMarkup},
requests::{
ChatId, Request, RequestContext, RequestFuture, ResponseResult,
},
types::{InputFile, Message, ParseMode, ReplyMarkup},
};
/// Use this method to send audio files, if you want Telegram clients to display
@ -71,11 +73,11 @@ impl<'a> Request<'a> for SendAudio<'a> {
.add_if_some("title", self.title.as_ref())
.add_if_some(
"disable_notification",
self.disable_notification.as_ref()
self.disable_notification.as_ref(),
)
.add_if_some(
"reply_to_message_id",
self.reply_to_message_id.as_ref()
self.reply_to_message_id.as_ref(),
);
params = match self.audio {
InputFile::File(file) => params.add_file("audio", &file),
@ -95,8 +97,9 @@ impl<'a> Request<'a> for SendAudio<'a> {
&self.ctx.client,
&self.ctx.token,
"sendAudio",
Some(params)
).await
Some(params),
)
.await
})
}
}
@ -119,7 +122,7 @@ impl<'a> SendAudio<'a> {
thumb: None,
disable_notification: None,
reply_to_message_id: None,
reply_markup: None
reply_markup: None,
}
}
@ -163,12 +166,18 @@ impl<'a> SendAudio<'a> {
self
}
pub fn disable_notification<T: Into<bool>>(mut self, disable_notification: T) -> Self {
pub fn disable_notification<T: Into<bool>>(
mut self,
disable_notification: T,
) -> Self {
self.disable_notification = Some(disable_notification.into());
self
}
pub fn reply_to_message_id<T: Into<i64>>(mut self, reply_to_message_id: T) -> Self {
pub fn reply_to_message_id<T: Into<i64>>(
mut self,
reply_to_message_id: T,
) -> Self {
self.reply_to_message_id = Some(reply_to_message_id.into());
self
}

View file

@ -0,0 +1,8 @@
use crate::core::requests::RequestContext;
//TODO:: need implementation
#[derive(Debug, Clone, Serialize)]
struct SendChatAction<'a> {
#[serde(skip_serializing)]
ctx: RequestContext<'a>,
}

View file

@ -0,0 +1,7 @@
use crate::core::requests::RequestContext;
//TODO:: need implementation
#[derive(Debug, Clone, Serialize)]
struct SendContact<'a> {
#[serde(skip_serializing)]
ctx: RequestContext<'a>,
}

View file

@ -1,7 +1,9 @@
use crate::core::{
requests::{RequestContext, ChatId, Request, RequestFuture, ResponseResult},
types::{Message, ReplyMarkup},
network,
requests::{
ChatId, Request, RequestContext, RequestFuture, ResponseResult,
},
types::{Message, ReplyMarkup},
};
use serde::Serialize;
@ -20,19 +22,19 @@ pub struct SendLocation<'a> {
latitude: f64,
/// Longitude of the location
longitude: f64,
#[serde(skip_serializing_if="Option::is_none")]
#[serde(skip_serializing_if = "Option::is_none")]
/// Period in seconds for which the location will be updated
/// (see [Live Locations](https://telegram.org/blog/live-locations)),
/// should be between 60 and 86400.
live_period: Option<i32>,
#[serde(skip_serializing_if="Option::is_none")]
#[serde(skip_serializing_if = "Option::is_none")]
/// Sends the message silently. Users will receive a notification with
/// no sound.
disable_notification: Option<bool>,
#[serde(skip_serializing_if="Option::is_none")]
#[serde(skip_serializing_if = "Option::is_none")]
/// If the message is a reply, ID of the original message
reply_to_message_id: Option<i64>,
#[serde(skip_serializing_if="Option::is_none")]
#[serde(skip_serializing_if = "Option::is_none")]
reply_markup: Option<ReplyMarkup>,
}
@ -45,8 +47,9 @@ impl<'a> Request<'a> for SendLocation<'a> {
&self.ctx.client,
&self.ctx.token,
"sendLocation",
&self
).await
&self,
)
.await
})
}
}
@ -66,7 +69,7 @@ impl<'a> SendLocation<'a> {
live_period: None,
disable_notification: None,
reply_to_message_id: None,
reply_markup: None
reply_markup: None,
}
}

View file

@ -1,14 +1,10 @@
use crate::core::{
network::request_multipart,
types::{Message, InputMedia, InputFile},
requests::{
ChatId,
Request,
RequestFuture,
RequestContext,
ResponseResult,
form_builder::FormBuilder,
}
form_builder::FormBuilder, ChatId, Request, RequestContext,
RequestFuture, ResponseResult,
},
types::{InputFile, InputMedia, Message},
};
use apply::Apply;
@ -32,28 +28,40 @@ impl<'a> Request<'a> for SendMediaGroup<'a> {
let params = FormBuilder::new()
.add("chat_id", &self.chat_id)
.apply(|form| {
self.media
.iter()
.map(|e| e.media())
.fold(form, |acc, file| {
self.media.iter().map(|e| e.media()).fold(
form,
|acc, file| {
if let InputFile::File(path) = file {
acc.add_file(
&path
.file_name()
.unwrap()
.to_string_lossy(),
path
path,
)
} else {
acc
}
})
},
)
})
.add("media", &self.media)
.add_if_some("disable_notification", self.disable_notification.as_ref())
.add_if_some("reply_to_message_id", self.reply_to_message_id.as_ref())
.add_if_some(
"disable_notification",
self.disable_notification.as_ref(),
)
.add_if_some(
"reply_to_message_id",
self.reply_to_message_id.as_ref(),
)
.build();
request_multipart(&self.ctx.client, &self.ctx.token, "sendMediaGroup", Some(params)).await
request_multipart(
&self.ctx.client,
&self.ctx.token,
"sendMediaGroup",
Some(params),
)
.await
})
}
}

View file

@ -1,19 +1,15 @@
use crate::core::{
network,
types::{Message, ParseMode, ReplyMarkup},
requests::{
form_builder::FormBuilder,
ChatId,
Request,
RequestFuture,
RequestContext,
ResponseResult,
form_builder::FormBuilder, ChatId, Request, RequestContext,
RequestFuture, ResponseResult,
},
types::{Message, ParseMode, ReplyMarkup},
};
#[derive(Debug, Clone, Serialize)]
/// Use this method to send text messages. On success, the sent [`Message`] is returned.
/// Use this method to send text messages. On success, the sent [`Message`] is
/// returned.
pub struct SendMessage<'a> {
#[serde(skip_serializing)]
ctx: RequestContext<'a>,
@ -32,18 +28,19 @@ pub struct SendMessage<'a> {
/// [Html]: crate::core::types::ParseMode::Html
/// [bold, italic, fixed-width text or inline URLs]:
/// crate::core::types::ParseMode
#[serde(skip_serializing_if="Option::is_none")]
#[serde(skip_serializing_if = "Option::is_none")]
pub parse_mode: Option<ParseMode>,
/// Disables link previews for links in this message
#[serde(skip_serializing_if="Option::is_none")]
#[serde(skip_serializing_if = "Option::is_none")]
pub disable_web_page_preview: Option<bool>,
/// Sends the message silently. Users will receive a notification with no sound.
#[serde(skip_serializing_if="Option::is_none")]
/// Sends the message silently. Users will receive a notification with no
/// sound.
#[serde(skip_serializing_if = "Option::is_none")]
pub disable_notification: Option<bool>,
/// If the message is a reply, ID of the original message
#[serde(skip_serializing_if="Option::is_none")]
#[serde(skip_serializing_if = "Option::is_none")]
pub reply_to_message_id: Option<i64>,
#[serde(skip_serializing_if="Option::is_none")]
#[serde(skip_serializing_if = "Option::is_none")]
pub reply_markup: Option<ReplyMarkup>,
}
@ -57,7 +54,8 @@ impl<'a> Request<'a> for SendMessage<'a> {
self.ctx.token,
"sendMessage",
&self,
).await
)
.await
})
}
}

View file

@ -2,20 +2,16 @@ use std::path::Path;
use crate::core::{
network,
types::{ParseMode, Message, InputFile, ReplyMarkup},
requests::{
ChatId,
Request,
RequestFuture,
RequestContext,
ResponseResult,
form_builder::FormBuilder,
form_builder::FormBuilder, ChatId, Request, RequestContext,
RequestFuture, ResponseResult,
},
types::{InputFile, Message, ParseMode, ReplyMarkup},
};
#[derive(Debug, Clone)]
/// Use this method to send photos. On success, the sent [`Message`] is returned.
/// Use this method to send photos. On success, the sent [`Message`] is
/// returned.
pub struct SendPhoto<'a> {
ctx: RequestContext<'a>,
@ -23,13 +19,14 @@ pub struct SendPhoto<'a> {
/// (in the format @channelusername)
pub chat_id: ChatId,
/// Photo to send.
/// [`InputFile::FileId`] - Pass a file_id as String to send a photo that exists on the
/// Telegram servers (recommended)
/// [`InputFile::FileId`] - Pass a file_id as String to send a photo that
/// exists on the Telegram servers (recommended)
/// [`InputFile::Url`] - Pass an HTTP URL as a String for Telegram
/// to get a photo from the Internet
/// [`InputFile::File`] - Upload a new photo.
pub photo: InputFile,
/// Photo caption (may also be used when resending photos by file_id), 0-1024 characters
/// Photo caption (may also be used when resending photos by file_id),
/// 0-1024 characters
pub caption: Option<String>,
/// Send [Markdown] or [HTML],
/// if you want Telegram apps to show [bold, italic, fixed-width text
@ -40,7 +37,8 @@ pub struct SendPhoto<'a> {
/// [bold, italic, fixed-width text or inline URLs]:
/// crate::core::types::ParseMode
pub parse_mode: Option<ParseMode>,
/// Sends the message silently. Users will receive a notification with no sound.
/// Sends the message silently. Users will receive a notification with no
/// sound.
pub disable_notification: Option<bool>,
/// If the message is a reply, ID of the original message
pub reply_to_message_id: Option<i64>,
@ -58,11 +56,11 @@ impl<'a> Request<'a> for SendPhoto<'a> {
.add_if_some("parse_mode", self.parse_mode.as_ref())
.add_if_some(
"disable_notification",
self.disable_notification.as_ref()
self.disable_notification.as_ref(),
)
.add_if_some(
"reply_to_message_id",
self.reply_to_message_id.as_ref()
self.reply_to_message_id.as_ref(),
);
params = match self.photo {
@ -76,8 +74,9 @@ impl<'a> Request<'a> for SendPhoto<'a> {
&self.ctx.client,
&self.ctx.token,
"sendPhoto",
Some(params)
).await
Some(params),
)
.await
})
}
}
@ -86,7 +85,7 @@ impl<'a> SendPhoto<'a> {
pub(crate) fn new(
ctx: RequestContext<'a>,
chat_id: ChatId,
photo: InputFile
photo: InputFile,
) -> Self {
Self {
ctx,
@ -96,7 +95,7 @@ impl<'a> SendPhoto<'a> {
parse_mode: None,
disable_notification: None,
reply_to_message_id: None,
reply_markup: None
reply_markup: None,
}
}
@ -120,12 +119,18 @@ impl<'a> SendPhoto<'a> {
self
}
pub fn disable_notification<T: Into<bool>>(mut self, disable_notification: T) -> Self {
pub fn disable_notification<T: Into<bool>>(
mut self,
disable_notification: T,
) -> Self {
self.disable_notification = Some(disable_notification.into());
self
}
pub fn reply_to_message_id<T: Into<i64>>(mut self, reply_to_message_id: T) -> Self {
pub fn reply_to_message_id<T: Into<i64>>(
mut self,
reply_to_message_id: T,
) -> Self {
self.reply_to_message_id = Some(reply_to_message_id.into());
self
}

View file

@ -0,0 +1,8 @@
use crate::core::requests::RequestContext;
//TODO:: need implementation
#[derive(Debug, Clone, Serialize)]
struct SendPoll<'a> {
#[serde(skip_serializing)]
ctx: RequestContext<'a>,
}

View file

@ -0,0 +1,43 @@
use crate::core::requests::{ChatId, RequestContext};
//TODO:: need implementation
///Use this method to send information about a venue. On success, the sent
/// 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
/// 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
#[serde(skip_serializing_if = "Option::is_none")]
foursquare_id: Option<String>,
/// String Optional 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<String>,
/// Boolean Optional Sends the message silently. Users will receive a
/// notification with no sound.
#[serde(skip_serializing_if = "Option::is_none")]
disable_notification: Option<bool>,
/// Integer Optional If the message is a reply, ID of the original
/// message
#[serde(skip_serializing_if = "Option::is_none")]
reply_to_message_id: Option<i32>,
/// 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
}

View file

@ -3,7 +3,7 @@ use std::path::Path;
use crate::core::{
network,
requests::{
ChatId, form_builder::FormBuilder, Request, RequestContext,
form_builder::FormBuilder, ChatId, Request, RequestContext,
RequestFuture, ResponseResult,
},
types::{InlineKeyboardMarkup, Message, ParseMode},
@ -14,23 +14,24 @@ use crate::core::{
/// returned, otherwise True is returned.
#[derive(Debug, Clone, Serialize)]
struct StopMessageLiveLocation<'a> {
#[serde(skip_serializing)]
ctx: RequestContext<'a>,
/// Required if inline_message_id is not specified. Unique identifier for
/// the target chat or username of the target channel (in the format
/// @channelusername)
#[serde(skip_serializing_if="Option::is_none")]
#[serde(skip_serializing_if = "Option::is_none")]
pub chat_id: Option<ChatId>,
/// Required if inline_message_id is not specified. Identifier of the
/// message with live location to stop
#[serde(skip_serializing_if="Option::is_none")]
#[serde(skip_serializing_if = "Option::is_none")]
pub message_id: Option<i32>,
/// Required if chat_id and message_id are not specified. Identifier of the
/// inline message
#[serde(skip_serializing_if="Option::is_none")]
#[serde(skip_serializing_if = "Option::is_none")]
pub inline_message_id: Option<String>,
/// A JSON-serialized object InlineKeyboardMarkup for a new inline
/// keyboard.
#[serde(skip_serializing_if="Option::is_none")]
#[serde(skip_serializing_if = "Option::is_none")]
pub reply_markup: Option<InlineKeyboardMarkup>,
}
@ -51,9 +52,7 @@ impl<'a> Request<'a> for StopMessageLiveLocation<'a> {
}
impl<'a> StopMessageLiveLocation<'a> {
fn new(
ctx: RequestContext<'a>,
) -> Self {
fn new(ctx: RequestContext<'a>) -> Self {
Self {
ctx,
chat_id: None,
@ -64,30 +63,34 @@ impl<'a> StopMessageLiveLocation<'a> {
}
pub fn chat_id<T>(mut self, chat_id: T) -> Self
where T: Into<ChatId>
where
T: Into<ChatId>,
{
self.chat_id = chat_id.into();
self.chat_id = Some(chat_id.into());
self
}
pub fn message_id<T>(mut self, message_id: T) -> Self
where T: Into<i32>
where
T: Into<i32>,
{
self.message_id = Some(message_id.into());
self
}
pub fn inline_message_id<T>(mut self, inline_message_id: T) -> Self
where T: Into<String>
where
T: Into<String>,
{
self.inline_message_id = Some(inline_message_id.into());
self
}
pub fn reply_markup<T>(mut self, reply_markup: T) -> Self
where T: Into<InlineKeyboardMarkup>
where
T: Into<InlineKeyboardMarkup>,
{
self.inline_message_id = Some(reply_markup.into());
self.reply_markup = Some(reply_markup.into());
self
}
}

View file

@ -0,0 +1,8 @@
use crate::core::requests::RequestContext;
//TODO:: need implementation
#[derive(Debug, Clone, Serialize)]
struct UnbanChatMember<'a> {
#[serde(skip_serializing)]
ctx: RequestContext<'a>,
}

View file

@ -1,9 +1,9 @@
use tokio::codec::FramedRead;
use std::fs::File;
use bytes::{Bytes, BytesMut};
use tokio::prelude::*;
use reqwest::r#async::multipart::Part;
use std::fs::File;
use std::path::PathBuf;
use tokio::codec::FramedRead;
use tokio::prelude::*;
struct FileDecoder;
@ -11,9 +11,12 @@ impl tokio::codec::Decoder for FileDecoder {
type Item = Bytes;
type Error = std::io::Error;
fn decode(&mut self, src: &mut BytesMut) -> Result<Option<Self::Item>, Self::Error> {
fn decode(
&mut self,
src: &mut BytesMut,
) -> Result<Option<Self::Item>, Self::Error> {
if src.is_empty() {
return Ok(None)
return Ok(None);
}
Ok(Some(src.take().freeze()))
}
@ -21,9 +24,19 @@ impl tokio::codec::Decoder for FileDecoder {
pub fn file_to_part(path_to_file: &PathBuf) -> Part {
let file = tokio::fs::File::open(path_to_file.clone())
.map(|file| FramedRead::new(file.unwrap() /* TODO: this can cause panics */, FileDecoder))
.map(|file| {
FramedRead::new(
file.unwrap(), /* TODO: this can cause panics */
FileDecoder,
)
})
.flatten_stream();
let part = Part::stream(file)
.file_name(path_to_file.file_name().unwrap().to_string_lossy().into_owned());
let part = Part::stream(file).file_name(
path_to_file
.file_name()
.unwrap()
.to_string_lossy()
.into_owned(),
);
part
}

View file

@ -0,0 +1,13 @@
use crate::core::types::PhotoSize;
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
pub struct Animation {
pub file_id: String,
pub width: u32,
pub height: u32,
pub duration: u32,
pub thumb: PhotoSize,
pub file_name: Option<String>,
pub mime_type: Option<String>,
pub file_size: Option<u32>
}

View file

@ -1,4 +1,4 @@
#[derive(Debug, Deserialize, Hash, PartialEq, Eq, Clone )]
#[derive(Debug, Deserialize, Hash, PartialEq, Eq, Clone)]
pub struct AnswerPreCheckoutQuery {
pub pre_checkout_query_id: String,
pub ok: bool,

View file

@ -32,7 +32,6 @@ pub enum ChatKind {
},
}
#[derive(Debug, Deserialize, Eq, Hash, PartialEq, Clone, Serialize)]
#[serde(rename_all = "snake_case")]
#[serde(tag = "type")]
@ -101,9 +100,8 @@ mod tests {
},
photo: None,
};
let actual = from_str(
r#"{"id":-1,"type":"channel","username":"channelname"}"#,
)
let actual =
from_str(r#"{"id":-1,"type":"channel","username":"channelname"}"#)
.unwrap();
assert_eq!(expected, actual);
}

View file

@ -1,8 +1,6 @@
use serde::Deserialization;
#[derive(Debug, Deserialization, Clone)]
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
/// This object represents a phone contact.
struct Contact {
pub struct Contact {
/// Contact's phone number
pub phone_number: String,
/// Contact's first name

6
src/core/types/file.rs Normal file
View file

@ -0,0 +1,6 @@
#[derive(Debug, Deserialize, Eq, Hash, PartialEq, Clone)]
pub struct File {
pub file_id: String,
pub file_size: u32,
pub file_path: String
}

View file

@ -8,7 +8,9 @@ pub struct InlineKeyboardButton {
pub kind: InlineKeyboardButtonKind,
}
#[derive(Debug, Clone, PartialEq, PartialOrd, Serialize, Eq, Hash, Deserialize)]
#[derive(
Debug, Clone, PartialEq, PartialOrd, Serialize, Eq, Hash, Deserialize,
)]
#[serde(rename_all = "snake_case")]
pub enum InlineKeyboardButtonKind {
/// HTTP or tg:// url to be opened when button is pressed
@ -27,13 +29,14 @@ pub enum InlineKeyboardButtonKind {
/// the user will be automatically returned to the chat they switched from,
/// skipping the chat selection screen.
SwitchInlineQuery(String),
/// Optional. If set, pressing the button will insert the bots username and
/// the specified inline query in the current chat's input field. Can be
/// empty, in which case only the bots username will be inserted.
/// Optional. If set, pressing the button will insert the bots username
/// and the specified inline query in the current chat's input field.
/// Can be empty, in which case only the bots username will be
/// inserted.
///
///This offers a quick way for the user to open your bot in inline mode in
/// the same chat good for selecting something from multiple options.
SwitchInlineQueryCurrentChat(String),
// CallbackGame(CallbackGame), TODO: разобраться, что с этим делать
// TODO: add LoginUrl, pay
/* CallbackGame(CallbackGame), TODO: разобраться, что с этим делать
* TODO: add LoginUrl, pay */
}

View file

@ -168,11 +168,11 @@ pub enum InputMedia {
impl InputMedia {
pub fn media(&self) -> &InputFile {
match self {
InputMedia::Photo { media, .. } |
InputMedia::Document { media, .. } |
InputMedia::Audio { media, .. } |
InputMedia::Animation { media, .. } |
InputMedia::Video { media, .. } => media,
InputMedia::Photo { media, .. }
| InputMedia::Document { media, .. }
| InputMedia::Audio { media, .. }
| InputMedia::Animation { media, .. }
| InputMedia::Video { media, .. } => media,
}
}
}

View file

@ -72,7 +72,7 @@ pub enum MessageKind {
},
}
#[derive(Debug, Deserialize, Eq, Hash, PartialEq, Clone, Serialize)]
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
pub enum Sender {
/// If message is sent from Chat
#[serde(rename = "from")]
@ -106,7 +106,7 @@ pub enum ForwardKind {
},
}
#[derive(Debug, Deserialize, Eq, Hash, PartialEq, Clone, Serialize)]
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
pub enum ForwardedFrom {
#[serde(rename = "forward_from")]
User(User),
@ -125,13 +125,13 @@ pub enum MediaKind {
document: (),
caption: Option<String>,
#[serde(default = "Vec::new")]
caption_entities: Vec<MessageEntity>
caption_entities: Vec<MessageEntity>,
},
Audio {
audio: Audio,
caption: Option<String>,
#[serde(default = "Vec::new")]
caption_entities: Vec<MessageEntity>
caption_entities: Vec<MessageEntity>,
},
Contact {
contact: Contact,
@ -140,7 +140,7 @@ pub enum MediaKind {
document: Document,
caption: Option<String>,
#[serde(default = "Vec::new")]
caption_entities: Vec<MessageEntity>
caption_entities: Vec<MessageEntity>,
},
Game {
game: Game,
@ -193,64 +193,7 @@ mod tests {
use serde_json::from_str;
#[test]
fn sent_message_de() {
let expected = Message {
id: 6534,
date: 1567898953,
chat: Chat {
id: 218485655,
photo: None,
kind: ChatKind::Private {
type_: (),
first_name: Some("W".to_string()),
last_name: None,
username: Some("WaffleLapkin".to_string()),
},
},
message_kind: MessageKind::IncomingMessage {
from: Sender::User(User {
id: 457569668,
is_bot: true,
first_name: "BT".to_string(),
last_name: None,
username: Some("BloodyTestBot".to_string()),
language_code: None,
}),
forward_kind: ForwardKind::Origin {
reply_to_message: None,
},
edit_date: None,
media_kind: MediaKind::Text {
text: "text".to_string(),
entities: vec![],
},
reply_markup: None,
},
};
// actual message from telegram
let json = r#"{
"message_id": 6534,
"from": {
"id": 457569668,
"is_bot": true,
"first_name": "BT",
"username": "BloodyTestBot"
},
"chat": {
"id": 218485655,
"first_name": "W",
"username": "WaffleLapkin",
"type": "private"
},
"date": 1567898953,
"text": "text"
}"#;
let actual = from_str::<Message>(json).unwrap();
assert_eq!(expected, actual);
}
#[test]
fn media_message_de() {
fn de_media_forwarded() {
let json = r#"{
"message_id": 198283,
"from": {
@ -283,60 +226,13 @@ mod tests {
"file_id": "BAADAgADpgQAAgX9qEud6QcgPiOAlhYE",
"file_size": 1381334
}
}"#;
let actual = from_str::<Message>(json).unwrap();
let expected = Message {
id: 198283,
date: 1567927221,
chat: Chat {
id: 250918540,
photo: None,
kind: ChatKind::Private {
first_name: Some("Андрей".to_string()),
last_name: Some("Власов".to_string()),
username: Some("aka_dude".to_string()),
type_: ()
}
},
message_kind: MessageKind::IncomingMessage {
from: Sender::User(User {
id: 250918540,
is_bot: false,
first_name: "Андрей".to_string(),
last_name: Some("Власов".to_string()),
username: Some("aka_dude".to_string()),
language_code: Some("en".to_string())
}),
forward_kind: ForwardKind::Origin { reply_to_message: None },
edit_date: None,
media_kind: MediaKind::Video {
video: Video {
duration: 13,
width: 512,
height: 640,
mime_type: Some("video/mp4".to_string()),
thumb: Some(PhotoSize {
file_id: "AAQCAAOmBAACBf2oS53pByA-I4CWWCObDwAEAQAHbQADMWcAAhYE".to_string(),
file_size: Some(10339),
width: 256,
height: 320
}),
file_id: "BAADAgADpgQAAgX9qEud6QcgPiOAlhYE".to_string(),
file_size: Some(1381334)
},
caption: None,
caption_entities: vec![],
media_group_id: None
},
reply_markup: None
},
};
assert_eq!(actual, expected);
}"#;
let message = from_str::<Message>(json);
assert!(message.is_ok());
}
#[test]
fn media_group_message_de() {
fn de_media_group_forwarded() {
let json = r#"{
"message_id": 198283,
"from": {
@ -370,55 +266,119 @@ mod tests {
"file_id": "BAADAgADpgQAAgX9qEud6QcgPiOAlhYE",
"file_size": 1381334
}
}"#;
let actual = from_str::<Message>(json).unwrap();
let expected = Message {
id: 198283,
date: 1567927221,
chat: Chat {
id: 250918540,
photo: None,
kind: ChatKind::Private {
first_name: Some("Андрей".to_string()),
last_name: Some("Власов".to_string()),
username: Some("aka_dude".to_string()),
type_: ()
}"#;
let message = from_str::<Message>(json);
assert!(message.is_ok());
}
},
message_kind: MessageKind::IncomingMessage {
from: Sender::User(User {
id: 250918540,
is_bot: false,
first_name: "Андрей".to_string(),
last_name: Some("Власов".to_string()),
username: Some("aka_dude".to_string()),
language_code: Some("en".to_string())
}),
forward_kind: ForwardKind::Origin { reply_to_message: None },
edit_date: None,
media_kind: MediaKind::Video {
video: Video {
duration: 13,
width: 512,
height: 640,
mime_type: Some("video/mp4".to_string()),
thumb: Some(PhotoSize {
file_id: "AAQCAAOmBAACBf2oS53pByA-I4CWWCObDwAEAQAHbQADMWcAAhYE".to_string(),
file_size: Some(10339),
width: 256,
height: 320
}),
file_id: "BAADAgADpgQAAgX9qEud6QcgPiOAlhYE".to_string(),
file_size: Some(1381334)
},
caption: None,
caption_entities: vec![],
media_group_id: Some("12543417770506682".to_string())
},
reply_markup: None
},
};
assert_eq!(actual, expected);
#[test]
fn de_text() {
let json = r#"{
"message_id": 199785,
"from": {
"id": 250918540,
"is_bot": false,
"first_name": "Андрей",
"last_name": "Власов",
"username": "aka_dude",
"language_code": "en"
},
"chat": {
"id": 250918540,
"first_name": "Андрей",
"last_name": "Власов",
"username": "aka_dude",
"type": "private"
},
"date": 1568289890,
"text": "Лол кек 😂"
}"#;
let message = from_str::<Message>(json);
assert!(message.is_ok());
}
#[test]
fn de_sticker() {
let json = r#"{
"message_id": 199787,
"from": {
"id": 250918540,
"is_bot": false,
"first_name": "Андрей",
"last_name": "Власов",
"username": "aka_dude",
"language_code": "en"
},
"chat": {
"id": 250918540,
"first_name": "Андрей",
"last_name": "Власов",
"username": "aka_dude",
"type": "private"
},
"date": 1568290188,
"sticker": {
"width": 512,
"height": 512,
"emoji": "😡",
"set_name": "AdvenTimeAnim",
"is_animated": true,
"thumb": {
"file_id": "AAQCAAMjAAOw0PgMaabKAcaXKCBLubkPAAQBAAdtAAPGKwACFgQ",
"file_size": 4118,
"width": 128,
"height": 128
},
"file_id": "CAADAgADIwADsND4DGmmygHGlyggFgQ",
"file_size": 16639
}
}"#;
let message = from_str::<Message>(json);
assert!(message.is_ok());
}
#[test]
fn de_image() {
let json = r#"{
"message_id": 199791,
"from": {
"id": 250918540,
"is_bot": false,
"first_name": "Андрей",
"last_name": "Власов",
"username": "aka_dude",
"language_code": "en"
},
"chat": {
"id": 250918540,
"first_name": "Андрей",
"last_name": "Власов",
"username": "aka_dude",
"type": "private"
},
"date": 1568290622,
"photo": [
{
"file_id": "AgADAgAD36sxG-PX0UvQSXIn9rccdw-ACA4ABAEAAwIAA20AAybcBAABFgQ",
"file_size": 18188,
"width": 320,
"height": 239
},
{
"file_id": "AgADAgAD36sxG-PX0UvQSXIn9rccdw-ACA4ABAEAAwIAA3gAAyfcBAABFgQ",
"file_size": 62123,
"width": 800,
"height": 598
},
{
"file_id": "AgADAgAD36sxG-PX0UvQSXIn9rccdw-ACA4ABAEAAwIAA3kAAyTcBAABFgQ",
"file_size": 75245,
"width": 962,
"height": 719
}
]
}"#;
let message = from_str::<Message>(json);
assert!(message.is_ok());
}
}

View file

@ -8,9 +8,13 @@ pub use self::{
chat_permissions::ChatPermissions,
chat_photo::ChatPhoto,
document::Document,
force_reply::ForceReply,
inline_keyboard_button::InlineKeyboardButton,
inline_keyboard_markup::InlineKeyboardMarkup,
input_file::InputFile,
input_media::InputMedia,
invoice::Invoice,
keyboard_button::KeyboardButton,
label_price::LabeledPrice,
message::{
ForwardKind, ForwardedFrom, MediaKind, Message, MessageKind, Sender,
@ -19,7 +23,11 @@ pub use self::{
order_info::OrderInfo,
parse_mode::ParseMode,
photo_size::PhotoSize,
poll::{Poll, PollOption},
pre_checkout_query::PreCheckoutQuery,
reply_keyboard_markup::ReplyKeyboardMarkup,
reply_keyboard_remove::ReplyKeyboardRemove,
reply_markup::ReplyMarkup,
response_parameters::ResponseParameters,
send_invoice::SendInvoice,
shipping_address::ShippingAddress,
@ -51,8 +59,8 @@ mod chat_permissions;
mod chat_photo;
mod document;
mod force_reply;
mod inline_keyboard_markup;
mod inline_keyboard_button;
mod inline_keyboard_markup;
mod input_file;
mod input_media;
mod invoice;
@ -64,11 +72,12 @@ mod not_implemented_types;
mod order_info;
mod parse_mode;
mod photo_size;
mod poll;
mod pre_checkout_query;
mod response_parameters;
mod reply_markup;
mod reply_keyboard_markup;
mod reply_keyboard_remove;
mod reply_markup;
mod response_parameters;
mod send_invoice;
mod shipping_address;
mod shipping_option;

View file

@ -1,35 +1,5 @@
#[derive(Debug, Deserialize, Eq, Hash, PartialEq, Serialize, Clone)]
pub struct Location;
#[derive(Debug, Deserialize, Eq, Hash, PartialEq, Serialize, Clone)]
pub struct InlineKeyboardMarkup;
#[derive(Debug, Deserialize, Eq, Hash, PartialEq, Serialize, Clone)]
pub struct PassportData;
#[derive(Debug, Deserialize, Eq, Hash, PartialEq, Serialize, Clone)]
pub struct Poll;
#[derive(Debug, Deserialize, Eq, Hash, PartialEq, Serialize, Clone)]
pub struct Animation;
#[derive(Debug, Deserialize, Eq, Hash, PartialEq, Serialize, Clone)]
pub struct Game;
#[derive(Debug, Deserialize, Eq, Hash, PartialEq, Serialize, Clone)]
pub struct Contact;
#[derive(Debug, Deserialize, Eq, Hash, PartialEq, Serialize, Clone)]
pub struct VideoNote;
#[derive(Debug, Deserialize, Eq, Hash, PartialEq, Serialize, Clone)]
pub struct Venue;
#[derive(Debug, Deserialize, Eq, Hash, PartialEq, Serialize, Clone)]
pub struct Voice;
#[derive(Debug, Deserialize, Eq, Hash, PartialEq, Serialize, Clone)]
pub struct MaskPosition;
#[derive(Debug, Deserialize, Eq, Hash, PartialEq, Serialize, Clone)]
pub struct ChatMemberStatus;

View file

@ -69,7 +69,6 @@ pub enum ParseMode {
Markdown,
}
#[cfg(test)]
mod tests {
use super::*;

13
src/core/types/poll.rs Normal file
View file

@ -0,0 +1,13 @@
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
pub struct Poll {
pub id: String,
pub question: String,
pub options: Vec<PollOption>,
pub is_closed: bool,
}
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
pub struct PollOption {
pub text: String,
pub voter_count: i32,
}

View file

@ -3,23 +3,24 @@ use crate::core::types::KeyboardButton;
/// This object represents a custom keyboard with reply options.
#[derive(Debug, Serialize, Deserialize, Hash, PartialEq, Eq, Clone)]
pub struct ReplyKeyboardMarkup {
/// Array of button rows, each represented by an Array of [`KeyboardButton`]
/// objects
/// Array of button rows, each represented by an Array of
/// [`KeyboardButton`] objects
pub keyboard: Vec<Vec<KeyboardButton>>,
#[serde(skip_serializing_if = "Option::is_none")]
/// Optional. Requests clients to resize the keyboard vertically for optimal
/// fit (e.g., make the keyboard smaller if there are just two rows of
/// buttons). Defaults to false, in which case the custom keyboard is always
/// of the same height as the app's standard keyboard.
/// Optional. Requests clients to resize the keyboard vertically for
/// optimal fit (e.g., make the keyboard smaller if there are just two
/// rows of buttons). Defaults to false, in which case the custom
/// keyboard is always of the same height as the app's standard
/// keyboard.
pub resize_keyboard: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
/// Optional. Requests clients to hide the keyboard as soon as it's been
/// used. The keyboard will still be available, but clients will
/// automatically display the usual letter-keyboard in the chat the user
/// can press a special button in the input field to see the custom keyboard
/// again. Defaults to false.
/// can press a special button in the input field to see the custom
/// keyboard again. Defaults to false.
pub one_time_keyboard: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]

View file

@ -1,8 +1,5 @@
use crate::core::types::{
InlineKeyboardMarkup,
ReplyKeyboardMarkup,
ReplyKeyboardRemove,
ForceReply,
ForceReply, InlineKeyboardMarkup, ReplyKeyboardMarkup, ReplyKeyboardRemove,
};
#[derive(Debug, Clone, Serialize, Deserialize)]

View file

@ -12,9 +12,8 @@ mod tests {
#[test]
fn migrate_to_chat_id_deserialization() {
let expected = ResponseParameters::MigrateToChatId(123456);
let actual: ResponseParameters = serde_json::from_str(
r#"{"migrate_to_chat_id":123456}"#
).unwrap();
let actual: ResponseParameters =
serde_json::from_str(r#"{"migrate_to_chat_id":123456}"#).unwrap();
assert_eq!(expected, actual);
}
@ -22,9 +21,8 @@ mod tests {
#[test]
fn retry_after_deserialization() {
let expected = ResponseParameters::RetryAfter(123456);
let actual: ResponseParameters = serde_json::from_str(
r#"{"retry_after":123456}"#
).unwrap();
let actual: ResponseParameters =
serde_json::from_str(r#"{"retry_after":123456}"#).unwrap();
assert_eq!(expected, actual);
}

View file

@ -0,0 +1,7 @@
use crate::core::types::PhotoSize;
#[derive(Debug, Deserialize, Eq, Hash, PartialEq, Clone, Serialize)]
pub struct UserProfilePhotos {
pub total_count: u32,
pub photos: Vec<Vec<PhotoSize>>
}

View file

@ -1,6 +1,5 @@
use crate::core::types::PhotoSize;
/// This object represents a video file.
#[derive(Debug, Deserialize, Eq, Hash, PartialEq, Serialize, Clone)]
pub struct Video {