aibot 1.0.1

WeChat iLink Bot API — full async Rust implementation
Documentation
//! # WeChat iLink Bot — 完整可运行示例
//!
//! 本文件提供了一个开箱即用的 Bot 高层封装 [`WeChatBot`],包含:
//!
//! - **扫码登录** —— 首次运行自动获取二维码,扫码后 Token 持久化到本地
//! - **消息循环** —— 长轮询接收消息,自动处理文本/图片/语音/文件
//! - **回声回复** —— 收到文本消息后自动回复相同内容(示例逻辑)
//! - **媒体下载** —— 自动下载并解密图片、语音、文件
//!
//! ## 运行方式
//!
//! ```bash
//! # 设置 Token(已有的话可直接从环境变量传入)
//! export WECHAT_BOT_TOKEN="your_token_here"
//!
//! # 直接运行(首次会弹出二维码)
//! cargo run
//! ```
//!
//! ## 环境变量
//!
//! | 变量 | 说明 |
//! |------|------|
//! | `WECHAT_BOT_TOKEN` | Bot Token,非必填(首次会扫码自动获取) |
//! | `WECHAT_BOT_TOKEN_FILE` | Token 持久化路径,默认 `~/.wechat_bot_token` |
//! | `RUST_LOG` | 日志级别,如 `info`、`debug`、`trace` |

use std::path::PathBuf;
use std::time::Duration;

use tokio::fs;
use aibot::*;
use xbw::all::log;
use xbw::crypt::base64::{Base64, Engine};

/// Bot 高层封装。
///
/// 提供扫码登录、消息循环、自动回复等开箱即用功能。
/// 适合快速搭建 Bot 服务。如需更精细的控制,可以直接使用 [`ILinkClient`]。
struct WeChatBot {
    /// 底层 API 客户端。
    client: ILinkClient,
    /// Token 持久化文件路径。
    token_file: PathBuf,
    /// 消息游标(长轮询断点续传)。
    updates_cursor: String,
}

impl WeChatBot {
    /// 创建新的 Bot 实例。
    ///
    /// 如果 `token_file` 中已存在持久化的 Token,会自动加载。
    ///
    /// ## 参数
    ///
    /// * `token_file` — Token 持久化文件路径(文件内容是纯文本 Token)。
    pub async fn new(token_file: PathBuf) -> BotResult<Self> {
        // 尝试从文件加载已保存的 Token
        let token = if token_file.exists() {
            let token = fs::read_to_string(&token_file)
                .await
                .map_err(|e| BotError::Io(e))?;
            log::info!("从文件加载 Bot Token");
            token.trim().to_string()
        } else {
            String::new()
        };

        let client = ILinkClient::new(token, None);

        Ok(Self {
            client,
            token_file,
            updates_cursor: String::new(),
        })
    }

    /// 扫码登录(如果尚未认证)。
    ///
    /// 流程:
    /// 1. 如果已有 Token 则跳过
    /// 2. 调用 `get_bot_qrcode` 获取二维码
    /// 3. 保存为 `wechat_qrcode.png` 文件
    /// 4. 轮询等待用户扫码确认(最长 5 分钟)
    /// 5. 更新客户端 Token 并持久化到文件
    pub async fn login(&mut self) -> BotResult<()> {
        if self.client.is_authenticated() {
            log::info!("已有 Bot Token,跳过扫码");
            return Ok(());
        }

        log::info!("获取登录二维码...");
        let qr_resp = self.client.get_bot_qrcode().await?;

        // 将 Base64 编码的二维码图片解码并保存为 PNG 文件
        let qr_img = Base64
            .decode(&qr_resp.qrcode_img_content)
            .map_err(|e| BotError::invalid_data(format!("解码二维码图片失败: {}", e)))?;
        fs::write("wechat_qrcode.png", &qr_img).await?;
        log::info!("二维码已保存到 wechat_qrcode.png,请用微信扫描");

        println!("请扫描二维码登录...");

        // 轮询等待扫码确认(2 秒间隔,5 分钟超时)
        let (bot_token, _base_url) = self
            .client
            .wait_for_login(
                &qr_resp.qrcode,
                Duration::from_secs(2),
                Duration::from_secs(300), // 5 分钟
            )
            .await?;

        log::info!("扫码登录成功!");

        // 更新客户端 Token
        self.client.set_bot_token(&bot_token);

        // 持久化 Token 到文件,下次无需扫码
        fs::write(&self.token_file, &bot_token).await?;
        log::info!("Token 已保存到 {:?}", self.token_file);

        Ok(())
    }

    /// 运行主消息监听循环。
    ///
    /// 使用长轮询持续接收消息,每收到一条消息就调用 [`handle_message`] 处理。
    /// 如果网络出错,等待 3 秒后自动重试。
    pub async fn run(&mut self) -> BotResult<()> {
        log::info!("启动消息监听循环...");

        loop {
            match self.client.get_updates(&self.updates_cursor).await {
                Ok(resp) => {
                    // 更新游标,下次从这个位置继续拉取
                    self.updates_cursor = resp.get_updates_buf;

                    // 逐条处理消息
                    for msg in resp.msgs {
                        if let Err(e) = self.handle_message(&msg).await {
                            log::error!("处理消息失败: {:?}", e);
                        }
                    }
                }
                Err(e) => {
                    log::error!("get_updates 请求失败: {:?}", e);
                    // 出错后等待一会再重试
                    tokio::time::sleep(Duration::from_secs(3)).await;
                }
            }
        }
    }

    /// 处理单条入站消息。
    ///
    /// 根据消息中的内容类型分别处理:
    /// - **文本**:回声回复(示例逻辑,可按需修改)
    /// - **图片**:下载解密后保存到本地,回复确认消息
    /// - **语音**:下载解密后保存为 `.silk` 文件,回复确认消息
    /// - **文件**:下载解密后保存到本地,回复确认消息
    async fn handle_message(&self, msg: &WeChatMessage) -> BotResult<()> {
        // 忽略空消息(无发送者)
        if msg.from_user_id.is_empty() {
            return Ok(());
        }

        let from_user = &msg.from_user_id;
        let ctx_token = &msg.context_token;

        log::info!("收到消息 from={}", from_user);

        // 遍历消息中的所有内容项
        for item in &msg.data.item_list {
            match item {
                // ── 文本消息 ──
                ContentItem::Text { text_item } => {
                    let text = &text_item.text;
                    log::info!("  文本: {}", text);

                    // 示例:简单回声回复(可按需改为 AI 对话等)
                    let reply = format!("你说了: {}", text);
                    self.client.send_text(from_user, &reply, ctx_token).await?;
                    log::info!("  已回复");
                }

                // ── 图片消息 ──
                ContentItem::Image { image_item } => {
                    log::info!("  图片: cdn={}", image_item.cdn_image_url);

                    match self
                        .client
                        .download_and_decrypt_media(
                            &image_item.cdn_image_url,
                            &image_item.aes_key,
                            None, // 图片不需要 encrypt_query_param
                        )
                        .await
                    {
                        Ok(data) => {
                            let filename = format!("received_{}.jpg", from_user);
                            fs::write(&filename, &data).await?;
                            log::info!("  图片已保存到 {}", filename);

                            self.client
                                .send_text(
                                    from_user,
                                    &format!("已收到你的图片 ({} bytes)", data.len()),
                                    ctx_token,
                                )
                                .await?;
                        }
                        Err(e) => {
                            log::error!("  图片解密/下载失败: {:?}", e);
                        }
                    }
                }

                // ── 语音消息 ──
                ContentItem::Voice { voice_item } => {
                    log::info!("  语音: aes_key={}", voice_item.aes_key);
                    match self
                        .client
                        .download_and_decrypt_media(
                            &voice_item.cdn_voice_url,
                            &voice_item.aes_key,
                            None, // 语音不需要 encrypt_query_param
                        )
                        .await
                    {
                        Ok(data) => {
                            let filename = format!("received_{}.silk", from_user);
                            fs::write(&filename, &data).await?;
                            log::info!("  语音已保存到 {}", filename);

                            self.client
                                .send_text(
                                    from_user,
                                    &format!("已收到你的语音 ({} bytes)", data.len()),
                                    ctx_token,
                                )
                                .await?;
                        }
                        Err(e) => {
                            log::error!("  语音解密/下载失败: {:?}", e);
                        }
                    }
                }

                // ── 文件消息 ──
                ContentItem::File { file_item } => {
                    log::info!("  文件: name={}", file_item.file_name);
                    match self
                        .client
                        .download_and_decrypt_media(
                            &file_item.cdn_file_url,
                            &file_item.aes_key,
                            Some(&file_item.encrypt_query_param), // 文件需要 encrypt_query_param
                        )
                        .await
                    {
                        Ok(data) => {
                            let filename =
                                format!("received_{}_{}", from_user, file_item.file_name);
                            fs::write(&filename, &data).await?;
                            log::info!("  文件已保存到 {}", filename);

                            self.client
                                .send_text(
                                    from_user,
                                    &format!("已收到你的文件 ({} bytes)", data.len()),
                                    ctx_token,
                                )
                                .await?;
                        }
                        Err(e) => {
                            log::error!("  文件解密/下载失败: {:?}", e);
                        }
                    }
                }
            }
        }

        Ok(())
    }
}

// ============================================================================
// 程序入口
// ============================================================================

#[tokio::main]
async fn main() -> BotResult<()> {
    // 确定 Token 持久化路径
    // 优先级:环境变量 WECHAT_BOT_TOKEN_FILE > ~/.wechat_bot_token > .wechat_bot_token
    let token_file = if let Ok(val) = std::env::var("WECHAT_BOT_TOKEN_FILE") {
        PathBuf::from(val)
    } else if let Ok(home) = std::env::var("HOME") {
        PathBuf::from(home).join(".wechat_bot_token")
    } else {
        PathBuf::from(".wechat_bot_token")
    };

    let mut bot = WeChatBot::new(token_file).await?;

    // 登录(已认证则跳过,未认证则自动触发扫码)
    bot.login().await?;

    // 启动消息监听(此调用会一直运行,直到进程终止)
    bot.run().await?;

    Ok(())
}