use std::path::PathBuf;
use std::time::Duration;
use tokio::fs;
use aibot::*;
use xbw::all::log;
use xbw::crypt::base64::{Base64, Engine};
struct WeChatBot {
client: ILinkClient,
token_file: PathBuf,
updates_cursor: String,
}
impl WeChatBot {
pub async fn new(token_file: PathBuf) -> BotResult<Self> {
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(),
})
}
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?;
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!("请扫描二维码登录...");
let (bot_token, _base_url) = self
.client
.wait_for_login(
&qr_resp.qrcode,
Duration::from_secs(2),
Duration::from_secs(300), )
.await?;
log::info!("扫码登录成功!");
self.client.set_bot_token(&bot_token);
fs::write(&self.token_file, &bot_token).await?;
log::info!("Token 已保存到 {:?}", self.token_file);
Ok(())
}
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;
}
}
}
}
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);
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, )
.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, )
.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), )
.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<()> {
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(())
}