use clap::Parser;
use crabchat::{GatewayConfig, config::WechatConfig};
const DEFAULT_BASE_URL: &str = "https://ilinkai.weixin.qq.com";
#[crabtalk_command::command(kind = "client", label = "ai.crabtalk.gateway-wechat")]
struct GatewayWechat;
impl GatewayWechat {
async fn run(&self) -> anyhow::Result<()> {
let socket = wcore::paths::SOCKET_PATH.clone();
let config_path = wcore::paths::CONFIG_DIR.join("gateway.toml");
let config = if config_path.exists() {
GatewayConfig::load(&config_path)?
} else {
GatewayConfig::default()
};
crabchat::serve::run(&socket.to_string_lossy(), &config).await
}
}
#[derive(Parser)]
#[command(name = "crabtalk-wechat", about = "Crabtalk WeChat gateway")]
struct App {
#[command(subcommand)]
action: GatewayWechatCommand,
}
fn config_path() -> std::path::PathBuf {
wcore::paths::CONFIG_DIR.join("gateway.toml")
}
fn ensure_config() -> anyhow::Result<()> {
let path = config_path();
let mut config = if path.exists() {
GatewayConfig::load(&path)?
} else {
GatewayConfig::default()
};
if config.wechat.as_ref().is_none_or(|w| w.token.is_empty()) {
let rt = tokio::runtime::Runtime::new()?;
let (token, base_url) = rt.block_on(qr_login())?;
config.wechat = Some(WechatConfig {
token,
base_url,
allowed_users: vec![],
});
config.save(&path)?;
println!("saved config to {}", path.display());
}
Ok(())
}
async fn qr_login() -> anyhow::Result<(String, String)> {
let client = reqwest::Client::new();
let base_url = DEFAULT_BASE_URL;
println!("Fetching QR code for WeChat login...");
let qr = crabchat::api::fetch_qrcode(&client, base_url).await?;
println!("\nScan this QR code with WeChat:\n");
qr2term::print_qr(&qr.qrcode_img_content)?;
println!();
println!("Waiting for scan...");
let mut scanned = false;
loop {
let status = crabchat::api::poll_qr_status(&client, base_url, &qr.qrcode).await?;
match status.status.as_str() {
"wait" => {}
"scaned" | "scanned" => {
if !scanned {
println!("Scanned! Confirm on your phone...");
scanned = true;
}
}
"confirmed" => {
let token = status
.bot_token
.ok_or_else(|| anyhow::anyhow!("confirmed but no bot_token"))?;
let url = status.baseurl.unwrap_or_else(|| base_url.to_string());
println!("Connected!");
return Ok((token, url));
}
"expired" => {
anyhow::bail!("QR code expired, please try again");
}
other => {
anyhow::bail!("unexpected QR status: {other}");
}
}
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
}
}
fn main() {
let app = App::parse();
if matches!(&app.action, GatewayWechatCommand::Start { .. })
&& let Err(e) = ensure_config()
{
eprintln!("Error: {e}");
std::process::exit(1);
}
app.action.start(GatewayWechat);
}