crabchat 0.0.12

WeChat gateway for Crabtalk
Documentation
//! `crabtalk-wechat` binary — WeChat gateway for Crabtalk.

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")
}

/// Ensure a WeChat token exists in gateway.toml, running QR login if needed.
///
/// Runs before the service runtime starts (same pattern as Telegram's
/// ensure_config). Uses a one-off tokio runtime for the HTTP calls.
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" => {}
            // NOTE: the server returns "scaned" (sic) — not a typo on our side.
            "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);
}