use std::sync::Arc;
use std::time::Duration;
use crate::types::{Logger, SdkError};
pub struct WeComApiClient {
client: reqwest::Client,
logger: Arc<dyn Logger>,
}
impl WeComApiClient {
pub fn new(logger: Arc<dyn Logger>, timeout_ms: u64) -> Self {
let timeout = Duration::from_millis(timeout_ms);
let client = reqwest::Client::builder()
.timeout(timeout)
.build()
.unwrap_or_default();
Self { client, logger }
}
pub async fn download_file_raw(
&self,
url: &str,
) -> Result<(Vec<u8>, Option<String>), SdkError> {
self.logger.info("Downloading file...");
let response = self.client.get(url).send().await?;
let response = response.error_for_status()?;
let filename = response
.headers()
.get("Content-Disposition")
.and_then(|v| v.to_str().ok())
.and_then(Self::parse_filename);
let data = response.bytes().await?.to_vec();
self.logger.info("File downloaded successfully");
Ok((data, filename))
}
fn parse_filename(content_disposition: &str) -> Option<String> {
let utf8_pattern = regex::Regex::new(r"filename\*=UTF-8''([^;\s]+)").ok()?;
if let Some(caps) = utf8_pattern.captures(content_disposition) {
if let Some(encoded) = caps.get(1) {
return Some(
urlencoding::decode(encoded.as_str())
.unwrap_or_else(|_| encoded.as_str().to_string().into())
.to_string(),
);
}
}
let fallback_pattern = regex::Regex::new(r#"filename="?([^";\s]+)"?"#).ok()?;
if let Some(caps) = fallback_pattern.captures(content_disposition) {
if let Some(filename) = caps.get(1) {
return Some(
urlencoding::decode(filename.as_str())
.unwrap_or_else(|_| filename.as_str().to_string().into())
.to_string(),
);
}
}
None
}
pub async fn send_reply_to_response_url(
&self,
response_url: &str,
body: serde_json::Value,
) -> Result<(), SdkError> {
self.logger
.info(&format!("Sending reply to response_url: {}", response_url));
self.logger
.debug(&format!("Request body: {}", body));
let response = self
.client
.post(response_url)
.json(&body)
.send()
.await?;
let status = response.status();
self.logger
.debug(&format!("Response status: {}", status));
let response = response.error_for_status()?;
let result: serde_json::Value = response.json().await?;
self.logger
.debug(&format!("Response URL reply result: {:?}", result));
if let Some(code) = result.get("errcode").and_then(|v| v.as_i64()) {
if code != 0 {
let errmsg = result
.get("errmsg")
.and_then(|v| v.as_str())
.unwrap_or("unknown error");
return Err(SdkError::Http(format!(
"response_url reply failed: errcode={}, errmsg={}",
code, errmsg
)));
}
}
self.logger.info("Response URL reply sent successfully");
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_filename_simple() {
let result = WeComApiClient::parse_filename("attachment; filename=\"test.txt\"");
assert_eq!(result, Some("test.txt".to_string()));
}
#[test]
fn test_parse_filename_utf8() {
let result =
WeComApiClient::parse_filename("attachment; filename*=UTF-8''%E6%B5%8B%E8%AF%95.txt");
assert_eq!(result, Some("测试.txt".to_string()));
}
#[test]
fn test_parse_filename_no_quotes() {
let result = WeComApiClient::parse_filename("attachment; filename=test.txt");
assert_eq!(result, Some("test.txt".to_string()));
}
#[test]
fn test_parse_filename_none() {
let result = WeComApiClient::parse_filename("inline");
assert_eq!(result, None);
}
}