use std::fmt;
use async_trait::async_trait;
use crate::BotError;
#[derive(Debug)]
pub struct FormPart {
pub name: String,
pub body: FormBody,
}
impl FormPart {
pub fn text(name: impl Into<String>, value: impl Into<String>) -> Self {
Self {
name: name.into(),
body: FormBody::Text(value.into()),
}
}
pub fn bytes(
name: impl Into<String>,
filename: impl Into<String>,
mime: impl Into<String>,
data: bytes::Bytes,
) -> Self {
Self {
name: name.into(),
body: FormBody::Bytes {
filename: filename.into(),
mime: mime.into(),
data,
},
}
}
}
#[derive(Debug)]
pub enum FormBody {
Text(String),
Bytes {
filename: String,
mime: String,
data: bytes::Bytes,
},
}
#[async_trait]
pub trait BotClient: Send + Sync + fmt::Debug {
async fn post_json(&self, url: &str, body: serde_json::Value)
-> Result<bytes::Bytes, BotError>;
async fn post_form(&self, url: &str, parts: Vec<FormPart>) -> Result<bytes::Bytes, BotError>;
}
#[derive(Debug, Clone)]
pub struct ReqwestClient {
pub(crate) inner: reqwest::Client,
}
impl ReqwestClient {
pub fn with_timeout(timeout: std::time::Duration) -> Result<Self, BotError> {
let inner = reqwest::Client::builder()
.timeout(timeout)
.build()
.map_err(BotError::Http)?;
Ok(Self { inner })
}
}
#[async_trait]
impl BotClient for ReqwestClient {
async fn post_json(
&self,
url: &str,
body: serde_json::Value,
) -> Result<bytes::Bytes, BotError> {
self.inner
.post(url)
.json(&body)
.send()
.await
.map_err(BotError::Http)?
.bytes()
.await
.map_err(BotError::Http)
}
async fn post_form(&self, url: &str, parts: Vec<FormPart>) -> Result<bytes::Bytes, BotError> {
let mut form = reqwest::multipart::Form::new();
for part in parts {
match part.body {
FormBody::Text(text) => {
form = form.text(part.name, text);
}
FormBody::Bytes { .. } => {
#[cfg(target_arch = "wasm32")]
return Err(BotError::Other(
"file uploads are not supported on WASM; use file_id or a URL".into(),
));
#[cfg(not(target_arch = "wasm32"))]
{
let FormBody::Bytes {
filename,
mime,
data,
} = part.body
else {
unreachable!()
};
let rpart = reqwest::multipart::Part::bytes(data.to_vec())
.file_name(filename)
.mime_str(&mime)
.map_err(|e| BotError::Other(e.to_string()))?;
form = form.part(part.name, rpart);
}
}
}
}
self.inner
.post(url)
.multipart(form)
.send()
.await
.map_err(BotError::Http)?
.bytes()
.await
.map_err(BotError::Http)
}
}