A bunch of changes to multipart requests

- remove `FormBuilder::add_if_some`
- `FormBuilder::add` now work properly with `Option` and `InputFile`
- `FormBuilder::add_file` now accept `PathBuf` instead of `&PathBuf` and is used only in `SendMediaGroup::send`
- `ToFormValue` renamed to `IntoFromValue`
- `IntoFormValue::into_form_value` now accepts self by value and return `Option<FormValue>` instead of `String` (that gives `FormBuilder::add` abilities to work properly with `Option` and `InputFile`)
- `requests::utils::file_to_part` now accepts `PathBuf` instead of `&PathBuf`
- add `impl From<InputFile> for Option<PathBuf>`
- add `impl From<InputMedia> for InputFile`
- clean `{SendAudio,SendMediaGroup,SendPhoto}::send` code
This commit is contained in:
Waffle 2019-10-15 00:10:12 +03:00
parent 2b8ba6986c
commit 04281bd80a
7 changed files with 134 additions and 109 deletions

View file

@ -6,6 +6,7 @@ use crate::{
requests::utils,
types::{ChatId, InputMedia, ParseMode},
};
use crate::types::InputFile;
/// This is a convenient struct that builds `reqwest::multipart::Form`
/// from scratch.
@ -19,28 +20,22 @@ impl FormBuilder {
}
/// Add the supplied key-value pair to this `FormBuilder`.
pub fn add<T>(self, name: &str, value: &T) -> Self
pub fn add<T>(self, name: &str, value: T) -> Self
where
T: ToFormValue + ?Sized,
T: IntoFormValue,
{
Self {
form: self.form.text(name.to_owned(), value.to_form_value()),
let name = name.to_owned();
match value.into_form_value() {
Some(FormValue::Str(string)) => Self {
form: self.form.text(name, string),
},
Some(FormValue::File(path)) => self.add_file(&name, path),
None => self,
}
}
/// 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,
{
match value {
None => Self { form: self.form },
Some(value) => self.add(name, value),
}
}
pub fn add_file(self, name: &str, path_to_file: &PathBuf) -> Self {
// used in SendMediaGroup
pub fn add_file(self, name: &str, path_to_file: PathBuf) -> Self {
Self {
form: self
.form
@ -53,50 +48,83 @@ impl FormBuilder {
}
}
pub trait ToFormValue {
fn to_form_value(&self) -> String;
pub enum FormValue {
File(PathBuf),
Str(String),
}
pub trait IntoFormValue {
fn into_form_value(self) -> Option<FormValue>;
}
macro_rules! impl_for_struct {
($($name:ty),*) => {
$(
impl ToFormValue for $name {
fn to_form_value(&self) -> String {
serde_json::to_string(self).expect("serde_json::to_string failed")
impl IntoFormValue for $name {
fn into_form_value(self) -> Option<FormValue> {
let json = serde_json::to_string(&self)
.expect("serde_json::to_string failed");
Some(FormValue::Str(json))
}
}
)*
};
}
impl_for_struct!(bool, i32, i64, Vec<InputMedia>);
impl_for_struct!(bool, i32, i64);
impl ToFormValue for str {
fn to_form_value(&self) -> String {
self.to_owned()
impl<T> IntoFormValue for Option<T> where T: IntoFormValue {
fn into_form_value(self) -> Option<FormValue> {
self.and_then(IntoFormValue::into_form_value)
}
}
impl ToFormValue for ParseMode {
fn to_form_value(&self) -> String {
match self {
impl IntoFormValue for &[InputMedia] {
fn into_form_value(self) -> Option<FormValue> {
let json = serde_json::to_string(self)
.expect("serde_json::to_string failed");
Some(FormValue::Str(json))
}
}
impl IntoFormValue for &str {
fn into_form_value(self) -> Option<FormValue> {
Some(FormValue::Str(self.to_owned()))
}
}
impl IntoFormValue for ParseMode {
fn into_form_value(self) -> Option<FormValue> {
let string = match self {
ParseMode::HTML => String::from("HTML"),
ParseMode::Markdown => String::from("Markdown"),
}
};
Some(FormValue::Str(string))
}
}
impl ToFormValue for ChatId {
fn to_form_value(&self) -> String {
match self {
impl IntoFormValue for ChatId {
fn into_form_value(self) -> Option<FormValue> {
let string = match self {
ChatId::Id(id) => id.to_string(),
ChatId::ChannelUsername(username) => username.clone(),
}
};
Some(FormValue::Str(string))
}
}
impl ToFormValue for String {
fn to_form_value(&self) -> String {
self.to_owned()
impl IntoFormValue for String {
fn into_form_value(self) -> Option<FormValue> {
Some(FormValue::Str(self.to_owned()))
}
}
impl IntoFormValue for InputFile {
fn into_form_value(self) -> Option<FormValue> {
match self {
InputFile::File(path) => Some(FormValue::File(path)),
InputFile::Url(url) => Some(FormValue::Str(url)),
InputFile::FileId(file_id) => Some(FormValue::Str(file_id)),
}
}
}

View file

@ -74,40 +74,23 @@ impl Request for SendAudio<'_> {
impl SendAudio<'_> {
pub async fn send(self) -> ResponseResult<Message> {
let mut params = FormBuilder::new()
.add("chat_id", &self.chat_id)
.add_if_some("caption", self.caption.as_ref())
.add_if_some("parse_mode", self.parse_mode.as_ref())
.add_if_some("duration", self.duration.as_ref())
.add_if_some("performer", self.performer.as_ref())
.add_if_some("title", self.title.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(),
);
params = match self.audio {
InputFile::File(file) => params.add_file("audio", &file),
InputFile::Url(url) => params.add("audio", &url),
InputFile::FileId(file_id) => params.add("audio", &file_id),
};
if let Some(thumb) = self.thumb {
params = match thumb {
InputFile::File(file) => params.add_file("thumb", &file),
InputFile::Url(url) => params.add("thumb", &url),
InputFile::FileId(file_id) => params.add("thumb", &file_id),
}
}
let params = params.build();
let params = FormBuilder::new()
.add("chat_id", self.chat_id)
.add("caption", self.caption)
.add("parse_mode", self.parse_mode)
.add("duration", self.duration)
.add("performer", self.performer)
.add("title", self.title)
.add("disable_notification", self.disable_notification)
.add("reply_to_message_id", self.reply_to_message_id)
.add("audio", self.audio)
.add("thumb", self.thumb);
network::request_multipart(
&self.ctx.client,
&self.ctx.token,
"sendAudio",
params,
params.build(),
)
.await
}

View file

@ -5,7 +5,7 @@ use crate::{
requests::{
form_builder::FormBuilder, Request, RequestContext, ResponseResult,
},
types::{ChatId, InputMedia, Message},
types::{ChatId, InputMedia, Message, InputFile},
};
/// Use this method to send a group of photos or videos as an album.
@ -32,21 +32,15 @@ impl Request for SendMediaGroup<'_> {
impl SendMediaGroup<'_> {
pub async fn send(self) -> ResponseResult<Vec<Message>> {
let form = FormBuilder::new()
.add("chat_id", &self.chat_id)
.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("chat_id", self.chat_id)
.add("media", &self.media[..])
.add("disable_notification", self.disable_notification)
.add("reply_to_message_id", self.reply_to_message_id);
let form = self.media.iter().filter_map(|e| e.media().as_file())
.fold(form, |acc, path|
let form = self.media.into_iter().filter_map(|e| InputFile::from(e).into())
.fold(form, |acc, path: std::path::PathBuf|
acc.add_file(
&path.file_name().unwrap().to_string_lossy(),
&path.file_name().unwrap().to_string_lossy().into_owned(),
path,
)
);
@ -96,3 +90,14 @@ impl<'a> SendMediaGroup<'a> {
self
}
}
#[tokio::test]
async fn main() {
use crate::types::InputMedia;
let bot = crate::bot::Bot::new("457569668:AAF4mhmoPmH1Ud943bZqX-EYRCxKXmTt0f8");
bot.send_media_group(218485655, vec![
InputMedia::Photo { media: InputFile::File(std::path::PathBuf::from("/home/waffle/Pictures/28b.png")), caption: None, parse_mode: None },
InputMedia::Photo { media: InputFile::File(std::path::PathBuf::from("/home/waffle/Pictures/334-3341035_free-png-download-tide-pod-chan-transparent-png.png")), caption: None, parse_mode: None }]).send().await.unwrap();
bot.send_photo(218485655, InputFile::File(std::path::PathBuf::from("/home/waffle/Pictures/28b.png"))).send().await.unwrap();
}

View file

@ -55,31 +55,19 @@ impl Request for SendPhoto<'_> {
impl SendPhoto<'_> {
pub async fn send(self) -> ResponseResult<Message> {
let mut params = FormBuilder::new()
.add("chat_id", &self.chat_id)
.add_if_some("caption", self.caption.as_ref())
.add_if_some("parse_mode", self.parse_mode.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(),
);
params = match self.photo {
InputFile::File(path) => params.add_file("photo", &path),
InputFile::Url(url) => params.add("photo", &url),
InputFile::FileId(file_id) => params.add("photo", &file_id),
};
let params = params.build();
let params = FormBuilder::new()
.add("chat_id", self.chat_id)
.add("caption", self.caption)
.add("parse_mode", self.parse_mode)
.add("disable_notification", self.disable_notification)
.add("reply_to_message_id", self.reply_to_message_id)
.add("photo", self.photo);
network::request_multipart(
&self.ctx.client,
&self.ctx.token,
"sendPhoto",
params,
params.build(),
)
.await
}

View file

@ -21,8 +21,14 @@ 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())
pub fn file_to_part(path_to_file: PathBuf) -> Part {
let file_name = path_to_file
.file_name()
.unwrap()
.to_string_lossy()
.into_owned();
let file = tokio::fs::File::open(path_to_file)
.map(|file| {
FramedRead::new(
file.unwrap(), /* TODO: this can cause panics */
@ -30,12 +36,6 @@ pub fn file_to_part(path_to_file: &PathBuf) -> Part {
)
})
.flatten_stream();
let part = Part::stream(Body::wrap_stream(file)).file_name(
path_to_file
.file_name()
.unwrap()
.to_string_lossy()
.into_owned(),
);
let part = Part::stream(Body::wrap_stream(file)).file_name(file_name);
part
}

View file

@ -30,6 +30,15 @@ impl InputFile {
}
}
impl From<InputFile> for Option<PathBuf> {
fn from(file: InputFile) -> Self {
match file {
InputFile::File(path) => Some(path),
_ => None,
}
}
}
impl serde::Serialize for InputFile {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where

View file

@ -177,6 +177,18 @@ impl InputMedia {
}
}
impl From<InputMedia> for InputFile {
fn from(media: InputMedia) -> InputFile {
match media {
InputMedia::Photo { media, .. }
| InputMedia::Document { media, .. }
| InputMedia::Audio { media, .. }
| InputMedia::Animation { media, .. }
| InputMedia::Video { media, .. } => media,
}
}
}
#[cfg(test)]
mod tests {
use super::*;