use reqwest::Client;
use std::time::Duration;
use crate::crypto;
use crate::error::{Result, WechatIlinkError};
use crate::protocol::CDN_BASE_URL;
use crate::types::CDNMedia;
#[derive(Debug, Clone)]
pub struct CdnClient {
http: Client,
base_url: String,
}
impl Default for CdnClient {
fn default() -> Self {
Self::new()
}
}
impl CdnClient {
pub fn new() -> Self {
Self::with_client(Client::new())
}
pub fn with_client(http: Client) -> Self {
Self {
http,
base_url: CDN_BASE_URL.to_string(),
}
}
pub fn with_base_url(mut self, base_url: impl Into<String>) -> Self {
self.base_url = base_url.into();
self
}
pub async fn download(
&self,
media: &CDNMedia,
aes_key_override: Option<&str>,
) -> Result<Vec<u8>> {
let download_url = if let Some(full_url) = &media.full_url {
full_url.clone()
} else {
let encrypted_query_param = media.encrypt_query_param.as_deref().ok_or_else(|| {
WechatIlinkError::Media("CDN media missing encrypted query param".into())
})?;
format!(
"{}/download?encrypted_query_param={}",
self.base_url,
urlencoding::encode(encrypted_query_param)
)
};
let resp = self
.http
.get(&download_url)
.timeout(Duration::from_secs(60))
.send()
.await?;
if !resp.status().is_success() {
return Err(WechatIlinkError::Media(format!(
"CDN download failed: HTTP {}",
resp.status()
)));
}
let ciphertext = resp.bytes().await?.to_vec();
let key_source = aes_key_override.or(media.aes_key.as_deref());
let Some(key_source) = key_source.filter(|key| !key.is_empty()) else {
return Ok(ciphertext);
};
let aes_key = crypto::decode_aes_key(key_source)?;
crypto::decrypt_aes_ecb(&ciphertext, &aes_key)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_and_new_equivalent() {
let a = CdnClient::default();
let b = CdnClient::new();
assert_eq!(a.base_url, b.base_url);
}
#[test]
fn with_base_url_overrides() {
let c = CdnClient::new().with_base_url("https://example.test/cdn");
assert_eq!(c.base_url, "https://example.test/cdn");
}
#[test]
fn clone_is_cheap_and_preserves_config() {
let c = CdnClient::new().with_base_url("https://x.y/z");
let cloned = c.clone();
assert_eq!(c.base_url, cloned.base_url);
}
}