botserver/src/channels/wechat/client.rs
Rodrigo Rodriguez 5ea171d126
Some checks failed
BotServer CI / build (push) Failing after 1m34s
Refactor: Split large files into modular subdirectories
Split 20+ files over 1000 lines into focused subdirectories for better
maintainability and code organization. All changes maintain backward
compatibility through re-export wrappers.

Major splits:
- attendance/llm_assist.rs (2074→7 modules)
- basic/keywords/face_api.rs → face_api/ (7 modules)
- basic/keywords/file_operations.rs → file_ops/ (8 modules)
- basic/keywords/hear_talk.rs → hearing/ (6 modules)
- channels/wechat.rs → wechat/ (10 modules)
- channels/youtube.rs → youtube/ (5 modules)
- contacts/mod.rs → contacts_api/ (6 modules)
- core/bootstrap/mod.rs → bootstrap/ (5 modules)
- core/shared/admin.rs → admin_*.rs (5 modules)
- designer/canvas.rs → canvas_api/ (6 modules)
- designer/mod.rs → designer_api/ (6 modules)
- docs/handlers.rs → handlers_api/ (11 modules)
- drive/mod.rs → drive_handlers.rs, drive_types.rs
- learn/mod.rs → types.rs
- main.rs → main_module/ (7 modules)
- meet/webinar.rs → webinar_api/ (8 modules)
- paper/mod.rs → (10 modules)
- security/auth.rs → auth_api/ (7 modules)
- security/passkey.rs → (4 modules)
- sources/mod.rs → sources_api/ (5 modules)
- tasks/mod.rs → task_api/ (5 modules)

Stats: 38,040 deletions, 1,315 additions across 318 files

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-12 21:09:30 +00:00

140 lines
4.3 KiB
Rust

//! WeChat API client implementation
use super::types::{AccessTokenResponse, CachedToken, WeChatApiResponse};
use crate::channels::ChannelError;
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
/// WeChat API provider for Official Accounts and Mini Programs
pub struct WeChatProvider {
pub(crate) client: reqwest::Client,
pub(crate) api_base_url: String,
/// Cache for access tokens (app_id -> token info)
pub(crate) token_cache: Arc<RwLock<HashMap<String, CachedToken>>>,
}
impl WeChatProvider {
pub fn new() -> Self {
Self {
client: reqwest::Client::new(),
api_base_url: "https://api.weixin.qq.com".to_string(),
token_cache: Arc::new(RwLock::new(HashMap::new())),
}
}
/// Get access token (with caching)
pub async fn get_access_token(
&self,
app_id: &str,
app_secret: &str,
) -> Result<String, ChannelError> {
// Check cache first
{
let cache = self.token_cache.read().await;
if let Some(cached) = cache.get(app_id) {
if cached.expires_at > chrono::Utc::now() + chrono::Duration::minutes(5) {
return Ok(cached.access_token.clone());
}
}
}
// Fetch new token
let url = format!(
"{}/cgi-bin/token?grant_type=client_credential&appid={}&secret={}",
self.api_base_url, app_id, app_secret
);
let response = self
.client
.get(&url)
.send()
.await
.map_err(|e| ChannelError::NetworkError(e.to_string()))?;
if !response.status().is_success() {
return Err(self.parse_error_response(response).await);
}
let token_response: AccessTokenResponse =
response.json().await.map_err(|e| ChannelError::ApiError {
code: None,
message: e.to_string(),
})?;
if let Some(errcode) = token_response.errcode {
if errcode != 0 {
return Err(ChannelError::ApiError {
code: Some(errcode.to_string()),
message: token_response.errmsg.unwrap_or_default(),
});
}
}
let access_token = token_response.access_token.ok_or_else(|| {
ChannelError::ApiError {
code: None,
message: "No access token in response".to_string(),
}
})?;
let expires_in = token_response.expires_in.unwrap_or(7200);
let expires_at = chrono::Utc::now() + chrono::Duration::seconds(expires_in as i64);
// Cache the token
{
let mut cache = self.token_cache.write().await;
cache.insert(
app_id.to_string(),
CachedToken {
access_token: access_token.clone(),
expires_at,
},
);
}
Ok(access_token)
}
pub(crate) fn check_error<T>(&self, response: &WeChatApiResponse<T>) -> Result<(), ChannelError> {
if let Some(errcode) = response.errcode {
if errcode != 0 {
return Err(ChannelError::ApiError {
code: Some(errcode.to_string()),
message: response.errmsg.clone().unwrap_or_default(),
});
}
}
Ok(())
}
pub(crate) async fn parse_error_response(&self, response: reqwest::Response) -> ChannelError {
let status = response.status();
if status.as_u16() == 401 {
return ChannelError::AuthenticationFailed("Invalid credentials".to_string());
}
let error_text = response.text().await.unwrap_or_default();
if let Ok(api_response) = serde_json::from_str::<WeChatApiResponse<()>>(&error_text) {
if let Some(errcode) = api_response.errcode {
return ChannelError::ApiError {
code: Some(errcode.to_string()),
message: api_response.errmsg.unwrap_or_default(),
};
}
}
ChannelError::ApiError {
code: Some(status.to_string()),
message: error_text,
}
}
}
impl Default for WeChatProvider {
fn default() -> Self {
Self::new()
}
}