maxbot 0.7.6

Автоматизация работы с чат-ботами на платформе MAX (max.ru)
Documentation
//! Демо-бот «Фрукты».
//!
//! При добавлении в чат или команде /start показывает приветствие с кнопкой «Встать».
//! При нажатии на кнопку — меню с фруктами.
//! При выборе фрукта — описание и картинка.
//! Нажатие на томат приводит к перезапуску.
//!
//! # Запуск
//! ```bash
//! export MAXBOT_TOKEN="ваш_токен"
//! cargo run --example fruits-bot
//! ```

use maxbot::{
    Attachment, Dispatcher, InlineKeyboard, InlineKeyboardButton, InlineKeyboardBuilder, MaxClient,
    SendMessageParamsBuilder,
};

/// Сборка inline-клавиатуры из списка кнопок.
fn make_keyboard(buttons: Vec<InlineKeyboardButton>) -> Result<InlineKeyboard, maxbot::KeyboardValidationError> {
    let mut builder = InlineKeyboardBuilder::new();
    for btn in buttons {
        builder = builder.button(btn);
    }
    builder.build()
}

/// Кнопка «Встать».
fn btn_stand() -> InlineKeyboardButton {
    InlineKeyboardButton::callback("Встать", "stand")
}

/// Кнопки выбора фруктов (🍏 🍐 🍊 🍅).
fn fruit_buttons() -> Result<InlineKeyboard, maxbot::KeyboardValidationError> {
    make_keyboard(vec![
        InlineKeyboardButton::callback("🍏", "fruit:apple"),
        InlineKeyboardButton::callback("🍐", "fruit:pear"),
        InlineKeyboardButton::callback("🍊", "fruit:orange"),
        InlineKeyboardButton::callback("🍅", "fruit:tomato"),
    ])
}

/// Кнопка «Начать заново» (для томата).
fn btn_restart() -> Result<InlineKeyboard, maxbot::KeyboardValidationError> {
    make_keyboard(vec![InlineKeyboardButton::callback("Начать заново", "stand")])
}

/// Сообщение приветствия.
fn greeting() -> Result<SendMessageParamsBuilder, maxbot::KeyboardValidationError> {
    Ok(SendMessageParamsBuilder::new()
        .text("Ну-ка, фрукты, встаньте в ряд!")
        .attachment(Attachment::inline_keyboard(
            make_keyboard(vec![btn_stand()])?,
        )))
}

/// Сообщение с фруктовым меню.
fn fruit_menu() -> Result<SendMessageParamsBuilder, maxbot::KeyboardValidationError> {
    Ok(SendMessageParamsBuilder::new()
        .text("Перед вами, друзья, фруктов дружная семья.")
        .attachment(Attachment::inline_keyboard(fruit_buttons()?)))
}

/// Сообщение о яблоке.
fn apple_message() -> Result<SendMessageParamsBuilder, maxbot::KeyboardValidationError> {
    let caption = "Сочное, спелое, наливное. Яблоко — вот кто я!";
    let image = Attachment::image_url("https://i0.wp.com/static.kinoafisha.info/upload/articles/362065694948.jpg");
    Ok(SendMessageParamsBuilder::new()
        .text(caption)
        .attachment(image)
        .attachment(Attachment::inline_keyboard(fruit_buttons()?)))
}

/// Сообщение о груше.
fn pear_message() -> Result<SendMessageParamsBuilder, maxbot::KeyboardValidationError> {
    let caption = "Я спелая грушка — яблоку подружка!";
    let image = Attachment::image_url("https://img0.reactor.cc/pics/comment/%D0%9A%D0%BE%D0%BC%D0%B8%D0%BA%D1%81%D1%8B-%D0%9A%D0%B0%D0%B6%D0%B4%D1%8B%D0%B9-%D1%82%D1%80%D0%B5%D1%82%D0%B8%D0%B9-%D1%81%D0%B0%D0%BC%D0%B0-%D1%81%D1%83%D1%82%D1%8C-%D0%BF%D0%B5%D1%81%D0%BE%D1%87%D0%BD%D0%B8%D1%86%D0%B0-1156516.jpeg");
    Ok(SendMessageParamsBuilder::new()
        .text(caption)
        .attachment(image)
        .attachment(Attachment::inline_keyboard(fruit_buttons()?)))
}

/// Сообщение об апельсине.
fn orange_message() -> Result<SendMessageParamsBuilder, maxbot::KeyboardValidationError> {
    let caption = "Я — пузатый апельсин. Солнышка весёлый сын.";
    let image = Attachment::image_url("https://click-or-die.ru/app/uploads/2024/07/apelsinn.jpg");
    Ok(SendMessageParamsBuilder::new()
        .text(caption)
        .attachment(image)
        .attachment(Attachment::inline_keyboard(fruit_buttons()?)))
}

/// Сообщение о томате.
fn tomato_message() -> Result<SendMessageParamsBuilder, maxbot::KeyboardValidationError> {
    let caption = "А я томат";
    let image = Attachment::image_url("https://cont.ws/uploads/pic/2021/2/jW0A4Rs8fKkqffwK.jpg");
    Ok(SendMessageParamsBuilder::new()
        .text(caption)
        .attachment(image)
        .attachment(Attachment::inline_keyboard(btn_restart()?)))
}

/// Преобразует `SendMessageParamsBuilder` в JSON для ответа на callback.
async fn builder_to_json(
    _bot: &MaxClient,
    builder: SendMessageParamsBuilder,
) -> Result<serde_json::Value, maxbot::Error> {
    let params = builder.build();
    let mut body = serde_json::Map::new();
    if !params.text.is_empty() {
        body.insert("text".into(), params.text.into());
    }
    if let Some(fmt) = params.format {
        body.insert("format".into(), fmt.into());
    }
    if let Some(disable) = params.disable_link_preview {
        body.insert("disable_link_preview".into(), disable.into());
    }
    if !params.attachments.is_empty() {
        // Сериализуем вложения напрямую, так как Attachment реализует Serialize
        let attachments_json = serde_json::to_value(&params.attachments)
            .map_err(|e| maxbot::Error::Json(e))?;
        body.insert("attachments".into(), attachments_json);
    }
    Ok(serde_json::Value::Object(body))
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let bot = MaxClient::from_env().expect("MAXBOT_TOKEN not set");
    let mut dp = Dispatcher::new(bot);

    // Обработчик /start и добавления в чат (BotStarted)
    dp.on_command("/start", |ctx| async move {
        let chat_id = ctx.chat_id().unwrap();
        let params = greeting()?.chat_id(chat_id).build();
        ctx.bot().send_message(params).await?;
        Ok(())
    });

    // Обработчик добавления бота в чат
    dp.on_bot_started(|ctx| async move {
        let chat_id = ctx.chat_id().unwrap();
        let params = greeting()?.chat_id(chat_id).build();
        ctx.bot().send_message(params).await?;
        Ok(())
    });

    // Обработчик callback-кнопок
    dp.on_callback(|ctx| async move {
        // Извлекаем payload
        let callback = match &ctx.update {
            maxbot::Update::MessageCallback { callback, .. } => callback,
            _ => return Ok(()),
        };
        let payload = &callback.payload;

        // Формируем JSON для ответа в зависимости от payload
        let message_json = match payload.as_str() {
            "stand" => Some(builder_to_json(ctx.bot(), fruit_menu()?).await?),
            "fruit:apple" => Some(builder_to_json(ctx.bot(), apple_message()?).await?),
            "fruit:pear" => Some(builder_to_json(ctx.bot(), pear_message()?).await?),
            "fruit:orange" => Some(builder_to_json(ctx.bot(), orange_message()?).await?),
            "fruit:tomato" => Some(builder_to_json(ctx.bot(), tomato_message()?).await?),
            _ => None,
        };

        if let Some(json) = message_json {
            // Отвечаем на callback, заменяя сообщение
            ctx.answer_callback_raw(None, Some(json)).await?;
        } else {
            // Неизвестный payload – просто подтверждаем без изменений
            ctx.answer_callback_raw(None, None).await?;
        }
        Ok(())
    });

    println!("🍎🍐🍊🍅 Фруктовый бот запущен! Начните общение с ботом или выполните /start в существующем диалоге. Нажмите Ctrl+C для остановки.");
    dp.start_polling().await;
    Ok(())
}