Skip to main content

fruits_bot/
fruits-bot.rs

1//! Демо-бот «Фрукты».
2//!
3//! При добавлении в чат или команде /start показывает приветствие с кнопкой «Встать».
4//! При нажатии на кнопку — меню с фруктами.
5//! При выборе фрукта — описание и картинка.
6//! Нажатие на томат приводит к перезапуску.
7//!
8//! # Запуск
9//! ```bash
10//! export MAXBOT_TOKEN="ваш_токен"
11//! cargo run --example fruits-bot
12//! ```
13
14use maxbot::{
15    Attachment, Dispatcher, InlineKeyboard, InlineKeyboardButton, InlineKeyboardBuilder, MaxClient,
16    SendMessageParamsBuilder,
17};
18
19/// Сборка inline-клавиатуры из списка кнопок.
20fn make_keyboard(buttons: Vec<InlineKeyboardButton>) -> Result<InlineKeyboard, maxbot::KeyboardValidationError> {
21    let mut builder = InlineKeyboardBuilder::new();
22    for btn in buttons {
23        builder = builder.button(btn);
24    }
25    builder.build()
26}
27
28/// Кнопка «Встать».
29fn btn_stand() -> InlineKeyboardButton {
30    InlineKeyboardButton::callback("Встать", "stand")
31}
32
33/// Кнопки выбора фруктов (🍏 🍐 🍊 🍅).
34fn fruit_buttons() -> Result<InlineKeyboard, maxbot::KeyboardValidationError> {
35    make_keyboard(vec![
36        InlineKeyboardButton::callback("🍏", "fruit:apple"),
37        InlineKeyboardButton::callback("🍐", "fruit:pear"),
38        InlineKeyboardButton::callback("🍊", "fruit:orange"),
39        InlineKeyboardButton::callback("🍅", "fruit:tomato"),
40    ])
41}
42
43/// Кнопка «Начать заново» (для томата).
44fn btn_restart() -> Result<InlineKeyboard, maxbot::KeyboardValidationError> {
45    make_keyboard(vec![InlineKeyboardButton::callback("Начать заново", "stand")])
46}
47
48/// Сообщение приветствия.
49fn greeting() -> Result<SendMessageParamsBuilder, maxbot::KeyboardValidationError> {
50    Ok(SendMessageParamsBuilder::new()
51        .text("Ну-ка, фрукты, встаньте в ряд!")
52        .attachment(Attachment::inline_keyboard(
53            make_keyboard(vec![btn_stand()])?,
54        )))
55}
56
57/// Сообщение с фруктовым меню.
58fn fruit_menu() -> Result<SendMessageParamsBuilder, maxbot::KeyboardValidationError> {
59    Ok(SendMessageParamsBuilder::new()
60        .text("Перед вами, друзья, фруктов дружная семья.")
61        .attachment(Attachment::inline_keyboard(fruit_buttons()?)))
62}
63
64/// Сообщение о яблоке.
65fn apple_message() -> Result<SendMessageParamsBuilder, maxbot::KeyboardValidationError> {
66    let caption = "Сочное, спелое, наливное. Яблоко — вот кто я!";
67    let image = Attachment::image_url("https://i0.wp.com/static.kinoafisha.info/upload/articles/362065694948.jpg");
68    Ok(SendMessageParamsBuilder::new()
69        .text(caption)
70        .attachment(image)
71        .attachment(Attachment::inline_keyboard(fruit_buttons()?)))
72}
73
74/// Сообщение о груше.
75fn pear_message() -> Result<SendMessageParamsBuilder, maxbot::KeyboardValidationError> {
76    let caption = "Я спелая грушка — яблоку подружка!";
77    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");
78    Ok(SendMessageParamsBuilder::new()
79        .text(caption)
80        .attachment(image)
81        .attachment(Attachment::inline_keyboard(fruit_buttons()?)))
82}
83
84/// Сообщение об апельсине.
85fn orange_message() -> Result<SendMessageParamsBuilder, maxbot::KeyboardValidationError> {
86    let caption = "Я — пузатый апельсин. Солнышка весёлый сын.";
87    let image = Attachment::image_url("https://click-or-die.ru/app/uploads/2024/07/apelsinn.jpg");
88    Ok(SendMessageParamsBuilder::new()
89        .text(caption)
90        .attachment(image)
91        .attachment(Attachment::inline_keyboard(fruit_buttons()?)))
92}
93
94/// Сообщение о томате.
95fn tomato_message() -> Result<SendMessageParamsBuilder, maxbot::KeyboardValidationError> {
96    let caption = "А я томат";
97    let image = Attachment::image_url("https://cont.ws/uploads/pic/2021/2/jW0A4Rs8fKkqffwK.jpg");
98    Ok(SendMessageParamsBuilder::new()
99        .text(caption)
100        .attachment(image)
101        .attachment(Attachment::inline_keyboard(btn_restart()?)))
102}
103
104/// Преобразует `SendMessageParamsBuilder` в JSON для ответа на callback.
105async fn builder_to_json(
106    _bot: &MaxClient,
107    builder: SendMessageParamsBuilder,
108) -> Result<serde_json::Value, maxbot::Error> {
109    let params = builder.build();
110    let mut body = serde_json::Map::new();
111    if !params.text.is_empty() {
112        body.insert("text".into(), params.text.into());
113    }
114    if let Some(fmt) = params.format {
115        body.insert("format".into(), fmt.into());
116    }
117    if let Some(disable) = params.disable_link_preview {
118        body.insert("disable_link_preview".into(), disable.into());
119    }
120    if !params.attachments.is_empty() {
121        // Сериализуем вложения напрямую, так как Attachment реализует Serialize
122        let attachments_json = serde_json::to_value(&params.attachments)
123            .map_err(|e| maxbot::Error::Json(e))?;
124        body.insert("attachments".into(), attachments_json);
125    }
126    Ok(serde_json::Value::Object(body))
127}
128
129#[tokio::main]
130async fn main() -> Result<(), Box<dyn std::error::Error>> {
131    let bot = MaxClient::from_env().expect("MAXBOT_TOKEN not set");
132    let mut dp = Dispatcher::new(bot);
133
134    // Обработчик /start и добавления в чат (BotStarted)
135    dp.on_command("/start", |ctx| async move {
136        let chat_id = ctx.chat_id().unwrap();
137        let params = greeting()?.chat_id(chat_id).build();
138        ctx.bot().send_message(params).await?;
139        Ok(())
140    });
141
142    // Обработчик добавления бота в чат
143    dp.on_bot_started(|ctx| async move {
144        let chat_id = ctx.chat_id().unwrap();
145        let params = greeting()?.chat_id(chat_id).build();
146        ctx.bot().send_message(params).await?;
147        Ok(())
148    });
149
150    // Обработчик callback-кнопок
151    dp.on_callback(|ctx| async move {
152        // Извлекаем payload
153        let callback = match &ctx.update {
154            maxbot::Update::MessageCallback { callback, .. } => callback,
155            _ => return Ok(()),
156        };
157        let payload = &callback.payload;
158
159        // Формируем JSON для ответа в зависимости от payload
160        let message_json = match payload.as_str() {
161            "stand" => Some(builder_to_json(ctx.bot(), fruit_menu()?).await?),
162            "fruit:apple" => Some(builder_to_json(ctx.bot(), apple_message()?).await?),
163            "fruit:pear" => Some(builder_to_json(ctx.bot(), pear_message()?).await?),
164            "fruit:orange" => Some(builder_to_json(ctx.bot(), orange_message()?).await?),
165            "fruit:tomato" => Some(builder_to_json(ctx.bot(), tomato_message()?).await?),
166            _ => None,
167        };
168
169        if let Some(json) = message_json {
170            // Отвечаем на callback, заменяя сообщение
171            ctx.answer_callback_raw(None, Some(json)).await?;
172        } else {
173            // Неизвестный payload – просто подтверждаем без изменений
174            ctx.answer_callback_raw(None, None).await?;
175        }
176        Ok(())
177    });
178
179    println!("🍎🍐🍊🍅 Фруктовый бот запущен! Начните общение с ботом или выполните /start в существующем диалоге. Нажмите Ctrl+C для остановки.");
180    dp.start_polling().await;
181    Ok(())
182}