Skip to content

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 user_id = message.author.as_ref().map(|a| &a.id);
let username = message.author.as_ref().and_then(|a| a.username.as_ref());
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>
let username = message.author.as_ref().and_then(|a| a.username.as_ref());

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.

Released under the MIT License.