maxoxide 2.0.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

  • โœ… Coverage of the published Max Bot REST API
  • โœ… Long polling and optional Webhook server via axum
  • โœ… Forward-compatible typed events (Update, Message, Callback, unknown update fallback)
  • โœ… Dispatcher with fluent handler registration, composable filters, startup hooks and scheduled tasks
  • โœ… Inline keyboards (callback, link, message, open_app, clipboard, request_contact, request_geo_location)
  • โœ… File uploads โ€” multipart, image photo-token payloads, correct token flow for video/audio, send helpers for image/video/audio/file
  • โœ… Markdown / HTML message formatting
  • โœ… Webhook secret verification (X-Max-Bot-Api-Secret)
  • โœ… Tokio async throughout

Quick start

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

# Enable the built-in axum webhook server:
# maxoxide = { version = "2.0.0", 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_to_chat(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_to_chat(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_to_chat(chat_id, text) Send plain text to a dialog/group/channel by chat_id
bot.send_text_to_user(user_id, text) Send plain text to a user by global MAX user_id
bot.send_markdown_to_chat(chat_id, text) Send Markdown to a dialog/group/channel by chat_id
bot.send_markdown_to_user(user_id, text) Send Markdown to a user by global MAX user_id
bot.send_message_to_chat(chat_id, body) Send message with attachments / keyboard by chat_id
bot.send_message_to_user(user_id, body) Send message with attachments / keyboard by global MAX user_id
bot.send_message_to_chat_with_options(chat_id, body, options) Send with query options such as disable_link_preview
bot.edit_message(mid, body) Edit a message
bot.delete_message(mid) Delete a message
bot.get_messages_by_ids(ids, โ€ฆ) Get one or more messages by message IDs
bot.get_video(video_token) Get video metadata and playback URLs
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.get_members_by_ids(chat_id, user_ids) Get selected members
bot.add_members(โ€ฆ) Add members
bot.remove_member(โ€ฆ) Remove a member
bot.get_admins(chat_id) List admins
bot.add_admins(chat_id, admins) Grant admin rights
bot.remove_admin(chat_id, user_id) Revoke admin rights
bot.pin_message(โ€ฆ) Pin a message
bot.unpin_message(โ€ฆ) Unpin
bot.send_sender_action(chat_id, action) Send a typed sender action
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.send_image_to_chat(...) Upload and send an image
bot.send_video_to_chat(...) Upload and send a video
bot.send_audio_to_chat(...) Upload and send audio
bot.send_file_to_chat(...) Upload and send a generic file
bot.set_my_commands(commands) Experimental: public MAX API currently returns 404 for /me/commands

User ID vs Chat ID

These two IDs are different and should not be used interchangeably:

  • user_id is the global MAX ID of a user.
  • chat_id is the ID of a concrete dialog, group, or channel.
  • In a private chat, message.sender.user_id identifies the user, while message.chat_id() identifies that specific dialog with the bot.
  • Use send_text_to_chat(chat_id, ...) / send_message_to_chat(chat_id, ...) when you already know the dialog or group.
  • Use send_text_to_user(user_id, ...) / send_message_to_user(user_id, ...) when you only know the user's global MAX ID.

Known MAX platform gaps

As of April 27, 2026, the crate can send these requests, but live behavior on the MAX side is still inconsistent:

  • Button::RequestContact is documented by MAX, but live tests received a contact attachment with empty contact_id and vcf_phone. Sending the button works; receiving the user's phone number is not confirmed on the MAX side.
  • Button::RequestGeoLocation delivers a structured Attachment::Location with latitude and longitude; the client can render the same shared position as a Yandex Maps card.
  • bot.send_sender_action(chat_id, SenderAction::TypingOn) returns success from the API, but live MAX tests did not confirm a visible typing indicator in the client.
  • bot.set_my_commands is kept as an experimental helper, but the public MAX REST docs do not list a write endpoint for bot commands, and live POST /me/commands requests return 404 Path /me/commands is not recognized.

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_update(
    Filter::message() & Filter::chat(chat_id) & Filter::text_contains("ping"),
    handler,
);                                           // composable filters
dp.on_start(handler);                        // runs before polling starts
dp.task(Duration::from_secs(60), handler);   // periodic task
dp.on_raw_update(handler);                   // raw JSON for every update
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_to_chat(chat_id, body).await?;

Clipboard buttons copy their payload in the MAX client. They do not send a callback update to the bot:

let keyboard = KeyboardPayload {
    buttons: vec![vec![Button::clipboard(
        "Copy invite code",
        "MAXOXIDE-2026",
    )]],
};

let body = NewMessageBody::empty().with_keyboard(keyboard);
bot.send_message_to_chat(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};

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(token)]),
    ..Default::default()
};
bot.send_message_to_chat(chat_id, body).await?;
// or:
// bot.send_message_to_user(user_id, body).await?;

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

For common cases, use upload-and-send helpers:

bot.send_image_to_chat(chat_id, "./photo.jpg", "photo.jpg", "image/jpeg", None).await?;
bot.send_video_to_chat(chat_id, "./clip.mp4", "clip.mp4", "video/mp4", None).await?;
bot.send_audio_to_chat(chat_id, "./track.mp3", "track.mp3", "audio/mpeg", None).await?;
bot.send_file_to_chat(chat_id, "./report.pdf", "report.pdf", "application/pdf", None).await?;

There are matching *_to_user and *_bytes_* helpers.

MAX can take a short time to finish processing uploaded attachments after the upload request succeeds. The upload-and-send helpers retry briefly when the API reports that an attachment is not processed yet.

Image uploads can return a MAX photos token map instead of a single token. The send_image_* helpers preserve that payload automatically.

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.

Use webhooks when your bot has a public HTTPS endpoint and you want MAX to deliver updates via incoming requests. For local development and simple launches, long polling is usually enough.

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.rs        โ€” all types (User, Chat, Message, Update, โ€ฆ)
โ””โ”€โ”€ examples/
    โ”œโ”€โ”€ echo_bot.rs
    โ”œโ”€โ”€ dispatcher_filters_bot.rs
    โ”œโ”€โ”€ keyboard_bot.rs
    โ”œโ”€โ”€ media_bot.rs
    โ”œโ”€โ”€ live_api_test.rs
    โ””โ”€โ”€ webhook_bot.rs  (feature = "webhook")

Running tests

cargo test

Live API test

For real-data verification there is a separate interactive harness:

cargo run --example live_api_test

At startup it asks in the terminal for:

  • bot token
  • bot URL for the tester
  • optional webhook URL and secret
  • optional local file path for upload_file
  • optional image, video and audio paths for media helper checks
  • HTTP timeout, polling timeout and delay between requests

The harness then walks the tester through Max-client actions and records PASS / FAIL / SKIP for real API calls. It uses small delays between requests, drains the long-poll backlog before the run, and asks for explicit confirmation before destructive or non-reversible steps such as:

  • set_my_commands
  • delete_chat
  • leave_chat
  • visible group title edits

The current run also probes the unclear MAX behavior around contact/location request buttons, message buttons, open_app, clipboard, sender actions, uploaded video metadata, selected members, and temporary admin rights changes.

License

MIT