use std::time::Duration;
use serde::{Deserialize, Serialize};
use crate::bot::BotCredential;
use crate::error::AuthError;
const SOURCE: &str = "wecom_cli_external";
const QR_GENERATE_URL: &str = "https://work.weixin.qq.com/ai/qc/generate";
const QR_QUERY_URL: &str = "https://work.weixin.qq.com/ai/qc/query_result";
const QR_CODE_PAGE: &str = "https://work.weixin.qq.com/ai/qc/gen";
const POLL_INTERVAL: Duration = Duration::from_secs(3);
const POLL_TIMEOUT: Duration = Duration::from_secs(300);
#[derive(Debug, Clone)]
pub struct QrSession {
pub scode: String,
pub auth_url: String,
pub page_url: String,
}
fn qr_network_error(message: &str, endpoint: &str, source: reqwest::Error) -> AuthError {
wecomx_transport::Error::Network {
message: message.to_string(),
endpoint: endpoint.to_string(),
source,
}
.into()
}
impl QrSession {
pub async fn create() -> Result<Self, AuthError> {
let url = format!(
"{}?source={}&plat={}",
QR_GENERATE_URL,
SOURCE,
get_plat_code()
);
let response: GenerateResponse = reqwest::Client::new()
.get(&url)
.send()
.await
.map_err(|e| qr_network_error("获取二维码失败", &url, e))?
.json()
.await
.map_err(|e| qr_network_error("获取二维码失败,响应格式异常", &url, e))?;
let Some(data) = response.data else {
return Err(protocol_error(
"获取二维码失败,响应格式异常",
&url,
serde_json::to_value(response).unwrap_or_default(),
));
};
let (Some(scode), Some(auth_url)) = (&data.scode, &data.auth_url) else {
return Err(protocol_error(
"获取二维码失败,响应格式异常",
&url,
serde_json::to_value(data).unwrap_or_default(),
));
};
let page_url = format!("{}?source={}&scode={}", QR_CODE_PAGE, SOURCE, scode);
tracing::debug!("qr session created");
Ok(Self {
scode: scode.to_string(),
auth_url: auth_url.to_string(),
page_url,
})
}
pub async fn poll(&self) -> Result<BotCredential, AuthError> {
let url = format!("{}?scode={}", QR_QUERY_URL, self.scode);
let client = reqwest::Client::new();
let start = std::time::Instant::now();
tracing::debug!(
timeout_secs = POLL_TIMEOUT.as_secs(),
"polling qr scan status"
);
loop {
if start.elapsed() >= POLL_TIMEOUT {
tracing::debug!("qr scan polling timed out");
return Err(AuthError::QrTimeout);
}
let response: QueryResponse = client
.get(&url)
.send()
.await
.map_err(|e| qr_network_error("查询扫码结果失败", &url, e))?
.json()
.await
.map_err(|e| qr_network_error("查询扫码结果失败,响应格式异常", &url, e))?;
if let Some(data) = &response.data
&& data.status.as_deref() == Some("success")
{
let Some(bot_info) = &data.bot_info else {
return Err(protocol_error(
"扫码成功但未获取到 Bot 信息",
&url,
serde_json::Value::Null,
));
};
let (Some(botid), Some(secret)) = (&bot_info.botid, &bot_info.secret) else {
return Err(protocol_error(
"扫码成功但未获取到 Bot 信息",
&url,
serde_json::Value::Null,
));
};
tracing::info!("qr code scanned, bot bound");
return Ok(BotCredential::new(botid.to_string(), secret.to_string()));
}
tokio::time::sleep(POLL_INTERVAL).await;
}
}
}
fn protocol_error(message: &str, endpoint: &str, body: serde_json::Value) -> AuthError {
wecomx_transport::Error::Parse {
message: message.to_string(),
endpoint: endpoint.to_string(),
body: Box::new(body),
source: None,
}
.into()
}
fn get_plat_code() -> u8 {
if cfg!(target_os = "macos") {
1
} else if cfg!(target_os = "windows") {
2
} else if cfg!(target_os = "linux") {
3
} else {
0
}
}
#[derive(Serialize, Deserialize)]
struct GenerateResponse {
data: Option<GenerateData>,
}
#[derive(Serialize, Deserialize)]
struct GenerateData {
scode: Option<String>,
auth_url: Option<String>,
}
#[derive(Serialize, Deserialize)]
struct QueryResponse {
data: Option<QueryData>,
}
#[derive(Serialize, Deserialize)]
struct QueryData {
status: Option<String>,
bot_info: Option<BotInfoPayload>,
}
#[derive(Serialize, Deserialize)]
struct BotInfoPayload {
botid: Option<String>,
secret: Option<String>,
}