use std::fmt::Write;
use std::time::Duration;
use bytes::BytesMut;
use reqwest::Url;
use salvo::oapi::{ToParameters, ToSchema};
use salvo::prelude::*;
use serde::{Deserialize, Serialize};
use crate::http_headers::ContentDisposition;
use crate::media::Method;
use crate::sending::{SendRequest, SendResult};
const MULTIPART_MIXED: &str = "multipart/mixed";
const MAX_HEADERS_COUNT: usize = 32;
const GENERATED_BOUNDARY_LENGTH: usize = 30;
pub fn thumbnail_request(origin: &str, args: ThumbnailReqArgs) -> SendResult<SendRequest> {
let mut url = Url::parse(&format!(
"{origin}/_matrix/federation/v1/media/thumbnail/{}",
args.media_id
))?;
{
let mut query = url.query_pairs_mut();
query.append_pair("width", &args.width.to_string());
query.append_pair("height", &args.height.to_string());
query.append_pair("timeout_ms", &args.timeout_ms.as_millis().to_string());
}
Ok(crate::sending::get(url))
}
#[derive(ToParameters, Deserialize, Debug)]
pub struct ThumbnailReqArgs {
#[salvo(parameter(parameter_in = Path))]
pub media_id: String,
#[salvo(parameter(parameter_in = Query))]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub method: Option<Method>,
#[salvo(parameter(parameter_in = Query))]
pub width: u32,
#[salvo(parameter(parameter_in = Query))]
pub height: u32,
#[salvo(parameter(parameter_in = Query))]
#[serde(
with = "crate::serde::duration::ms",
default = "crate::client::media::default_download_timeout",
skip_serializing_if = "crate::media::is_default_download_timeout"
)]
pub timeout_ms: Duration,
#[salvo(parameter(parameter_in = Query))]
#[serde(skip_serializing_if = "Option::is_none")]
pub animated: Option<bool>,
}
#[derive(ToSchema, Debug)]
pub struct ThumbnailResBody {
pub metadata: ContentMetadata,
pub content: FileOrLocation,
}
impl Scribe for ThumbnailResBody {
fn render(self, res: &mut Response) {
use rand::Rng as _;
let boundary = rand::thread_rng()
.sample_iter(&rand::distr::Alphanumeric)
.map(char::from)
.take(GENERATED_BOUNDARY_LENGTH)
.collect::<String>();
let mut body_writer = BytesMut::new();
let _ = write!(
body_writer,
"\r\n--{boundary}\r\n{}: {}\r\n\r\n",
http::header::CONTENT_TYPE,
mime::APPLICATION_JSON
);
match serde_json::to_vec(&self.metadata) {
Ok(bytes) => {
body_writer.extend_from_slice(&bytes);
}
Err(e) => {
tracing::error!("Failed to serialize metadata: {}", e);
res.render(StatusError::internal_server_error().brief("Failed to serialize metadata"));
return;
}
}
let _ = write!(body_writer, "\r\n--{boundary}\r\n");
match self.content {
FileOrLocation::File(content) => {
let content_type = content
.content_type
.as_deref()
.unwrap_or(mime::APPLICATION_OCTET_STREAM.as_ref());
let _ = write!(body_writer, "{}: {content_type}\r\n", http::header::CONTENT_TYPE);
if let Some(content_disposition) = &content.content_disposition {
let _ = write!(
body_writer,
"{}: {content_disposition}\r\n",
http::header::CONTENT_DISPOSITION
);
}
body_writer.extend_from_slice(b"\r\n");
body_writer.extend_from_slice(&content.file);
}
FileOrLocation::Location(location) => {
let _ = write!(body_writer, "{}: {location}\r\n\r\n", http::header::LOCATION);
}
}
let _ = write!(body_writer, "\r\n--{boundary}--");
let content_type = format!("{MULTIPART_MIXED}; boundary={boundary}");
let _ = res.add_header(http::header::CONTENT_TYPE, content_type, true);
if let Err(e) = res.write_body(body_writer) {
res.render(StatusError::internal_server_error().brief("Failed to set response body"));
tracing::error!("Failed to set response body: {}", e);
}
}
}
pub fn content_request(origin: &str, args: ContentReqArgs) -> SendResult<SendRequest> {
let url = Url::parse(&format!(
"{origin}/_matrix/federation/v1/media/download/{}?timeout_ms={}",
args.media_id,
args.timeout_ms.as_millis()
))?;
Ok(crate::sending::get(url))
}
#[derive(ToParameters, Deserialize, Debug)]
pub struct ContentReqArgs {
#[salvo(parameter(parameter_in = Path))]
pub media_id: String,
#[salvo(parameter(parameter_in = Query))]
#[serde(
with = "crate::serde::duration::ms",
default = "crate::client::media::default_download_timeout",
skip_serializing_if = "crate::client::media::is_default_download_timeout"
)]
pub timeout_ms: Duration,
}
#[derive(ToSchema, Serialize, Debug)]
pub struct ContentResBody {
pub metadata: ContentMetadata,
pub content: FileOrLocation,
}
#[derive(ToSchema, Serialize, Debug, Clone)]
pub enum FileOrLocation {
File(Content),
Location(String),
}
#[derive(ToSchema, Serialize, Debug, Clone)]
pub struct Content {
pub file: Vec<u8>,
pub content_type: Option<String>,
pub content_disposition: Option<ContentDisposition>,
}
#[derive(ToSchema, Serialize, Deserialize, Debug, Clone, Default)]
pub struct ContentMetadata {}
impl ContentMetadata {
pub fn new() -> Self {
Self {}
}
}