- Add WeChatBotError enum for error handling with various error types. - Create a Result type alias for easier error management. - Implement ILinkClient for low-level API interactions including QR code generation, message sending, and updates retrieval. - Define message types and structures for handling incoming messages and media content. - Add tests for error handling and message parsing to ensure reliability. Co-authored-by: Copilot <copilot@github.com>
227 lines
5.0 KiB
Markdown
227 lines
5.0 KiB
Markdown
# wechatbot — Rust SDK
|
|
|
|
WeChat iLink Bot SDK for Rust — async, type-safe, zero-copy where possible.
|
|
|
|
## Install
|
|
|
|
```toml
|
|
[dependencies]
|
|
wechatbot = "0.1"
|
|
tokio = { version = "1", features = ["full"] }
|
|
```
|
|
|
|
Requires Rust 2021 edition. Built on `tokio` + `reqwest`.
|
|
|
|
## Quick Start
|
|
|
|
```rust
|
|
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
|
|
|
|
```rust
|
|
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
|
|
|
|
```rust
|
|
// 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
|
|
|
|
```rust
|
|
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
|
|
|
|
```rust
|
|
// 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
|
|
|
|
```rust
|
|
// 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?;
|
|
```
|
|
|
|
```rust
|
|
// 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?;
|
|
```
|
|
|
|
```rust
|
|
// Upload to CDN without sending a message
|
|
let result = bot.upload(&file_bytes, user_id, 3).await?;
|
|
```
|
|
|
|
### Lifecycle
|
|
|
|
```rust
|
|
// Start polling (blocks)
|
|
bot.run().await?;
|
|
|
|
// Stop
|
|
bot.stop().await;
|
|
```
|
|
|
|
## Error Handling
|
|
|
|
```rust
|
|
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
|
|
|
|
```rust
|
|
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`:
|
|
|
|
```rust
|
|
// 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
|
|
|
|
```bash
|
|
cd rust
|
|
cargo test
|
|
```
|
|
|
|
## License
|
|
|
|
MIT
|