use serde::Deserialize;
use serde_json::Value;
use std::borrow::Cow;
use std::io::Read;
use std::time::Instant;
use crate::core::error::{AppError, Result};
pub const MAX_DOWNLOAD_BYTES: u64 = 128 * 1024 * 1024;
pub const MAX_BASE64_BYTES: usize = MAX_DOWNLOAD_BYTES.div_ceil(3) as usize * 4;
use crate::transport::client::{Action, OneBotClient};
pub struct Bytes {
pub encoded: String,
pub size: Option<u64>,
}
#[derive(Deserialize)]
struct Envelope<'a> {
#[serde(borrow)]
status: Cow<'a, str>,
retcode: i64,
#[serde(borrow)]
data: Option<Data<'a>>,
}
#[derive(Deserialize)]
struct Data<'a> {
#[serde(borrow)]
base64: Option<Cow<'a, str>>,
#[serde(borrow)]
file_size: Option<Cow<'a, str>>,
}
pub(crate) fn fetch(
client: &OneBotClient,
action: Action,
payload: &Value,
deadline: Instant,
) -> Result<Bytes> {
if !matches!(action, Action::File | Action::Image) {
return Err(AppError::invalid());
}
let limit = MAX_BASE64_BYTES + client.response_limit();
let response = client.response(action, payload, deadline)?;
if response
.content_length()
.is_some_and(|length| length > limit as u64)
{
return Err(limit_error());
}
let mut body = Vec::new();
response
.take(limit as u64 + 1)
.read_to_end(&mut body)
.map_err(|_| AppError::new("BACKEND_TRANSPORT", "Media response was interrupted.", 3))?;
if body.len() > limit {
return Err(limit_error());
}
from_slice(&body, client.response_limit())
}
pub(crate) fn from_slice(body: &[u8], metadata_limit: usize) -> Result<Bytes> {
if body.len() > MAX_BASE64_BYTES + metadata_limit {
return Err(limit_error());
}
let envelope: Envelope<'_> = serde_json::from_slice(body).map_err(|_| AppError::protocol())?;
crate::transport::client::final_status(&envelope.status, envelope.retcode)?;
let data = envelope.data.ok_or_else(AppError::protocol)?;
let encoded = data.base64.ok_or_else(unavailable)?;
if encoded.len() > MAX_BASE64_BYTES || body.len().saturating_sub(encoded.len()) > metadata_limit
{
return Err(limit_error());
}
let size = size(data.file_size.as_deref())?;
Ok(Bytes {
encoded: encoded.into_owned(),
size,
})
}
pub fn from_value(mut value: Value) -> Result<Bytes> {
let encoded = match value.get_mut("base64").map(Value::take) {
Some(Value::String(value)) => value,
_ => return Err(unavailable()),
};
if encoded.len() > MAX_BASE64_BYTES
|| serde_json::to_vec(&value)
.map_err(|_| AppError::protocol())?
.len()
> 4 * 1024 * 1024
{
return Err(limit_error());
}
let size = match value.get("file_size") {
None | Some(Value::Null) => None,
Some(Value::String(value)) => size(Some(value))?,
_ => return Err(AppError::protocol()),
};
Ok(Bytes { encoded, size })
}
fn size(value: Option<&str>) -> Result<Option<u64>> {
match value {
None | Some("") => Ok(None),
Some(value) => value
.parse::<u64>()
.ok()
.filter(|value| *value <= MAX_DOWNLOAD_BYTES)
.map(Some)
.ok_or_else(AppError::protocol),
}
}
fn unavailable() -> AppError {
let mut error = AppError::new(
"MEDIA_BYTES_UNAVAILABLE",
"Backend did not provide media bytes; inspect its managed media capability.",
4,
);
error.next_action = Some(Box::new(
crate::core::contracts::action::NextAction::Doctor { instance: None },
));
error
}
fn limit_error() -> AppError {
AppError::new(
"RESPONSE_LIMIT",
"Media response exceeded its dedicated byte or metadata budget.",
3,
)
}