use super::base::{ClientResponse, Session, DEFAULT_TIMEOUT};
use crate::{
client::{telegram, Bot},
methods::TelegramMethod,
serializers::reqwest::{Error as SerializerError, MultipartSerializer},
types::InputFile,
utils::format_error_report,
};
use reqwest::{
multipart::{Form, Part},
Body, Client, ClientBuilder,
};
use serde::Serialize;
use std::{borrow::Cow, time::Duration};
use tracing::{event, field, instrument, Level, Span};
#[derive(Debug, Clone)]
pub struct Reqwest {
client: Client,
api: Cow<'static, telegram::APIServer>,
}
impl Reqwest {
#[must_use]
pub fn new(client: Client) -> Self {
Self {
client,
api: Cow::Borrowed(&telegram::PRODUCTION),
}
}
#[must_use]
pub fn with_api_server(self, api: impl Into<Cow<'static, telegram::APIServer>>) -> Self {
Self {
api: api.into(),
..self
}
}
#[instrument(skip(self, data))]
async fn build_form_data<Data: Serialize>(
&self,
data: Data,
files: Option<Vec<InputFile>>,
) -> Result<Form, SerializerError> {
let mut form = data
.serialize(MultipartSerializer::new())
.inspect_err(|err| {
event!(
Level::ERROR,
error = format_error_report(&err),
"Cannot build a form"
);
})?;
let Some(files) = files else {
return Ok(form);
};
for file in files {
match file {
InputFile::FS(file) => {
let id = file.id.to_string();
let file_name = file.file_name().map(ToOwned::to_owned);
let body = Body::wrap_stream(file.stream());
let part = if let Some(file_name) = file_name {
Part::stream(body).file_name(file_name)
} else {
Part::stream(body).file_name(id.clone())
};
form = form.part(id, part);
}
InputFile::Buffered(file) => {
let id = file.id.to_string();
let part = if let Some(file_name) = file.file_name {
Part::bytes(file.bytes.to_vec()).file_name(file_name.clone())
} else {
Part::bytes(file.bytes.to_vec()).file_name(id.clone())
};
form = form.part(id, part);
}
InputFile::Stream(file) => {
let id = file.id.to_string();
let body = Body::wrap_stream(file.stream.expect("file stream is empty"));
let part = if let Some(file_name) = file.file_name {
Part::stream(body).file_name(file_name)
} else {
Part::stream(body).file_name(id.clone())
};
form = form.part(id, part);
}
InputFile::Id(_) | InputFile::Url(_) => {}
}
}
Ok(form)
}
}
impl Default for Reqwest {
fn default() -> Self {
Self {
client: ClientBuilder::new()
.timeout(Duration::from_secs_f32(DEFAULT_TIMEOUT))
.build()
.unwrap(),
api: Cow::Borrowed(&telegram::PRODUCTION),
}
}
}
impl Session for Reqwest {
fn api(&self) -> &telegram::APIServer {
&self.api
}
#[instrument(name = "send", skip_all, fields(files, method_name, timeout))]
async fn send_request<Client, T>(
&self,
bot: &Bot<Client>,
method: T,
timeout: Option<f32>,
) -> Result<ClientResponse, anyhow::Error>
where
Client: Session,
T: TelegramMethod + Send + Sync,
T::Method: Send + Sync,
{
let req = method.build_request(bot);
Span::current()
.record("files", field::debug(&req.files))
.record("method_name", req.method_name);
let form = self.build_form_data(req.data, req.files).await?;
let url = self.api.api_url(&bot.token, req.method_name);
let response = if let Some(timeout) = timeout {
Span::current().record("timeout", timeout);
self.client
.post(url.as_ref())
.multipart(form)
.timeout(Duration::from_secs_f32(timeout))
} else {
self.client.post(url.as_ref()).multipart(form)
}
.send()
.await
.map_err(|err| {
if err.is_timeout() {
event!(Level::WARN, error = %err, "Request timed out",);
} else {
event!(
Level::ERROR,
error = format_error_report(&err),
"Cannot send a request",
);
}
err
})?;
let status_code = response.status().as_u16();
let content = response.text().await.inspect_err(|err| {
event!(
Level::ERROR,
error = format_error_report(&err),
status_code,
"Cannot get a response content",
);
})?;
Ok(ClientResponse::new(status_code, content))
}
}