# Migration from a Python QQ bot SDK
This page maps the most common Python QQ-bot SDK patterns to their `botrs` equivalents. The framework's surface area is intentionally similar — the same gateway, the same intents, the same message types — so most of the work is mechanical.
## Handler class → `EventHandler` trait
In a Python SDK you typically subclass a `Client` (or register handlers via decorators) and override `async def on_at_message_create(self, msg)` and friends.
In `botrs` you implement the `EventHandler` trait on a struct of your own. Each event is one `async fn` with a default empty body, so you only override what you care about. The handler must be `Send + Sync` and `'static`.
```rust
use botrs::{Client, Context, EventHandler, Intents, Message, Token};
struct MyBot;
#[async_trait::async_trait]
impl EventHandler for MyBot {
async fn at_message_create(&self, ctx: Context, msg: Message) {
if let Some(content) = msg.content.as_deref() {
if content.contains("ping") {
let _ = msg.reply(&ctx.api, &ctx.token, "pong").await;
}
}
}
}
```
The Python `Context`-equivalent (`api` + `token`) is passed in explicitly as the second parameter rather than reached for via `self`.
## Token
Python SDKs accept an `app_id` and a `secret` (sometimes called `token`). In `botrs`:
```rust
let token = Token::new(app_id, secret);
// or read QQ_BOT_APP_ID + QQ_BOT_SECRET from the environment
let token = Token::from_env()?;
```
`Token::validate()` exists for sanity-checking before you start; it errors with `BotError::Auth` on missing fields.
## Intents
Where Python uses something like `Intents.default() | Intents.public_guild_messages`, `botrs` uses bitflag constructors:
```rust
let intents = Intents::default()
.with_public_guild_messages()
.with_direct_message();
```
`Intents::default()` matches the Python "all public, no privileged" preset. The full table is in [intents](/guide/intents).
## Sending a message
The Python pattern of `await api.post_message(channel_id, content="hi", msg_id=...)` translates to the `*Params` builder family:
```rust
use botrs::models::message::MessageParams;
let params = MessageParams::new_text("hi").with_reply(&msg_id);
ctx.api.post_message_with_params(&ctx.token, &channel_id, params).await?;
```
The same pattern applies to group, C2C, and DM messages: `GroupMessageParams` → `post_group_message_with_params`, `C2CMessageParams` → `post_c2c_message_with_params`, `DirectMessageParams` → `post_dms_with_params`.
For richer payloads (embed, ark, markdown, keyboard) build the params struct directly with `..Default::default()`. There is no kwarg explosion — every channel kind has exactly one builder type.
## Starting the bot
Python: `client.run(app_id, secret)`.
`botrs`:
```rust
let mut client = Client::new(token, intents, MyBot, false /* is_sandbox */)?;
client.start().await?;
```
`client.start().await` is the long-lived future. Pair it with `tokio::main` (or your own runtime).
## Logging
`tracing` replaces Python's `logging`. The framework emits at `info` for lifecycle events and `debug` for gateway frames. Initialize a subscriber once:
```rust
tracing_subscriber::fmt().with_env_filter("botrs=info").init();
```
## Error handling
Python's `try/except` becomes Rust's `match` / `?`. Every API call returns `Result<T, BotError>`. See [error handling](/guide/error-handling) for the variant list. The framework does not retry on your behalf, so if your Python code relied on the SDK retrying transient failures, port that loop over manually.