maxoxide 0.1.0

Async Rust library for the Max messenger Bot API
Documentation

Crates.io docs.rs MIT Build Status made-with-rust

Readme in different languages: EN ยท RU

alt text maxoxide

๐Ÿฆ€ An async Rust library for building bots on the Max messenger platform, inspired by teloxide.

Features

  • โœ… Full coverage of the Max Bot REST API
  • โœ… Long polling (dev & test) and Webhook via axum (production)
  • โœ… Strongly-typed events (Update, Message, Callback, โ€ฆ)
  • โœ… Dispatcher with fluent handler registration and filters
  • โœ… Inline keyboards (all button types: callback, link, message, request_contact, request_geo_location)
  • โœ… File uploads โ€” multipart, correct token flow for video/audio
  • โœ… Markdown / HTML message formatting
  • โœ… Webhook secret verification (X-Max-Bot-Api-Secret)
  • โœ… Tokio async throughout

Quick start

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

# For webhook support (production):
# maxoxide = { version = "0.1", features = ["webhook"] }
use maxoxide::{Bot, Context, Dispatcher};
use maxoxide::types::Update;

#[tokio::main]
async fn main() {
    tracing_subscriber::fmt::init();

    let bot = Bot::from_env();     // reads MAX_BOT_TOKEN env var
    let mut dp = Dispatcher::new(bot);

    dp.on_command("/start", |ctx: Context| async move {
        if let Update::MessageCreated { message, .. } = &ctx.update {
            ctx.bot.send_markdown(message.chat_id(), "Hello! ๐Ÿ‘‹").await?;
        }
        Ok(())
    });

    dp.on_message(|ctx: Context| async move {
        if let Update::MessageCreated { message, .. } = &ctx.update {
            let text = message.text().unwrap_or("(no text)").to_string();
            ctx.bot.send_text(message.chat_id(), text).await?;
        }
        Ok(())
    });

    dp.start_polling().await;
}
MAX_BOT_TOKEN=your_token cargo run --example echo_bot

API methods

Method Description
bot.get_me() Bot info
bot.send_text(chat_id, text) Send plain text
bot.send_markdown(chat_id, text) Send Markdown
bot.send_message(chat_id, body) Send message with attachments / keyboard
bot.edit_message(mid, body) Edit a message
bot.delete_message(mid) Delete a message
bot.answer_callback(body) Answer an inline button press
bot.get_chat(chat_id) Chat info
bot.get_chats(โ€ฆ) List all group chats
bot.edit_chat(chat_id, body) Edit chat title / description
bot.leave_chat(chat_id) Leave a chat
bot.get_members(โ€ฆ) List members
bot.add_members(โ€ฆ) Add members
bot.remove_member(โ€ฆ) Remove a member
bot.get_admins(chat_id) List admins
bot.pin_message(โ€ฆ) Pin a message
bot.unpin_message(โ€ฆ) Unpin
bot.send_action(chat_id, "typing") Typing indicator
bot.subscribe(body) Register a webhook
bot.get_upload_url(type) Get upload URL
bot.upload_file(type, path, name, mime) Full two-step file upload
bot.upload_bytes(type, bytes, name, mime) Same, from bytes
bot.set_my_commands(commands) Set bot commands

Dispatcher filters

dp.on_command("/start", handler);             // specific command
dp.on_message(handler);                       // any new message
dp.on_edited_message(handler);               // edited message
dp.on_callback(handler);                     // any callback button
dp.on_callback_payload("btn:ok", handler);   // specific payload
dp.on_bot_started(handler);                  // user starts bot
dp.on_filter(|u| { โ€ฆ }, handler);            // custom predicate
dp.on(handler);                              // every update

First matching handler wins. Register more specific filters before general ones.

Inline keyboard

use maxoxide::types::{Button, KeyboardPayload, NewMessageBody};

let keyboard = KeyboardPayload {
    buttons: vec![
        vec![
            Button::callback("Yes โœ…", "answer:yes"),
            Button::callback("No โŒ",  "answer:no"),
        ],
        vec![Button::link("๐ŸŒ Website", "https://max.ru")],
    ],
};

let body = NewMessageBody::text("Are you sure?").with_keyboard(keyboard);
bot.send_message(chat_id, body).await?;

File upload

Max uses a two-step upload flow. upload_file / upload_bytes handle it automatically:

use maxoxide::types::{NewAttachment, NewMessageBody, UploadType, UploadedToken};

let token = bot
    .upload_file(UploadType::Image, "./photo.jpg", "photo.jpg", "image/jpeg")
    .await?;

let body = NewMessageBody {
    text: Some("Here's the photo!".into()),
    attachments: Some(vec![NewAttachment::Image {
        payload: UploadedToken { token },
    }]),
    ..Default::default()
};
bot.send_message(chat_id, body).await?;

Note: type=photo was removed from the Max API. Always use UploadType::Image.

Webhook server (features = ["webhook"])

use maxoxide::webhook::WebhookServer;
use maxoxide::types::SubscribeBody;

bot.subscribe(SubscribeBody {
    url: "https://your-domain.com/webhook".into(),
    update_types: None,
    version: None,
    secret: Some("my_secret_123".into()),
}).await?;

WebhookServer::new(dp)
    .secret("my_secret_123")
    .path("/webhook")
    .serve("0.0.0.0:8443")
    .await;

Max requires HTTPS on port 443 and does not support self-signed certificates.

Project layout

maxoxide/
โ”œโ”€โ”€ Cargo.toml
โ”œโ”€โ”€ src/
โ”‚   โ”œโ”€โ”€ lib.rs          โ€” public API & re-exports
โ”‚   โ”œโ”€โ”€ bot.rs          โ€” Bot + all HTTP methods
โ”‚   โ”œโ”€โ”€ uploader.rs     โ€” two-step file upload helpers
โ”‚   โ”œโ”€โ”€ dispatcher.rs   โ€” Dispatcher, Filter, Context
โ”‚   โ”œโ”€โ”€ errors.rs       โ€” MaxError
โ”‚   โ”œโ”€โ”€ webhook.rs      โ€” axum webhook server (feature = "webhook")
โ”‚   โ”œโ”€โ”€ tests.rs        โ€” unit tests
โ”‚   โ””โ”€โ”€ types/
โ”‚       โ””โ”€โ”€ mod.rs      โ€” all types (User, Chat, Message, Update, โ€ฆ)
โ””โ”€โ”€ examples/
    โ”œโ”€โ”€ echo_bot.rs
    โ”œโ”€โ”€ keyboard_bot.rs
    โ””โ”€โ”€ webhook_bot.rs  (feature = "webhook")

Running tests

cargo test

License

MIT