wechatbot

March 24, 2026 · View on GitHub

WeChat iLink Bot SDK for Rust — async, type-safe, zero-copy where possible.

Install

[dependencies]
wechatbot = "0.1"
tokio = { version = "1", features = ["full"] }

Requires Rust 2021 edition. Built on tokio + reqwest.

Quick Start

use wechatbot::{WeChatBot, BotOptions};

#[tokio::main]
async fn main() {
    let bot = WeChatBot::new(BotOptions::default());
    let creds = bot.login(false).await.unwrap();
    println!("Logged in: {}", creds.account_id);

    bot.on_message(Box::new(|msg| {
        println!("{}: {}", msg.user_id, msg.text);
    })).await;

    bot.run().await.unwrap();
}

Architecture

src/
├── lib.rs           ← Public re-exports
├── types.rs         ← All protocol & public types (serde)
├── error.rs         ← Error hierarchy (thiserror)
├── protocol.rs      ← Raw iLink API calls (reqwest)
├── crypto.rs        ← AES-128-ECB encrypt/decrypt + key encoding
└── bot.rs           ← WeChatBot client (login, run, reply, send)

API Reference

Creating a Bot

use wechatbot::{WeChatBot, BotOptions};

let bot = WeChatBot::new(BotOptions {
    base_url: None,     // default: ilinkai.weixin.qq.com
    cred_path: None,    // default: ~/.wechatbot/credentials.json
    on_qr_url: Some(Box::new(|url| {
        println!("Scan: {}", url);
    })),
    on_error: Some(Box::new(|err| {
        eprintln!("Error: {}", err);
    })),
});

Authentication

// Login (skips QR if credentials exist)
let creds = bot.login(false).await?;

// Force re-login
let creds = bot.login(true).await?;

// Credentials struct
println!("Token: {}", creds.token);
println!("Base URL: {}", creds.base_url);
println!("Account: {}", creds.account_id);
println!("User: {}", creds.user_id);

Message Handling

bot.on_message(Box::new(|msg| {
    match msg.content_type {
        ContentType::Text => println!("Text: {}", msg.text),
        ContentType::Image => {
            for img in &msg.images {
                println!("Image URL: {:?}", img.url);
            }
        }
        ContentType::Voice => {
            for voice in &msg.voices {
                println!("Voice: {:?} ({}ms)", voice.text, voice.duration_ms.unwrap_or(0));
            }
        }
        ContentType::File => {
            for file in &msg.files {
                println!("File: {:?}", file.file_name);
            }
        }
        ContentType::Video => println!("Video received"),
    }

    if let Some(ref quoted) = msg.quoted {
        println!("Quoted: {:?}", quoted.title);
    }
})).await;

Sending Messages

// Reply to incoming message
bot.reply(&msg, "Echo: hello").await?;

// Send to user (needs prior context_token)
bot.send(user_id, "Hello").await?;

// Typing indicator
bot.send_typing(user_id).await?;

Media Operations

// Reply with media content
bot.reply_media(&msg, SendContent::Image(png_bytes)).await?;
bot.reply_media(&msg, SendContent::File { data, file_name: "report.pdf".into() }).await?;
bot.reply_media(&msg, SendContent::Video(mp4_bytes)).await?;
// Download media from incoming message (priority: image > file > video > voice)
if let Some(media) = bot.download(&msg).await? {
    println!("Type: {}, Size: {} bytes", media.media_type, media.data.len());
    if let Some(name) = &media.file_name {
        println!("Filename: {}", name);
    }
}

// Download a raw CDN reference directly
let raw = bot.download_raw(&msg.images[0].media.as_ref().unwrap(), None).await?;
// Upload to CDN without sending a message
let result = bot.upload(&file_bytes, user_id, 3).await?;

Lifecycle

// Start polling (blocks)
bot.run().await?;

// Stop
bot.stop().await;

Error Handling

use wechatbot::WeChatBotError;

match result {
    Err(WeChatBotError::Api { message, errcode, .. }) => {
        if errcode == -14 {
            // session expired — handled automatically
        }
    }
    Err(WeChatBotError::NoContext(user_id)) => {
        // no context_token for this user yet
    }
    Err(WeChatBotError::Transport(e)) => {
        // network error
    }
    _ => {}
}

AES-128-ECB Crypto

use wechatbot::{generate_aes_key, encrypt_aes_ecb, decrypt_aes_ecb, decode_aes_key};

// Generate key
let key = generate_aes_key();

// Encrypt/decrypt
let ciphertext = encrypt_aes_ecb(b"Hello", &key);
let plaintext = decrypt_aes_ecb(&ciphertext, &key)?;

// Decode protocol key (handles all 3 formats)
let key = decode_aes_key("ABEiM0RVZneImaq7zN3u/w==")?;
let key = decode_aes_key("00112233445566778899aabbccddeeff")?;

Types

All protocol types derive Serialize + Deserialize + Clone + Debug:

// Wire-level (protocol)
WireMessage, WireMessageItem, CDNMedia, TextItem, ImageItem, ...

// Parsed (user-friendly)
IncomingMessage, ImageContent, VoiceContent, FileContent, VideoContent

// Auth
Credentials

// Enums
MessageType, MessageState, MessageItemType, ContentType, MediaType

Testing

cd rust
cargo test

License

MIT