rewrite requests to use serde-multipart

Rewrite multipart requests to use serde_multipart instead of FromBuilder
This commit is contained in:
Waffle 2020-08-16 00:21:55 +03:00
parent b303958afd
commit 5afe72b368
19 changed files with 149 additions and 475 deletions

View file

@ -17,7 +17,7 @@ const TELEGRAM_API_URL: &str = "https://api.telegram.org";
///
/// [Telegram documentation]: https://core.telegram.org/bots/api#making-requests
fn method_url(base: &str, token: &str, method_name: &str) -> String {
format!("{url}/bot{token}/{method}", url = base, token = token, method = method_name,)
format!("{url}/bot{token}/{method}", url = base, token = token, method = method_name)
}
/// Creates URL for downloading a file. See the [Telegram documentation].

View file

@ -1,25 +1,39 @@
use reqwest::{multipart::Form, Client, Response};
use std::time::Duration;
use reqwest::{Client, Response};
use serde::{de::DeserializeOwned, Serialize};
use crate::{requests::ResponseResult, RequestError};
use super::{TelegramResponse, TELEGRAM_API_URL};
use std::time::Duration;
use crate::{
net::{TelegramResponse, TELEGRAM_API_URL},
requests::ResponseResult,
serde_multipart::to_form,
RequestError,
};
const DELAY_ON_SERVER_ERROR: Duration = Duration::from_secs(10);
pub async fn request_multipart<T>(
pub async fn request_multipart<P, R>(
client: &Client,
token: &str,
method_name: &str,
params: Form,
) -> ResponseResult<T>
params: &P, // I'll regret this
) -> ResponseResult<R>
where
T: DeserializeOwned,
P: Serialize,
R: DeserializeOwned,
{
use crate::serde_multipart::Error;
let form = match to_form(params).await {
Ok(x) => x,
Err(Error::Io(ioerr)) => return Err(RequestError::Io(ioerr)),
Err(_) => unreachable!(
"we don't create requests those fail to serialize (if you see this, open an issue :|)"
),
};
let response = client
.post(&super::method_url(TELEGRAM_API_URL, token, method_name))
.multipart(params)
.multipart(form)
.send()
.await
.map_err(RequestError::NetworkError)?;
@ -27,15 +41,15 @@ where
process_response(response).await
}
pub async fn request_json<T, P>(
pub async fn request_json<P, R>(
client: &Client,
token: &str,
method_name: &str,
params: &P,
) -> ResponseResult<T>
) -> ResponseResult<R>
where
T: DeserializeOwned,
P: Serialize,
R: DeserializeOwned,
{
let response = client
.post(&super::method_url(TELEGRAM_API_URL, token, method_name))

View file

@ -1,51 +1,38 @@
use serde::Serialize;
use crate::{
net,
requests::form_builder::FormBuilder,
types::{MaskPosition, True},
Bot,
};
use crate::{
requests::{RequestWithFile, ResponseResult},
requests::{Request, ResponseResult},
types::StickerType,
};
/// Use this method to add a new sticker to a set created by the bot.
///
/// [The official docs](https://core.telegram.org/bots/api#addstickertoset).
#[derive(Debug, Clone)]
#[serde_with_macros::skip_serializing_none]
#[derive(Debug, Clone, Serialize)]
pub struct AddStickerToSet {
#[serde(skip_serializing)]
bot: Bot,
user_id: i32,
name: String,
#[serde(flatten)]
sticker_type: StickerType,
emojis: String,
mask_position: Option<MaskPosition>,
}
#[async_trait::async_trait]
impl RequestWithFile for AddStickerToSet {
impl Request for AddStickerToSet {
type Output = True;
async fn send(&self) -> tokio::io::Result<ResponseResult<True>> {
let builder =
FormBuilder::new().add_text("user_id", &self.user_id).add_text("name", &self.name);
let builder = match &self.sticker_type {
StickerType::Png(file) => builder.add_input_file("png_sticker", &file),
StickerType::Tgs(file) => builder.add_input_file("tgs_sticker", &file),
}
.await?
.add_text("emojis", &self.emojis)
.add_text("mask_position", &self.mask_position);
Ok(net::request_multipart(
self.bot.client(),
self.bot.token(),
"addStickerToSet",
builder.build(),
)
.await)
async fn send(&self) -> ResponseResult<True> {
net::request_multipart(self.bot.client(), self.bot.token(), "addStickerToSet", self).await
}
}

View file

@ -1,6 +1,8 @@
use serde::Serialize;
use crate::{
net,
requests::{form_builder::FormBuilder, RequestWithFile, ResponseResult},
requests::{Request, ResponseResult},
types::{MaskPosition, StickerType, True},
Bot,
};
@ -9,12 +11,15 @@ use crate::{
/// able to edit the created sticker set.
///
/// [The official docs](https://core.telegram.org/bots/api#createnewstickerset).
#[derive(Debug, Clone)]
#[serde_with_macros::skip_serializing_none]
#[derive(Debug, Clone, Serialize)]
pub struct CreateNewStickerSet {
#[serde(skip_serializing)]
bot: Bot,
user_id: i32,
name: String,
title: String,
#[serde(flatten)]
sticker_type: StickerType,
emojis: String,
contains_masks: Option<bool>,
@ -22,31 +27,12 @@ pub struct CreateNewStickerSet {
}
#[async_trait::async_trait]
impl RequestWithFile for CreateNewStickerSet {
impl Request for CreateNewStickerSet {
type Output = True;
async fn send(&self) -> tokio::io::Result<ResponseResult<True>> {
let builder = FormBuilder::new()
.add_text("user_id", &self.user_id)
.add_text("name", &self.name)
.add_text("title", &self.title);
let builder = match &self.sticker_type {
StickerType::Png(file) => builder.add_input_file("png_sticker", &file),
StickerType::Tgs(file) => builder.add_input_file("tgs_sticker", &file),
}
.await?
.add_text("emojis", &self.emojis)
.add_text("contains_masks", &self.contains_masks)
.add_text("mask_position", &self.mask_position);
Ok(net::request_multipart(
self.bot.client(),
self.bot.token(),
"createNewStickerSet",
builder.build(),
)
.await)
async fn send(&self) -> ResponseResult<True> {
net::request_multipart(self.bot.client(), self.bot.token(), "createNewStickerSet", self)
.await
}
}

View file

@ -1,6 +1,8 @@
use serde::Serialize;
use crate::{
net,
requests::{form_builder::FormBuilder, Request, ResponseResult},
requests::{Request, ResponseResult},
types::{InlineKeyboardMarkup, InputMedia, True},
Bot,
};
@ -17,8 +19,10 @@ use crate::{
/// [The official docs](https://core.telegram.org/bots/api#editmessagemedia).
///
/// [`True`]: crate::types::True
#[derive(Debug, Clone)]
#[serde_with_macros::skip_serializing_none]
#[derive(Debug, Clone, Serialize)]
pub struct EditInlineMessageMedia {
#[serde(skip_serializing)]
bot: Bot,
inline_message_id: String,
media: InputMedia,
@ -30,17 +34,7 @@ impl Request for EditInlineMessageMedia {
type Output = True;
async fn send(&self) -> ResponseResult<True> {
net::request_multipart(
self.bot.client(),
self.bot.token(),
"editMessageMedia",
FormBuilder::new()
.add_text("media", &self.media)
.add_text("reply_markup", &self.reply_markup)
.add_text("inline_message_id", &self.inline_message_id)
.build(),
)
.await
net::request_multipart(self.bot.client(), self.bot.token(), "editMessageMedia", self).await
}
}

View file

@ -1,6 +1,8 @@
use serde::Serialize;
use crate::{
net,
requests::{form_builder::FormBuilder, Request, ResponseResult},
requests::{Request, ResponseResult},
types::{ChatId, InlineKeyboardMarkup, InputMedia, Message},
Bot,
};
@ -15,8 +17,10 @@ use crate::{
/// [The official docs](https://core.telegram.org/bots/api#editmessagemedia).
///
/// [`Message`]: crate::types::Message
#[derive(Debug, Clone)]
#[serde_with_macros::skip_serializing_none]
#[derive(Debug, Clone, Serialize)]
pub struct EditMessageMedia {
#[serde(skip_serializing)]
bot: Bot,
chat_id: ChatId,
message_id: i32,
@ -29,18 +33,7 @@ impl Request for EditMessageMedia {
type Output = Message;
async fn send(&self) -> ResponseResult<Message> {
net::request_multipart(
self.bot.client(),
self.bot.token(),
"editMessageMedia",
FormBuilder::new()
.add_text("chat_id", &self.chat_id)
.add_text("message_id", &self.message_id)
.add_text("media", &self.media)
.add_text("reply_markup", &self.reply_markup)
.build(),
)
.await
net::request_multipart(self.bot.client(), self.bot.token(), "editMessageMedia", self).await
}
}

View file

@ -1,6 +1,8 @@
use serde::Serialize;
use crate::{
net,
requests::{form_builder::FormBuilder, RequestWithFile, ResponseResult},
requests::{Request, ResponseResult},
types::{ChatId, InputFile, Message, ParseMode, ReplyMarkup},
Bot,
};
@ -12,8 +14,10 @@ use crate::{
/// may be changed in the future.
///
/// [The official docs](https://core.telegram.org/bots/api#sendanimation).
#[derive(Debug, Clone)]
#[serde_with_macros::skip_serializing_none]
#[derive(Debug, Clone, Serialize)]
pub struct SendAnimation {
#[serde(skip_serializing)]
bot: Bot,
pub chat_id: ChatId,
pub animation: InputFile,
@ -29,32 +33,11 @@ pub struct SendAnimation {
}
#[async_trait::async_trait]
impl RequestWithFile for SendAnimation {
impl Request for SendAnimation {
type Output = Message;
async fn send(&self) -> tokio::io::Result<ResponseResult<Message>> {
let mut builder = FormBuilder::new()
.add_text("chat_id", &self.chat_id)
.add_input_file("animation", &self.animation)
.await?
.add_text("duration", &self.duration)
.add_text("width", &self.width)
.add_text("height", &self.height)
.add_text("caption", &self.caption)
.add_text("parse_mode", &self.parse_mode)
.add_text("disable_notification", &self.disable_notification)
.add_text("reply_to_message_id", &self.reply_to_message_id)
.add_text("reply_markup", &self.reply_markup);
if let Some(thumb) = self.thumb.as_ref() {
builder = builder.add_input_file("thumb", thumb).await?;
}
Ok(net::request_multipart(
self.bot.client(),
self.bot.token(),
"sendAnimation",
builder.build(),
)
.await)
async fn send(&self) -> ResponseResult<Message> {
net::request_multipart(self.bot.client(), self.bot.token(), "sendAnimation", self).await
}
}

View file

@ -1,6 +1,8 @@
use serde::Serialize;
use crate::{
net,
requests::{form_builder::FormBuilder, RequestWithFile, ResponseResult},
requests::{Request, ResponseResult},
types::{ChatId, InputFile, Message, ParseMode, ReplyMarkup},
Bot,
};
@ -16,8 +18,10 @@ use crate::{
/// [The official docs](https://core.telegram.org/bots/api#sendaudio).
///
/// [`Bot::send_voice`]: crate::Bot::send_voice
#[derive(Debug, Clone)]
#[serde_with_macros::skip_serializing_none]
#[derive(Debug, Clone, Serialize)]
pub struct SendAudio {
#[serde(skip_serializing)]
bot: Bot,
chat_id: ChatId,
audio: InputFile,
@ -33,32 +37,11 @@ pub struct SendAudio {
}
#[async_trait::async_trait]
impl RequestWithFile for SendAudio {
impl Request for SendAudio {
type Output = Message;
async fn send(&self) -> tokio::io::Result<ResponseResult<Message>> {
let mut builder = FormBuilder::new()
.add_text("chat_id", &self.chat_id)
.add_input_file("audio", &self.audio)
.await?
.add_text("caption", &self.caption)
.add_text("parse_mode", &self.parse_mode)
.add_text("duration", &self.duration)
.add_text("performer", &self.performer)
.add_text("title", &self.title)
.add_text("disable_notification", &self.disable_notification)
.add_text("reply_to_message_id", &self.reply_to_message_id)
.add_text("reply_markup", &self.reply_markup);
if let Some(thumb) = self.thumb.as_ref() {
builder = builder.add_input_file("thumb", thumb).await?;
}
Ok(net::request_multipart(
self.bot.client(),
self.bot.token(),
"sendAudio",
builder.build(),
)
.await)
async fn send(&self) -> ResponseResult<Message> {
net::request_multipart(self.bot.client(), self.bot.token(), "sendAudio", self).await
}
}

View file

@ -1,6 +1,8 @@
use serde::Serialize;
use crate::{
net,
requests::{form_builder::FormBuilder, RequestWithFile, ResponseResult},
requests::{Request, ResponseResult},
types::{ChatId, InputFile, Message, ParseMode, ReplyMarkup},
Bot,
};
@ -11,8 +13,10 @@ use crate::{
/// may be changed in the future.
///
/// [The official docs](https://core.telegram.org/bots/api#senddocument).
#[derive(Debug, Clone)]
#[serde_with_macros::skip_serializing_none]
#[derive(Debug, Clone, Serialize)]
pub struct SendDocument {
#[serde(skip_serializing)]
bot: Bot,
chat_id: ChatId,
document: InputFile,
@ -25,29 +29,11 @@ pub struct SendDocument {
}
#[async_trait::async_trait]
impl RequestWithFile for SendDocument {
impl Request for SendDocument {
type Output = Message;
async fn send(&self) -> tokio::io::Result<ResponseResult<Message>> {
let mut builder = FormBuilder::new()
.add_text("chat_id", &self.chat_id)
.add_input_file("document", &self.document)
.await?
.add_text("caption", &self.caption)
.add_text("parse_mode", &self.parse_mode)
.add_text("disable_notification", &self.disable_notification)
.add_text("reply_to_message_id", &self.reply_to_message_id)
.add_text("reply_markup", &self.reply_markup);
if let Some(thumb) = self.thumb.as_ref() {
builder = builder.add_input_file("thumb", thumb).await?;
}
Ok(net::request_multipart(
self.bot.client(),
self.bot.token(),
"sendDocument",
builder.build(),
)
.await)
async fn send(&self) -> ResponseResult<Message> {
net::request_multipart(self.bot.client(), self.bot.token(), "sendDocument", self).await
}
}

View file

@ -1,6 +1,8 @@
use serde::Serialize;
use crate::{
net,
requests::{form_builder::FormBuilder, Request, ResponseResult},
requests::{Request, ResponseResult},
types::{ChatId, InputMedia, Message},
Bot,
};
@ -8,8 +10,10 @@ use crate::{
/// Use this method to send a group of photos or videos as an album.
///
/// [The official docs](https://core.telegram.org/bots/api#sendmediagroup).
#[derive(Debug, Clone)]
#[serde_with_macros::skip_serializing_none]
#[derive(Debug, Clone, Serialize)]
pub struct SendMediaGroup {
#[serde(skip_serializing)]
bot: Bot,
chat_id: ChatId,
media: Vec<InputMedia>, // TODO: InputMediaPhoto and InputMediaVideo
@ -22,18 +26,7 @@ impl Request for SendMediaGroup {
type Output = Vec<Message>;
async fn send(&self) -> ResponseResult<Vec<Message>> {
net::request_multipart(
self.bot.client(),
self.bot.token(),
"sendMediaGroup",
FormBuilder::new()
.add_text("chat_id", &self.chat_id)
.add_text("media", &self.media)
.add_text("disable_notification", &self.disable_notification)
.add_text("reply_to_message_id", &self.reply_to_message_id)
.build(),
)
.await
net::request_multipart(self.bot.client(), self.bot.token(), "sendMediaGroup", self).await
}
}

View file

@ -1,6 +1,8 @@
use serde::Serialize;
use crate::{
net,
requests::{form_builder::FormBuilder, RequestWithFile, ResponseResult},
requests::{Request, ResponseResult},
types::{ChatId, InputFile, Message, ParseMode, ReplyMarkup},
Bot,
};
@ -8,8 +10,10 @@ use crate::{
/// Use this method to send photos.
///
/// [The official docs](https://core.telegram.org/bots/api#sendphoto).
#[derive(Debug, Clone)]
#[serde_with_macros::skip_serializing_none]
#[derive(Debug, Clone, Serialize)]
pub struct SendPhoto {
#[serde(skip_serializing)]
bot: Bot,
chat_id: ChatId,
photo: InputFile,
@ -21,26 +25,11 @@ pub struct SendPhoto {
}
#[async_trait::async_trait]
impl RequestWithFile for SendPhoto {
impl Request for SendPhoto {
type Output = Message;
async fn send(&self) -> tokio::io::Result<ResponseResult<Message>> {
Ok(net::request_multipart(
self.bot.client(),
self.bot.token(),
"sendPhoto",
FormBuilder::new()
.add_text("chat_id", &self.chat_id)
.add_input_file("photo", &self.photo)
.await?
.add_text("caption", &self.caption)
.add_text("parse_mode", &self.parse_mode)
.add_text("disable_notification", &self.disable_notification)
.add_text("reply_to_message_id", &self.reply_to_message_id)
.add_text("reply_markup", &self.reply_markup)
.build(),
)
.await)
async fn send(&self) -> ResponseResult<Message> {
net::request_multipart(self.bot.client(), self.bot.token(), "sendPhoto", self).await
}
}

View file

@ -1,6 +1,8 @@
use serde::Serialize;
use crate::{
net,
requests::{form_builder::FormBuilder, RequestWithFile, ResponseResult},
requests::{Request, ResponseResult},
types::{ChatId, InputFile, Message, ReplyMarkup},
Bot,
};
@ -10,8 +12,10 @@ use crate::{
/// [The official docs](https://core.telegram.org/bots/api#sendsticker).
///
/// [animated]: https://telegram.org/blog/animated-stickers
#[derive(Debug, Clone)]
#[serde_with_macros::skip_serializing_none]
#[derive(Debug, Clone, Serialize)]
pub struct SendSticker {
#[serde(skip_serializing)]
bot: Bot,
chat_id: ChatId,
sticker: InputFile,
@ -21,24 +25,11 @@ pub struct SendSticker {
}
#[async_trait::async_trait]
impl RequestWithFile for SendSticker {
impl Request for SendSticker {
type Output = Message;
async fn send(&self) -> tokio::io::Result<ResponseResult<Message>> {
Ok(net::request_multipart(
self.bot.client(),
self.bot.token(),
"sendSticker",
FormBuilder::new()
.add_text("chat_id", &self.chat_id)
.add_input_file("sticker", &self.sticker)
.await?
.add_text("disable_notification", &self.disable_notification)
.add_text("reply_to_message_id", &self.reply_to_message_id)
.add_text("reply_markup", &self.reply_markup)
.build(),
)
.await)
async fn send(&self) -> ResponseResult<Message> {
net::request_multipart(self.bot.client(), self.bot.token(), "sendSticker", self).await
}
}

View file

@ -1,6 +1,8 @@
use serde::Serialize;
use crate::{
net,
requests::{form_builder::FormBuilder, RequestWithFile, ResponseResult},
requests::{Request, ResponseResult},
types::{ChatId, InputFile, Message, ParseMode, ReplyMarkup},
Bot,
};
@ -12,8 +14,10 @@ use crate::{
/// limit may be changed in the future.
///
/// [The official docs](https://core.telegram.org/bots/api#sendvideo).
#[derive(Debug, Clone)]
#[serde_with_macros::skip_serializing_none]
#[derive(Debug, Clone, Serialize)]
pub struct SendVideo {
#[serde(skip_serializing)]
bot: Bot,
chat_id: ChatId,
video: InputFile,
@ -30,33 +34,11 @@ pub struct SendVideo {
}
#[async_trait::async_trait]
impl RequestWithFile for SendVideo {
impl Request for SendVideo {
type Output = Message;
async fn send(&self) -> tokio::io::Result<ResponseResult<Message>> {
let mut builder = FormBuilder::new()
.add_text("chat_id", &self.chat_id)
.add_input_file("video", &self.video)
.await?
.add_text("duration", &self.duration)
.add_text("width", &self.width)
.add_text("height", &self.height)
.add_text("caption", &self.caption)
.add_text("parse_mode", &self.parse_mode)
.add_text("supports_streaming", &self.supports_streaming)
.add_text("disable_notification", &self.disable_notification)
.add_text("reply_to_message_id", &self.reply_to_message_id)
.add_text("reply_markup", &self.reply_markup);
if let Some(thumb) = self.thumb.as_ref() {
builder = builder.add_input_file("thumb", thumb).await?;
}
Ok(net::request_multipart(
self.bot.client(),
self.bot.token(),
"sendVideo",
builder.build(),
)
.await)
async fn send(&self) -> ResponseResult<Message> {
net::request_multipart(self.bot.client(), self.bot.token(), "sendVideo", self).await
}
}

View file

@ -1,6 +1,8 @@
use serde::Serialize;
use crate::{
net,
requests::{form_builder::FormBuilder, RequestWithFile, ResponseResult},
requests::{Request, ResponseResult},
types::{ChatId, InputFile, Message, ReplyMarkup},
Bot,
};
@ -11,8 +13,10 @@ use crate::{
/// [The official docs](https://core.telegram.org/bots/api#sendvideonote).
///
/// [v.4.0]: https://telegram.org/blog/video-messages-and-telescope
#[derive(Debug, Clone)]
#[serde_with_macros::skip_serializing_none]
#[derive(Debug, Clone, Serialize)]
pub struct SendVideoNote {
#[serde(skip_serializing)]
bot: Bot,
chat_id: ChatId,
video_note: InputFile,
@ -25,29 +29,11 @@ pub struct SendVideoNote {
}
#[async_trait::async_trait]
impl RequestWithFile for SendVideoNote {
impl Request for SendVideoNote {
type Output = Message;
async fn send(&self) -> tokio::io::Result<ResponseResult<Message>> {
let mut builder = FormBuilder::new()
.add_text("chat_id", &self.chat_id)
.add_input_file("video_note", &self.video_note)
.await?
.add_text("duration", &self.duration)
.add_text("length", &self.length)
.add_text("disable_notification", &self.disable_notification)
.add_text("reply_to_message_id", &self.reply_to_message_id)
.add_text("reply_markup", &self.reply_markup);
if let Some(thumb) = self.thumb.as_ref() {
builder = builder.add_input_file("thumb", thumb).await?;
}
Ok(net::request_multipart(
self.bot.client(),
self.bot.token(),
"sendVideoNote",
builder.build(),
)
.await)
async fn send(&self) -> ResponseResult<Message> {
net::request_multipart(self.bot.client(), self.bot.token(), "sendVideoNote", self).await
}
}

View file

@ -1,6 +1,8 @@
use serde::Serialize;
use crate::{
net,
requests::{form_builder::FormBuilder, RequestWithFile, ResponseResult},
requests::{Request, ResponseResult},
types::{ChatId, InputFile, Message, ParseMode, ReplyMarkup},
Bot,
};
@ -17,8 +19,10 @@ use crate::{
///
/// [`Audio`]: crate::types::Audio
/// [`Document`]: crate::types::Document
#[derive(Debug, Clone)]
#[serde_with_macros::skip_serializing_none]
#[derive(Debug, Clone, Serialize)]
pub struct SendVoice {
#[serde(skip_serializing)]
bot: Bot,
chat_id: ChatId,
voice: InputFile,
@ -31,27 +35,11 @@ pub struct SendVoice {
}
#[async_trait::async_trait]
impl RequestWithFile for SendVoice {
impl Request for SendVoice {
type Output = Message;
async fn send(&self) -> tokio::io::Result<ResponseResult<Message>> {
Ok(net::request_multipart(
self.bot.client(),
self.bot.token(),
"sendVoice",
FormBuilder::new()
.add_text("chat_id", &self.chat_id)
.add_input_file("voice", &self.voice)
.await?
.add_text("caption", &self.caption)
.add_text("parse_mode", &self.parse_mode)
.add_text("duration", &self.duration)
.add_text("disable_notification", &self.disable_notification)
.add_text("reply_to_message_id", &self.reply_to_message_id)
.add_text("reply_markup", &self.reply_markup)
.build(),
)
.await)
async fn send(&self) -> ResponseResult<Message> {
net::request_multipart(self.bot.client(), self.bot.token(), "sendVoice", self).await
}
}

View file

@ -1,156 +0,0 @@
use std::{borrow::Cow, path::PathBuf};
use reqwest::multipart::Form;
use crate::{
requests::utils::{file_from_memory_to_part, file_to_part},
types::{
ChatId, InlineKeyboardMarkup, InputFile, InputMedia, MaskPosition, ParseMode, ReplyMarkup,
},
};
/// This is a convenient struct that builds `reqwest::multipart::Form`
/// from scratch.
pub(crate) struct FormBuilder {
form: Form,
}
impl FormBuilder {
pub(crate) fn new() -> Self {
Self { form: Form::new() }
}
pub fn add_text<'a, T, N>(self, name: N, value: &T) -> Self
where
N: Into<Cow<'a, str>>,
T: IntoFormText,
{
match value.into_form_text() {
Some(val) => Self { form: self.form.text(name.into().into_owned(), val) },
None => self,
}
}
pub async fn add_input_file<'a, N>(self, name: N, value: &InputFile) -> tokio::io::Result<Self>
where
N: Into<Cow<'a, str>>,
{
Ok(match value {
InputFile::File(path) => self.add_file(name, path.clone()).await?,
InputFile::Memory { file_name, data } => {
self.add_file_from_memory(name, file_name.clone(), data.clone())
}
InputFile::Url(url) => self.add_text(name, url),
InputFile::FileId(file_id) => self.add_text(name, file_id),
})
}
// used in SendMediaGroup
pub async fn add_file<'a, N>(self, name: N, path_to_file: PathBuf) -> tokio::io::Result<Self>
where
N: Into<Cow<'a, str>>,
{
Ok(Self {
form: self.form.part(name.into().into_owned(), file_to_part(path_to_file).await?),
})
}
fn add_file_from_memory<'a, N>(
self,
name: N,
file_name: String,
data: Cow<'static, [u8]>,
) -> Self
where
N: Into<Cow<'a, str>>,
{
Self {
form: self
.form
.part(name.into().into_owned(), file_from_memory_to_part(data, file_name)),
}
}
pub fn build(self) -> Form {
self.form
}
}
pub(crate) trait IntoFormText {
fn into_form_text(&self) -> Option<String>;
}
macro_rules! impl_for_struct {
($($name:ty),*) => {
$(
impl IntoFormText for $name {
fn into_form_text(&self) -> Option<String> {
let json = serde_json::to_string(self)
.expect("serde_json::to_string failed");
Some(json)
}
}
)*
};
}
impl_for_struct!(bool, i32, i64, u32, ReplyMarkup, InlineKeyboardMarkup, MaskPosition);
impl<T> IntoFormText for Option<T>
where
T: IntoFormText,
{
fn into_form_text(&self) -> Option<String> {
self.as_ref().and_then(IntoFormText::into_form_text)
}
}
// TODO: fix InputMedia implementation of IntoFormValue (for now it doesn't
// encode files :|)
impl IntoFormText for Vec<InputMedia> {
fn into_form_text(&self) -> Option<String> {
let json = serde_json::to_string(self).expect("serde_json::to_string failed");
Some(json)
}
}
impl IntoFormText for InputMedia {
fn into_form_text(&self) -> Option<String> {
let json = serde_json::to_string(self).expect("serde_json::to_string failed");
Some(json)
}
}
impl IntoFormText for str {
fn into_form_text(&self) -> Option<String> {
Some(self.to_owned())
}
}
impl IntoFormText for ParseMode {
fn into_form_text(&self) -> Option<String> {
let string = match self {
ParseMode::MarkdownV2 => String::from("MarkdownV2"),
ParseMode::HTML => String::from("HTML"),
#[allow(deprecated)]
ParseMode::Markdown => String::from("Markdown"),
};
Some(string)
}
}
impl IntoFormText for ChatId {
fn into_form_text(&self) -> Option<String> {
let string = match self {
ChatId::Id(id) => id.to_string(),
ChatId::ChannelUsername(username) => username.clone(),
};
Some(string)
}
}
impl IntoFormText for String {
fn into_form_text(&self) -> Option<String> {
Some(self.clone())
}
}

View file

@ -1,7 +1,6 @@
//! API requests.
mod all;
mod form_builder;
mod utils;
pub use all::*;
@ -18,15 +17,3 @@ pub trait Request {
/// Asynchronously sends this request to Telegram and returns the result.
async fn send(&self) -> ResponseResult<Self::Output>;
}
/// Designates an API request with possibly sending file.
#[async_trait::async_trait]
pub trait RequestWithFile {
/// A data structure returned if success.
type Output;
/// Asynchronously sends this request to Telegram and returns the result.
/// Returns `tokio::io::Result::Err` when trying to send file which does not
/// exists.
async fn send(&self) -> tokio::io::Result<ResponseResult<Self::Output>>;
}

View file

@ -1,8 +1,5 @@
use std::{borrow::Cow, path::PathBuf};
use bytes::{Bytes, BytesMut};
use reqwest::{multipart::Part, Body};
use tokio_util::codec::{Decoder, FramedRead};
use tokio_util::codec::Decoder;
struct FileDecoder;
@ -17,15 +14,3 @@ impl Decoder for FileDecoder {
Ok(Some(src.split().freeze()))
}
}
pub async fn file_to_part(path_to_file: PathBuf) -> std::io::Result<Part> {
let file_name = path_to_file.file_name().unwrap().to_string_lossy().into_owned();
let file = FramedRead::new(tokio::fs::File::open(path_to_file).await?, FileDecoder);
Ok(Part::stream(Body::wrap_stream(file)).file_name(file_name))
}
pub fn file_from_memory_to_part(data: Cow<'static, [u8]>, name: String) -> Part {
Part::bytes(data).file_name(name)
}

View file

@ -1,7 +1,10 @@
use serde::Serialize;
use crate::types::InputFile;
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize)]
#[non_exhaustive]
#[serde(untagged)]
pub enum StickerType {
/// PNG image with the sticker, must be up to 512 kilobytes in size,
/// dimensions must not exceed 512px, and either width or height must be
@ -17,10 +20,10 @@ pub enum StickerType {
/// [`InputFile::FileId`]: crate::types::InputFile::FileId
///
/// [More info on Sending Files »]: https://core.telegram.org/bots/api#sending-files
Png(InputFile),
Png { png_sticker: InputFile },
/// TGS animation with the sticker, uploaded using multipart/form-data.
///
/// See https://core.telegram.org/animated_stickers#technical-requirements for technical requirements
Tgs(InputFile),
Tgs { tgs_sticker: InputFile },
}