# maxbot

[![Crates.io](https://img.shields.io/crates/v/maxbot.svg)](https://crates.io/crates/maxbot)
[![Documentation](https://docs.rs/maxbot/badge.svg)](https://docs.rs/maxbot)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

> Rust-библиотека для создания ботов на платформе [MAX](https://max.ru/)  
> API: https://dev.max.ru/docs-api

Библиотека предоставляет удобный, асинхронный и типобезопасный интерфейс ко всем методам API MAX: отправка сообщений (с разбиением длинного текста), загрузка файлов, управление чатами и участниками, inline-клавиатуры, обработка callback-запросов, webhook/long polling, диспетчер событий и многое другое.

**Важно:** API MAX доступен только для российских юридических лиц и индивидуальных предпринимателей. Для получения токена бота обратитесь к документации MAX <https://business.max.ru/self>.

---

## 📦 Установка

Добавьте в `Cargo.toml`:

```toml
[dependencies]
maxbot = "0.5"
tokio = { version = "1", features = ["full"] }
```

---

## 🚀 Быстрый старт

### 1. Пропишите полученный токен доступа к боту

```bash
export MAXBOT_TOKEN="ваш_токен"
```

### 2. Создайте клиент работы с MAX

```rust
use maxbot::MaxClient;

let client = MaxClient::from_env().expect("Токен не найден");
```

Перед началом работы убедитесь, что бот активен и токен корректен.

```rust
let me = client.get_me().await?;
println!("is_bot: {}", me.user.is_bot);
```

### 3. Узнайте идентификатор своей группы (chat_id)

Для отправки сообщений в группу необходимо знать её числовой идентификатор `chat_id`.  
Алгоритм:

1. Создайте группу в MAX (через приложение или веб-интерфейс).
2. Добавьте бота в эту группу.
3. Запросите список чатов, в которых участвует бот, и найдите нужный по полю `title`.

```rust
// Получаем первые 10 чатов
let (chats, _next_marker) = client.get_chats(Some(10), None).await?;

for chat in chats {
    println!("Chat ID: {}, Title: {:?}, Participants: {}", 
             chat.chat_id, 
             chat.title.as_deref().unwrap_or("без названия"),
             chat.participants_count);
}
```

Вы увидите список групп, где присутствует бот. Найдите нужную по заголовку (`title`) и запомните `chat_id`.  
Это число нужно будет подставлять в параметры `chat_id` при вызове `SendMessageParamsBuilder::new().chat_id(...)`.

### 4. Отправьте простое сообщение через построитель

```rust
use maxbot::{MaxClient, SendMessageParamsBuilder};

let mids = client.send_message_builder(
    SendMessageParamsBuilder::new()
        .text("Привет, чат!")
        .chat_id(chat_id)
).await?;
println!("Сообщение отправлено, ID: {:?}", mids);
```

### 5. Можно использовать диспетчер для обработки событий

```rust
use maxbot::{Dispatcher, MaxClient, SendMessageParamsBuilder};

#[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);

    dp.on_command("/start", |ctx| async move {
        ctx.reply_markdown("Привет! Я эхо-бот. Отправь мне любое сообщение.").await?;
        Ok(())
    });

    dp.on_message(|ctx| async move {
        if let Some(text) = ctx.text() {
            ctx.reply_text(text).await?;
        }
        Ok(())
    });

    dp.start_polling().await;
    Ok(())
}
```

### 6. Отправьте сообщение с inline-клавиатурой и обработайте нажатие

```rust
use maxbot::{
    MaxClient, SendMessageParamsBuilder, Attachment,
    InlineKeyboardBuilder, InlineKeyboardButton, GetUpdatesParams,
};

// Отправка сообщения с клавиатурой
let keyboard = InlineKeyboardBuilder::new()
    .button(InlineKeyboardButton::link("е1.ru", "https://www.e1.ru"))
    .button(InlineKeyboardButton::callback("Щёлк!", "my_payload"))
    .button(InlineKeyboardButton::clipboard("Скопировать код", "SECRET"))
    .build();

let mids = client.send_message_builder(
    SendMessageParamsBuilder::new()
        .text("Выбери действие:")
        .chat_id(chat_id)
        .attachment(Attachment::inline_keyboard(keyboard))
).await?;

// Ожидание callback
let params = GetUpdatesParams {
    marker: None,
    limit: Some(10),
    timeout: Some(30),
    types: vec!["message_callback".to_string()],
};
let (updates, _) = client.get_updates(params).await?;
for update in updates {
    if let Some(cb) = update.callback {
        // Ответ на нажатие: заменяем сообщение и показываем уведомление
        client.answer_callback_query(&cb.callback_id, new_message_json, Some("Нажато!")).await?;
    }
}
```

### 7. Отправка файла (изображения, видео, аудио, документа)

```rust
use maxbot::{Attachment, SendMessageParamsBuilder};

// Из локального файла
let builder = SendMessageParamsBuilder::new()
    .text("Вот изображение")
    .chat_id(chat_id)
    .attachment(Attachment::image_local("cat.jpg"));

// Или по прямому URL
let builder = SendMessageParamsBuilder::new()
    .text("Картинка по ссылке")
    .chat_id(chat_id)
    .attachment(Attachment::image_url("https://example.com/image.jpg"));

client.send_message_builder(builder).await?;
```

---

## 🍃 Простые методы отправки

Для самых частых задач есть удобные сокращённые методы (типаж `MaxClientSimpleExt`):

```rust
use maxbot::{MaxClient, MaxClientSimpleExt};

let client = MaxClient::from_env().unwrap();
let chat_id = 123456789;

// Простой текст
client.send_plain_text_to_chat(chat_id, "Привет!").await?;

// Markdown
client.send_markdown_to_chat(chat_id, "**bold** и *italic*").await?;

// HTML
client.send_html_to_chat(chat_id, "<b>bold</b> и <i>italic</i>").await?;

// Изображение с подписью
client.send_image_to_chat(chat_id, "cat.jpg".as_ref(), Some("Кот")).await?;

// Видео
client.send_video_to_chat(chat_id, "video.mp4".as_ref(), None).await?;

// Стикер
client.send_sticker_to_chat(chat_id, "154ed15bb").await?;

// Геолокация
client.send_location_to_chat(chat_id, 55.7558, 37.6176).await?;

// Клавиатура
let keyboard = InlineKeyboardBuilder::new()
    .button(InlineKeyboardButton::callback("Нажми", "data"))
    .build();
client.send_keyboard_to_chat(chat_id, "Выбери:", keyboard).await?;
```

---

## 🔒 Проксирование запросов через nginx (безопасное хранение токенов)

По умолчанию библиотека обращается напрямую к API MAX (`https://platform-api.max.ru`), и токен бота передаётся в заголовке `Authorization` каждого запроса. Это означает, что любой разработчик, имеющий доступ к исходному коду или переменным окружения, может увидеть токен.

**Проблема:** Токен бота — секретная информация. Если вы не хотите раскрывать его всем разработчикам или хранить в файлах конфигурации, можно развернуть собственный прокси-сервер (например, на базе nginx), который будет:

- Принимать запросы от ботов с базовой HTTP-аутентификацией (логин/пароль).
- Подменять заголовок `Authorization` на реальный токен бота MAX.
- Проксировать запросы к API MAX.

**Преимущества:**  
- Разработчики работают только со своим личным логином/паролем, не зная токен бота.  
- При увольнении сотрудника или при завершении им работы над проектом достаточно удалить его учётную запись на прокси-сервере.  
- Токен бота хранится только на сервере с nginx.

#### Пример настройки nginx

Предположим, вы хотите использовать домен `https://max.my-russian-company.ru` как прокси.

1. **Добавьте пользователей** в файл `.htpasswd`:

```bash
htpasswd /etc/nginx/.htpasswd ivanov.i
htpasswd /etc/nginx/.htpasswd petrov.p
```

2. **Создайте конфигурацию nginx** (например, `/etc/nginx/sites-available/max-proxy`):

```nginx
# Соответствие имён пользователей из .htpasswd токенам бота
map $remote_user $backend_token {
    default      "";
    "ivanov.i"   "реальный_токен_бота_1";
    "petrov.p"   "реальный_токен_бота_2";
}

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    server_name max.my-russian-company.ru;

    ssl_certificate /etc/letsencrypt/live/my-russian-company.ru-0001/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/my-russian-company.ru-0001/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

    auth_basic "Restricted access";
    auth_basic_user_file /etc/nginx/.htpasswd;

    location / {
        proxy_pass https://platform-api.max.ru;
        proxy_ssl_verify off;

        proxy_set_header Host platform-api.max.ru;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header Authorization "$backend_token";

        proxy_connect_timeout 60s;
        proxy_send_timeout 60s;
        proxy_read_timeout 60s;
    }
}
```

3. **Активируйте конфигурацию** и перезапустите nginx:

```bash
ln -s /etc/nginx/sites-available/max-proxy /etc/nginx/sites-enabled/
nginx -t
systemctl restart nginx
```

#### Использование в коде

В вашем приложении нужно указать библиотеке использовать собственный базовый URL (адрес вашего прокси-сервера) и передавать логин/пароль через стандартный заголовок `Authorization: Basic ...`. Библиотека `maxbot` позволяет это сделать:

```rust
use maxbot::{MaxClient, set_global_base_url};

// 1. Устанавливаем глобальный базовый URL (все клиенты будут использовать прокси)
set_global_base_url("https://max.my-russian-company.ru");

// 2. Создаём клиент с токеном базовой авторизации HTTP
let client = MaxClient::new("Basic aXZhbm92Lmk6bXlfc2VjcmV0X3Bhc3N3b3JkMQ==");

// 3. Все дальнейшие вызовы автоматически используют прокси.
let me = client.get_me().await?;
```

> **Важно:** В этом режиме токен бота нигде в коде не фигурирует — он остаётся только на сервере nginx. Разработчики используют свои личные учётные данные.

Если вам нужно переопределить базовый URL только для конкретного клиента, используйте метод `set_base_url`:

```rust
let mut client = MaxClient::new("Basic cGV0cm92LnA6bXlfc2VjcmV0X3Bhc3N3b3JkMg==");
client.set_base_url("https://max.my-russian-company.ru");
```

---

## 📚 Полная документация API

Все методы сгруппированы в `impl MaxClient`:

| Модуль | Методы |
|--------|--------|
| **bot** | `get_me()` – информация о боте |
| **chats** | `get_chat`, `get_chats`, `update_chat`, `delete_chat`, `leave_chat`, `get_chat_members`, `get_chat_members_by_ids`, `get_chat_admins`, `add_chat_admin`, `add_chat_admins`, `remove_chat_admin`, `remove_chat_admins`, `add_chat_members`, `remove_chat_member`, `get_bot_member`, `send_chat_action`, `get_pinned_message`, `pin_message`, `unpin_message` |
| **messages** | `send_message` (и `send_message_builder`), `forward_message`, `delete_message`, `delete_messages`, `get_messages`, `edit_message`, `edit_multimessage` |
| **simple** | `send_plain_text_to_chat`, `send_markdown_to_chat`, `send_html_to_chat`, `send_image_to_chat`, `send_video_to_chat`, `send_audio_to_chat`, `send_file_to_chat`, `send_sticker_to_chat`, `send_location_to_chat`, `send_contact_to_chat`, `send_share_to_chat`, `send_keyboard_to_chat` |
| **videos** | `get_video_info`, `download_video` |
| **uploads** | `upload_file` – загрузка файла и получение токена |
| **updates** | `get_updates`, `answer_callback_query` |
| **subscriptions** | `get_subscriptions`, `create_subscription`, `delete_subscription` |

Все методы асинхронны, используют `reqwest` и автоматически соблюдают лимит **30 запросов в секунду** (rate limiting). Длинные тексты разбиваются на фрагменты до 4000 символов с сохранением целостности слов и предложений.

---

## 🧪 Примеры

Полные примеры в папке `examples/`:

- `check-bot.rs` – проверка работоспособности бота (выводит `true` или `false`)
- `me-demo.rs` – информация о боте
- `echo-bot.rs` – простой эхо-бот
- `fruits-bot.rs` – демонстрация бота с обработкой нажатия на кнопки
- `video-info-bot.rs` – демонстрация бота с получением информации о видео
- `get-chats-demo.rs` – получение списка чатов и их идентификаторов
- `get-chat-demo.rs` – получение информации о групповом чате
- `update-chat-demo.rs` – изменение информации о групповом чате (название, иконка, закрепление сообщения)
- `delete-chat-demo.rs` – удаление группового чата (с обработкой ошибок)
- `leave-chat-demo.rs` – выход бота из чата (с фиктивным ID и обработкой ошибки)
- `get-bot-member-demo.rs` – информация о членстве бота в групповом чате (права, дата вступления и т.д.)
- `get-chat-members-demo.rs` – получение списка участников чата (постранично или по ID)
- `get-chat-admins-demo.rs` – получение списка администраторов чата с правами
- `add-chat-admin-demo.rs` – назначение одного или нескольких администраторов
- `remove-chat-admin-demo.rs` – снятие прав администратора с пользователей
- `get-messages-demo.rs` – получение истории сообщений чата (если доступно)
- `send-text-demo.rs` – отправка простого текста
- `send-long-text-demo.rs` – разбиение длинного текста
- `send-picture-demo.rs` – отправка изображения
- `send-picture-only-demo.rs` – отправка изображения без текста
- `send-video-demo.rs` – отправка видео
- `send-audio-demo.rs` – отправка аудио
- `send-file-any-demo.rs` – отправка произвольного файла
- `upload-file-demo.rs` – предварительная загрузка файла и использование токена
- `get-video-info-demo.rs` – получение метаданных видео
- `send-inline-keyboard-demo.rs` – сообщение с клавиатурой
- `combined-attachments-demo.rs` – все типы вложений (стикер, контакт, гео, ссылка, клавиатура)
- `send-chat-actions-demo.rs` – демонстрация отправки действий бота (набор текста, отправка медиа и т.д.)
- `pin-message-demo.rs` – закрепление и открепление сообщения
- `forward-demo.rs` – пересылка сообщения
- `delete-messages-demo.rs` – отправка и удаление
- `edit-message-demo.rs` – редактирование сообщения
- `full-demo.rs` – комплексная демонстрация большинства возможностей
- `stress-test.rs` – стресс-тест ограничения RPS (отправка сообщений до ошибки 429)

Запуск примера:

```bash
export MAXBOT_TOKEN="ваш_токен"
export CHAT_ID=-123456789
cargo run --example stress-test -- 10
```

---

## ✅ Что уже реализовано

- [x] Базовая HTTP-обёртка с rate limiting (30 RPS)
- [x] Получение информации о боте (`/me`)
- [x] Управление чатами: получение, обновление (название, иконка, закрепление, уведомления), удаление, выход
- [x] Управление участниками: получение списка (постранично и по ID), добавление, удаление
- [x] Полное управление администраторами: получение списка, добавление (одного или нескольких с правами и псевдонимом), снятие прав (одного или нескольких)
- [x] Отправка сообщений (текст, markdown, html, reply)
- [x] Разбиение длинного текста на фрагменты (сохранение целостности слов/предложений)
- [x] Отправка файлов (изображения, видео, аудио, документы) — из файла, по токену или по прямому URL
- [x] Предварительная загрузка файлов и повторное использование токенов
- [x] Получение информации о видео (`/videos/{token}`)
- [x] Пересылка сообщений
- [x] Удаление сообщений (одиночное и массовое)
- [x] Получение истории сообщений чата (`/messages`)
- [x] Редактирование сообщений (одиночных и многофрагментных)
- [x] Закрепление/открепление сообщений
- [x] Отправка действий бота в чате (typing_on, sending_photo, ...)
- [x] Long polling (`/updates`) и диспетчер событий (с фильтрами, `on_command`, `on_message`, `on_callback` и т.д.)
- [x] Ответ на callback-запросы (inline-кнопки) – замена сообщения и уведомления
- [x] Webhook-подписки (создание, удаление, список)
- [x] Удобные Builder'ы для параметров сообщений и клавиатур
- [x] Простые методы отправки (типаж `MaxClientSimpleExt`)
- [x] Поддержка всех типов вложений: стикеры, контакты, геолокация, share, файлы, изображения, видео, аудио
- [x] Кнопка `clipboard` (копирование в буфер обмена без callback)

---

## 📄 Лицензия

Проект распространяется под лицензией MIT.

---

## 🤝 Вклад

Принимаются pull request'ы. Для крупных изменений сначала откройте issue для обсуждения.

---

## 🌐 Полезные ссылки

- [Документация API MAX](https://dev.max.ru/docs-api)
- [Репозиторий библиотеки](https://gitflic.ru/project/perdumonocle/maxbot)
- [Crates.io](https://crates.io/crates/maxbot)
