use reqwest::Client;
use std::time::Duration;
use crate::crypto;
use crate::error::{Result, WeChatBotError};
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 = format!(
"{}/download?encrypted_query_param={}",
self.base_url,
urlencoding::encode(&media.encrypt_query_param)
);
let resp = self
.http
.get(&download_url)
.timeout(Duration::from_secs(60))
.send()
.await?;
if !resp.status().is_success() {
return Err(WeChatBotError::Media(format!(
"CDN download failed: HTTP {}",
resp.status()
)));
}
let ciphertext = resp.bytes().await?.to_vec();
let key_source = aes_key_override.unwrap_or(&media.aes_key);
if key_source.is_empty() {
return Err(WeChatBotError::Media("no AES key available".into()));
}
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);
}
}