use std::fmt::Write as _;
use std::time::Duration;
use crate::api::client::HttpApiClient;
use crate::error::Result;
use crate::types::{
DEFAULT_ILINK_BOT_TYPE, DEFAULT_QR_POLL_TIMEOUT_MS, QrCodeResponse, QrStatusResponse,
};
#[derive(Debug, Clone)]
pub struct QrLoginSession {
pub qrcode: String,
pub qrcode_img_content: String,
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum LoginStatus {
Wait,
Scanned,
ScannedButRedirect {
redirect_host: String,
},
Confirmed {
bot_token: String,
ilink_bot_id: String,
base_url: String,
ilink_user_id: String,
},
Expired,
NeedVerifyCode,
VerifyCodeBlocked,
BindedRedirect,
}
pub struct QrLoginApi<'a> {
api: &'a HttpApiClient,
}
impl<'a> QrLoginApi<'a> {
pub(crate) fn new(api: &'a HttpApiClient) -> Self {
Self { api }
}
pub async fn start(
&self,
bot_type: Option<&str>,
local_tokens: &[String],
) -> Result<QrLoginSession> {
let bt = bot_type.unwrap_or(DEFAULT_ILINK_BOT_TYPE);
let endpoint = format!(
"ilink/bot/get_bot_qrcode?bot_type={}",
urlencoding::encode(bt)
);
let tokens: Vec<&str> = local_tokens.iter().take(10).map(String::as_str).collect();
let body = serde_json::json!({ "local_token_list": tokens });
let raw = self.api.api_post(&endpoint, &body, None).await?;
let resp: QrCodeResponse = serde_json::from_str(&raw)?;
Ok(QrLoginSession {
qrcode: resp.qrcode,
qrcode_img_content: resp.qrcode_img_content,
})
}
pub async fn poll_status(
&self,
session: &QrLoginSession,
verify_code: Option<&str>,
) -> Result<LoginStatus> {
let mut endpoint = format!(
"ilink/bot/get_qrcode_status?qrcode={}",
urlencoding::encode(&session.qrcode)
);
if let Some(code) = verify_code {
let _ = write!(endpoint, "&verify_code={}", urlencoding::encode(code));
}
let raw = match self
.api
.api_get(&endpoint, Duration::from_millis(DEFAULT_QR_POLL_TIMEOUT_MS))
.await
{
Ok(r) => r,
Err(crate::error::Error::Http(e)) if e.is_timeout() => {
return Ok(LoginStatus::Wait);
}
Err(e) => return Err(e),
};
let resp: QrStatusResponse = serde_json::from_str(&raw)?;
Ok(match resp.status.as_str() {
"scaned" => LoginStatus::Scanned,
"scaned_but_redirect" => LoginStatus::ScannedButRedirect {
redirect_host: resp.redirect_host.unwrap_or_default(),
},
"confirmed" => LoginStatus::Confirmed {
bot_token: resp.bot_token.unwrap_or_default(),
ilink_bot_id: resp.ilink_bot_id.unwrap_or_default(),
base_url: resp.baseurl.unwrap_or_default(),
ilink_user_id: resp.ilink_user_id.unwrap_or_default(),
},
"expired" => LoginStatus::Expired,
"need_verifycode" => LoginStatus::NeedVerifyCode,
"verify_code_blocked" => LoginStatus::VerifyCodeBlocked,
"binded_redirect" => LoginStatus::BindedRedirect,
_ => LoginStatus::Wait,
})
}
}
pub struct StandaloneQrLogin {
api: HttpApiClient,
}
impl StandaloneQrLogin {
pub fn new(config: &crate::config::WeixinConfig) -> Self {
Self {
api: HttpApiClient::new(config),
}
}
pub async fn start(
&self,
bot_type: Option<&str>,
local_tokens: &[String],
) -> Result<QrLoginSession> {
QrLoginApi::new(&self.api)
.start(bot_type, local_tokens)
.await
}
pub async fn poll_status(
&self,
session: &QrLoginSession,
verify_code: Option<&str>,
) -> Result<LoginStatus> {
QrLoginApi::new(&self.api)
.poll_status(session, verify_code)
.await
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_need_verify_code_status() {
let json = r#"{"status":"need_verifycode"}"#;
let resp: QrStatusResponse = serde_json::from_str(json).unwrap();
assert_eq!(resp.status, "need_verifycode");
}
#[test]
fn parse_verify_code_blocked_status() {
let json = r#"{"status":"verify_code_blocked"}"#;
let resp: QrStatusResponse = serde_json::from_str(json).unwrap();
assert_eq!(resp.status, "verify_code_blocked");
}
#[test]
fn parse_binded_redirect_status() {
let json = r#"{"status":"binded_redirect"}"#;
let resp: QrStatusResponse = serde_json::from_str(json).unwrap();
assert_eq!(resp.status, "binded_redirect");
}
#[test]
fn verify_code_appended_to_endpoint() {
let qrcode = "test_qr";
let mut endpoint = format!(
"ilink/bot/get_qrcode_status?qrcode={}",
urlencoding::encode(qrcode)
);
let verify_code = Some("1234");
if let Some(code) = verify_code {
let _ = write!(endpoint, "&verify_code={}", urlencoding::encode(code));
}
assert!(endpoint.contains("verify_code=1234"));
assert!(endpoint.contains("qrcode=test_qr"));
}
}