use anyhow::{anyhow, Result};
use reqwest::Client;
use std::time::Duration;
use tokio::sync::mpsc::UnboundedSender;
use tracing::{info, warn};
use super::types::*;
#[derive(Clone, serde::Serialize)]
#[serde(tag = "kind")]
pub enum QrLoginUiEvent {
#[serde(rename = "ready")]
Ready { image: String, link: String },
#[serde(rename = "status")]
Status { message: String },
#[serde(rename = "done")]
Done,
}
pub struct LoginClient {
client: Client,
base_url: String,
}
impl LoginClient {
pub fn new(base_url: Option<String>) -> Self {
Self {
client: Client::builder()
.timeout(Duration::from_secs(30))
.build()
.expect("http client"),
base_url: base_url.unwrap_or_else(|| ILINK_BASE_URL.to_string()),
}
}
pub async fn login_with_qr(&self) -> Result<String> {
self.login_with_qr_ui(None).await
}
pub async fn login_with_qr_ui(
&self,
ui: Option<UnboundedSender<QrLoginUiEvent>>,
) -> Result<String> {
info!("Starting iLink QR login");
let qr_resp = self.get_qrcode().await?;
let key = qr_resp
.qrcode
.ok_or_else(|| anyhow!("no qrcode key in response"))?;
let qr_url = qr_resp
.qrcode_img_content
.ok_or_else(|| anyhow!("no qrcode URL in response"))?;
if let Some(ref tx) = ui {
let image = crate::client::pairing::encode_qr_svg_data_uri(&qr_url)?;
let _ = tx.send(QrLoginUiEvent::Ready {
image,
link: qr_url.clone(),
});
} else {
println!("\n╔══════════════════════════════════════╗");
println!("║ WeChat ClawBot Login ║");
println!("╚══════════════════════════════════════╝");
println!();
crate::client::pairing::render_qr_terminal(&qr_url)?;
println!();
println!("Scan the QR code with WeChat to log in.");
println!("QR URL: {}", qr_url);
println!();
}
let out = self.poll_qrcode_status(&key, ui.as_ref()).await;
if let Some(tx) = &ui {
let _ = tx.send(QrLoginUiEvent::Done);
}
out
}
async fn get_qrcode(&self) -> Result<GetQrcodeResponse> {
let url = format!("{}/ilink/bot/get_bot_qrcode?bot_type=3", self.base_url);
let resp = self
.client
.get(&url)
.send()
.await?
.json::<GetQrcodeResponse>()
.await?;
if resp.ret != 0 {
return Err(anyhow!(
"get_bot_qrcode failed: {}",
resp.errmsg.as_deref().unwrap_or("unknown")
));
}
Ok(resp)
}
async fn poll_qrcode_status(
&self,
key: &str,
ui: Option<&UnboundedSender<QrLoginUiEvent>>,
) -> Result<String> {
let url = format!(
"{}/ilink/bot/get_qrcode_status?qrcode={}",
self.base_url, key
);
let mut attempts = 0u32;
const MAX_ATTEMPTS: u32 = 120;
loop {
if attempts >= MAX_ATTEMPTS {
return Err(anyhow!("QR login timed out (120s)"));
}
attempts += 1;
tokio::time::sleep(Duration::from_secs(1)).await;
let resp = match self
.client
.get(&url)
.send()
.await?
.json::<QrcodeStatusResponse>()
.await
{
Ok(r) => r,
Err(e) => {
warn!(error = %e, "error polling qrcode status");
continue;
}
};
match resp.status.as_deref() {
Some("wait") | None => {}
Some("scaned") | Some("scanned") => {
info!("QR code scanned, waiting for confirmation...");
if let Some(tx) = ui {
let _ = tx.send(QrLoginUiEvent::Status {
message: "已在手机上扫码,请在微信里确认登录".into(),
});
}
}
Some("confirmed") => {
if let Some(token) = resp.bot_token {
info!("QR login successful");
return Ok(token);
}
return Err(anyhow!("login confirmed but no bot_token in response"));
}
Some("expired") => {
return Err(anyhow!("QR code expired, please run login again"));
}
Some(status) => {
if resp.ret != 0 {
return Err(anyhow!(
"qrcode status error: {}",
resp.errmsg.as_deref().unwrap_or(status)
));
}
warn!(status, "unknown qrcode status, continuing to poll");
}
}
}
}
}