# Migration from Python botpy
This guide helps developers migrate from the official Python `botpy` library to BotRS. While both libraries implement the QQ Guild Bot API, BotRS provides Rust's type safety, performance benefits, and memory safety guarantees.
## Overview
BotRS maintains API compatibility with Python botpy's design patterns while adding Rust-specific improvements. This makes migration straightforward for developers familiar with the Python ecosystem.
### Key Differences
- **Type Safety**: Rust's compile-time type checking prevents many runtime errors
- **Performance**: Rust's zero-cost abstractions and efficient memory management
- **Memory Safety**: No garbage collection overhead, predictable memory usage
- **Async Runtime**: Uses Tokio for high-performance async operations
- **Error Handling**: Explicit error handling with `Result<T, E>` types
## Project Structure Migration
### Python botpy Project
```
my_bot/
├── main.py
├── config.yaml
├── bot/
│ ├── __init__.py
│ ├── handlers.py
│ └── utils.py
└── requirements.txt
```
### BotRS Project
```
my_bot/
├── Cargo.toml
├── src/
│ ├── main.rs
│ ├── handlers.rs
│ └── utils.rs
├── config.toml
└── examples/
```
### Cargo.toml Setup
```toml
[package]
name = "my_bot"
version = "0.1.0"
edition = "2021"
[dependencies]
botrs = "0.2"
tokio = { version = "1.0", features = ["full"] }
tracing = "0.1"
tracing-subscriber = "0.3"
serde = { version = "1.0", features = ["derive"] }
```
## Configuration Migration
### Python botpy Configuration
```python
# config.py
import yaml
class Config:
def __init__(self):
with open('config.yaml') as f:
data = yaml.safe_load(f)
self.app_id = data['bot']['app_id']
self.secret = data['bot']['secret']
self.sandbox = data.get('sandbox', False)
```
### BotRS Configuration
```rust
// src/config.rs
use serde::Deserialize;
use std::fs;
#[derive(Deserialize)]
pub struct Config {
pub bot: BotConfig,
pub sandbox: Option<bool>,
}
#[derive(Deserialize)]
pub struct BotConfig {
pub app_id: String,
pub secret: String,
}
impl Config {
pub fn load() -> Result<Self, Box<dyn std::error::Error>> {
let content = fs::read_to_string("config.toml")?;
let config: Config = toml::from_str(&content)?;
Ok(config)
}
}
```
## Client Setup Migration
### Python botpy Client
```python
import botpy
from botpy import logging
from botpy.ext.cog_yaml import read
class MyClient(botpy.Client):
async def on_ready(self):
_log.info(f"robot 「{self.robot.name}」 on_ready!")
async def on_at_message_create(self, message):
await message.reply(content=f"机器人{self.robot.name}收到你的@消息了: {message.content}")
if __name__ == "__main__":
intents = botpy.Intents(public_guild_messages=True)
client = MyClient(intents=intents)
client.run(appid="APP_ID", secret="SECRET")
```
### BotRS Client
```rust
// src/main.rs
use botrs::{Client, EventHandler, Context, Message, Intents, Token};
use tracing::info;
struct MyBot;
#[async_trait::async_trait]
impl EventHandler for MyBot {
async fn ready(&self, ctx: Context) {
info!("robot is ready!");
}
async fn message_create(&self, ctx: Context, message: Message) {
if let Some(content) = &message.content {
let reply = format!("机器人收到你的@消息了: {}", content);
let params = botrs::MessageParams::new_text(&reply);
if let Some(channel_id) = &message.channel_id {
ctx.api.post_message_with_params(&ctx.token, channel_id, params).await.ok();
}
}
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
tracing_subscriber::init();
let config = Config::load()?;
let token = Token::new(config.bot.app_id, config.bot.secret);
let intents = Intents::default().with_public_guild_messages();
let mut client = Client::new(token, intents, MyBot, false)?;
client.start().await?;
Ok(())
}
```
## Event Handler Migration
### Python botpy Event Handlers
```python
class MyClient(botpy.Client):
async def on_at_message_create(self, message):
"""Handle @ mentions"""
await self.handle_at_message(message)
async def on_guild_member_add(self, member):
"""Handle new member joins"""
await self.welcome_member(member)
async def on_message_reaction_add(self, reaction):
"""Handle reaction additions"""
await self.handle_reaction(reaction)
async def handle_at_message(self, message):
if message.content.strip() == "hello":
await message.reply(content="Hello! How can I help you?")
elif message.content.strip() == "ping":
await message.reply(content="Pong!")
```
### BotRS Event Handlers
```rust
use botrs::{EventHandler, Context, Message, GuildMember, MessageReaction};
struct MyBot;
#[async_trait::async_trait]
impl EventHandler for MyBot {
async fn message_create(&self, ctx: Context, message: Message) {
self.handle_at_message(ctx, message).await;
}
async fn guild_member_add(&self, ctx: Context, member: GuildMember) {
self.welcome_member(ctx, member).await;
}
async fn message_reaction_add(&self, ctx: Context, reaction: MessageReaction) {
self.handle_reaction(ctx, reaction).await;
}
}
impl MyBot {
async fn handle_at_message(&self, ctx: Context, message: Message) {
let content = match &message.content {
Some(content) => content.trim(),
None => return,
};
let response = match content {
"hello" => "Hello! How can I help you?",
"ping" => "Pong!",
_ => return,
};
if let Some(channel_id) = &message.channel_id {
let params = botrs::MessageParams::new_text(response);
ctx.api.post_message_with_params(&ctx.token, channel_id, params).await.ok();
}
}
}
```
## Message Sending Migration
### Python botpy Message Sending
```python
# Simple text message
await message.reply(content="Hello, world!")
# Embed message
embed = botpy.Embed(title="My Embed", description="This is an embed")
await message.reply(embed=embed)
# File upload
with open("image.png", "rb") as f:
await message.reply(file=botpy.File(f, "image.png"))
# Markdown message
markdown = botpy.MessageMarkdown(content="# Hello\n\nThis is **bold**")
await message.reply(markdown=markdown)
# Keyboard message
keyboard = botpy.MessageKeyboard(content=buttons_data)
await message.reply(keyboard=keyboard)
```
### BotRS Message Sending
```rust
use botrs::{MessageParams, Embed, MarkdownPayload};
// Simple text message
let params = MessageParams::new_text("Hello, world!");
ctx.api.post_message_with_params(&ctx.token, &channel_id, params).await?;
// Embed message
let embed = Embed {
title: Some("My Embed".to_string()),
description: Some("This is an embed".to_string()),
..Default::default()
};
let params = MessageParams {
content: Some("Check this out:".to_string()),
embed: Some(embed),
..Default::default()
};
ctx.api.post_message_with_params(&ctx.token, &channel_id, params).await?;
// File upload
let image_data = std::fs::read("image.png")?;
let params = MessageParams::new_text("Here's an image:")
.with_file_image(&image_data);
ctx.api.post_message_with_params(&ctx.token, &channel_id, params).await?;
// Markdown message
let markdown = MarkdownPayload {
content: Some("# Hello\n\nThis is **bold**".to_string()),
..Default::default()
};
let params = MessageParams {
markdown: Some(markdown),
..Default::default()
};
ctx.api.post_message_with_params(&ctx.token, &channel_id, params).await?;
// Keyboard message (similar structure)
let params = MessageParams {
keyboard: Some(keyboard_data),
..Default::default()
};
ctx.api.post_message_with_params(&ctx.token, &channel_id, params).await?;
```
## Intent System Migration
### Python botpy Intents
```python
import botpy
# Basic intents
intents = botpy.Intents.default()
intents.public_guild_messages = True
# Multiple intents
intents = botpy.Intents(
public_guild_messages=True,
direct_message=True,
guild_messages=True
)
# All intents
intents = botpy.Intents.all()
```
### BotRS Intents
```rust
use botrs::Intents;
// Basic intents
let intents = Intents::default().with_public_guild_messages();
// Multiple intents
let intents = Intents::default()
.with_public_guild_messages()
.with_direct_message()
.with_guild_messages();
// All intents
let intents = Intents::all();
// Custom combination
let intents = Intents::from_bits(0b1010).unwrap_or_default();
```
## Error Handling Migration
### Python botpy Error Handling
```python
import botpy
from botpy.errors import *
try:
await message.reply(content="Hello!")
except ServerError as e:
print(f"Server error: {e}")
except Forbidden as e:
print(f"Permission error: {e}")
except Exception as e:
print(f"Unknown error: {e}")
```
### BotRS Error Handling
```rust
use botrs::Error;
match ctx.api.post_message_with_params(&ctx.token, &channel_id, params).await {
Ok(response) => {
println!("Message sent successfully: {:?}", response);
}
Err(Error::Http(status)) if status == 403 => {
println!("Permission error: Bot lacks necessary permissions");
}
Err(Error::Http(status)) if status >= 500 => {
println!("Server error: {}", status);
}
Err(e) => {
println!("Other error: {}", e);
}
}
// Using ? operator for early return
async fn send_message(&self, ctx: &Context, channel_id: &str, content: &str) -> Result<(), Error> {
let params = MessageParams::new_text(content);
ctx.api.post_message_with_params(&ctx.token, channel_id, params).await?;
Ok(())
}
```
## Async/Await Migration
### Python botpy Async
```python
import asyncio
import botpy
class MyClient(botpy.Client):
async def on_at_message_create(self, message):
# Simple async operation
await message.reply(content="Hello!")
# Multiple async operations
tasks = [
self.send_notification(message.author.id),
self.log_message(message.content),
self.update_stats()
]
await asyncio.gather(*tasks)
async def send_notification(self, user_id):
await asyncio.sleep(1) # Simulate work
print(f"Notification sent to {user_id}")
```
### BotRS Async
```rust
use tokio::time::{sleep, Duration};
impl MyBot {
async fn handle_message(&self, ctx: Context, message: Message) {
// Simple async operation
let params = MessageParams::new_text("Hello!");
if let Some(channel_id) = &message.channel_id {
ctx.api.post_message_with_params(&ctx.token, channel_id, params).await.ok();
}
// Multiple async operations
let user_id = message.author.as_ref().map(|a| &a.id);
let content = message.content.as_deref();
tokio::join!(
self.send_notification(user_id),
self.log_message(content),
self.update_stats()
);
}
async fn send_notification(&self, user_id: Option<&String>) {
sleep(Duration::from_secs(1)).await; // Simulate work
if let Some(id) = user_id {
println!("Notification sent to {}", id);
}
}
}
```
## Data Models Migration
### Python botpy Models
```python
# Accessing message data
user_id = message.author.id
username = message.author.username
channel_id = message.channel_id
guild_id = message.guild_id
content = message.content
# Accessing guild data
guild_name = guild.name
guild_id = guild.id
member_count = guild.member_count
```
### BotRS Models
```rust
// Accessing message data (with Option handling)
let channel_id = message.channel_id.as_ref();
let guild_id = message.guild_id.as_ref();
let content = message.content.as_ref();
// Safe access with pattern matching
if let Some(author) = &message.author {
if let Some(username) = &author.username {
println!("Message from: {}", username);
}
}
// Using unwrap_or for defaults
let content = message.content.as_deref().unwrap_or("No content");
```
## Command System Migration
### Python botpy Commands
```python
class MyClient(botpy.Client):
async def on_at_message_create(self, message):
content = message.content.strip()
if content.startswith("!hello"):
await self.handle_hello(message)
elif content.startswith("!help"):
await self.handle_help(message)
elif content.startswith("!echo "):
text = content[6:] # Remove "!echo "
await message.reply(content=f"Echo: {text}")
async def handle_hello(self, message):
await message.reply(content="Hello there!")
async def handle_help(self, message):
help_text = """
Available commands:
!hello - Say hello
!help - Show this help
!echo <text> - Echo your text
"""
await message.reply(content=help_text)
```
### BotRS Commands
```rust
impl MyBot {
async fn handle_message(&self, ctx: Context, message: Message) {
let content = match message.content.as_deref() {
Some(content) => content.trim(),
None => return,
};
if content.starts_with("!hello") {
self.handle_hello(&ctx, &message).await;
} else if content.starts_with("!help") {
self.handle_help(&ctx, &message).await;
} else if content.starts_with("!echo ") {
let text = &content[6..]; // Remove "!echo "
self.handle_echo(&ctx, &message, text).await;
}
}
async fn handle_hello(&self, ctx: &Context, message: &Message) {
if let Some(channel_id) = &message.channel_id {
let params = MessageParams::new_text("Hello there!");
ctx.api.post_message_with_params(&ctx.token, channel_id, params).await.ok();
}
}
async fn handle_help(&self, ctx: &Context, message: &Message) {
let help_text = r#"Available commands:
!hello - Say hello
!help - Show this help
!echo <text> - Echo your text"#;
if let Some(channel_id) = &message.channel_id {
let params = MessageParams::new_text(help_text);
ctx.api.post_message_with_params(&ctx.token, channel_id, params).await.ok();
}
}
async fn handle_echo(&self, ctx: &Context, message: &Message, text: &str) {
let response = format!("Echo: {}", text);
if let Some(channel_id) = &message.channel_id {
let params = MessageParams::new_text(&response);
ctx.api.post_message_with_params(&ctx.token, channel_id, params).await.ok();
}
}
}
```
## Database Integration Migration
### Python botpy with SQLAlchemy
```python
from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
Base = declarative_base()
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
user_id = Column(String, unique=True)
username = Column(String)
class MyClient(botpy.Client):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.engine = create_engine('sqlite:///bot.db')
Base.metadata.create_all(self.engine)
Session = sessionmaker(bind=self.engine)
self.session = Session()
async def on_at_message_create(self, message):
# Store user info
user = self.session.query(User).filter_by(user_id=message.author.id).first()
if not user:
user = User(user_id=message.author.id, username=message.author.username)
self.session.add(user)
self.session.commit()
```
### BotRS with SQLx
```rust
use sqlx::{SqlitePool, Row};
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
struct User {
id: i64,
user_id: String,
username: Option<String>,
}
struct MyBot {
db_pool: SqlitePool,
}
impl MyBot {
async fn new() -> Result<Self, sqlx::Error> {
let pool = SqlitePool::connect("sqlite:bot.db").await?;
// Create tables
sqlx::query(
r#"
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY,
user_id TEXT UNIQUE NOT NULL,
username TEXT
)
"#
)
.execute(&pool)
.await?;
Ok(Self { db_pool: pool })
}
async fn store_user(&self, user_id: &str, username: Option<&str>) -> Result<(), sqlx::Error> {
sqlx::query(
"INSERT OR REPLACE INTO users (user_id, username) VALUES (?, ?)"
)
.bind(user_id)
.bind(username)
.execute(&self.db_pool)
.await?;
Ok(())
}
}
#[async_trait::async_trait]
impl EventHandler for MyBot {
async fn message_create(&self, ctx: Context, message: Message) {
if let Some(author) = &message.author {
let username = author.username.as_deref();
if let Err(e) = self.store_user(&author.id, username).await {
println!("Database error: {}", e);
}
}
}
}
```
## Testing Migration
### Python botpy Testing
```python
import unittest
from unittest.mock import AsyncMock, patch
import botpy
class TestMyBot(unittest.IsolatedAsyncioTestCase):
async def test_hello_command(self):
client = MyClient()
# Mock message
message = AsyncMock()
message.content = "!hello"
message.reply = AsyncMock()
await client.on_at_message_create(message)
message.reply.assert_called_once_with(content="Hello there!")
```
### BotRS Testing
```rust
#[cfg(test)]
mod tests {
use super::*;
use botrs::{Context, Message, Author};
#[tokio::test]
async fn test_hello_command() {
let bot = MyBot::new().await.unwrap();
// Create mock message
let message = Message {
id: Some("123".to_string()),
content: Some("!hello".to_string()),
channel_id: Some("channel_123".to_string()),
author: Some(Author {
id: "user_123".to_string(),
username: Some("TestUser".to_string()),
..Default::default()
}),
..Default::default()
};
// Test would require mocking the API calls
// In practice, you'd use dependency injection or traits for testing
}
#[test]
fn test_message_parsing() {
let content = "!echo Hello World";
assert!(content.starts_with("!echo "));
let text = &content[6..];
assert_eq!(text, "Hello World");
}
}
```
## Performance Considerations
### Memory Usage
- **Python**: Garbage collected, unpredictable memory usage
- **Rust**: Stack allocation, predictable memory patterns, zero-cost abstractions
### Concurrency
- **Python**: Global Interpreter Lock (GIL) limits true parallelism
- **Rust**: True parallelism with `tokio`, no GIL limitations
### Error Handling
- **Python**: Runtime exceptions, can crash unexpectedly
- **Rust**: Compile-time error checking, explicit error handling
## Migration Checklist
- [ ] Set up Rust project with `Cargo.toml`
- [ ] Convert configuration files from YAML/JSON to TOML
- [ ] Migrate event handlers to use `#[async_trait::async_trait]`
- [ ] Update message sending to use `MessageParams`
- [ ] Convert intent setup to use BotRS intent system
- [ ] Update error handling to use `Result<T, E>`
- [ ] Migrate database code to use async Rust libraries
- [ ] Update logging to use `tracing` instead of Python logging
- [ ] Add proper type annotations and Option handling
- [ ] Set up CI/CD for Rust compilation and testing
## Common Migration Patterns
### Optional Values
```rust
// Python: value or None
username = message.author.username if message.author else None
// Rust: Option<T>
### Error Propagation
```python
# Python: try/except
try:
result = await api_call()
return process(result)
except Exception as e:
print(f"Error: {e}")
return None
```
```rust
// Rust: ? operator
async fn handle_api_call(&self) -> Result<ProcessedResult, Error> {
let result = api_call().await?;
Ok(process(result))
}
```
### String Handling
```python
# Python: str
content = message.content.strip().lower()
```
```rust
// Rust: String/&str with Option
let content = message.content
.as_deref()
.unwrap_or("")
.trim()
.to_lowercase();
```
This migration guide provides a comprehensive path from Python botpy to BotRS, leveraging Rust's strengths while maintaining familiar patterns where possible.