Metadata-Version: 2.4
Name: aiogram-max
Version: 1.1.1
Summary: Запускает aiogram-бота в мессенджере MAX: подменяем сессию, код бота не трогаем
Project-URL: Homepage, https://github.com/juntatalor/aiogram-max
Project-URL: Source, https://github.com/juntatalor/aiogram-max
Project-URL: Issues, https://github.com/juntatalor/aiogram-max/issues
Project-URL: Changelog, https://github.com/juntatalor/aiogram-max/releases
Author-email: Sergey Borisov <juntatalor@gmail.com>
License-Expression: MIT
License-File: LICENSE
Keywords: aiogram,bot,max,messenger,telegram
Classifier: Development Status :: 4 - Beta
Classifier: Framework :: AsyncIO
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Communications :: Chat
Classifier: Typing :: Typed
Requires-Python: >=3.12
Requires-Dist: aiogram>=3.20
Requires-Dist: httpx>=0.27
Provides-Extra: dev
Requires-Dist: mypy>=1.13; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Requires-Dist: ruff>=0.8; extra == 'dev'
Description-Content-Type: text/markdown

# aiogram-max

[![PyPI](https://img.shields.io/pypi/v/aiogram-max)](https://pypi.org/project/aiogram-max/)
[![Python](https://img.shields.io/pypi/pyversions/aiogram-max)](https://pypi.org/project/aiogram-max/)
[![CI](https://github.com/juntatalor/aiogram-max/actions/workflows/ci.yml/badge.svg)](https://github.com/juntatalor/aiogram-max/actions/workflows/ci.yml)
[![License](https://img.shields.io/pypi/l/aiogram-max)](https://github.com/juntatalor/aiogram-max/blob/master/LICENSE)
[![Checked with mypy](https://img.shields.io/badge/mypy-strict-2a6db2)](https://mypy-lang.org/)
[![Ruff](https://img.shields.io/badge/ruff-passing-261230)](https://docs.astral.sh/ruff/)

Запускает бота, написанного на **aiogram**, в мессенджере **MAX** — без правок кода бота.

```python
from aiogram import Dispatcher
from aiogram_max import make_bot

bot = make_bot(max_token="...")  # единственная изменённая строка
await Dispatcher().start_polling(bot)
```

Роутеры, фильтры (`Command`, магический `F`), FSM, middleware и типы остаются
родными aiogram-овскими.

## Статус

**Рабочая библиотека, проверенная на живом MAX.** Настоящий
`Dispatcher.start_polling` ходит в MAX Bot API, хендлеры срабатывают, ответы
и `callback.answer()` доезжают до мессенджера.

Покрыто 40 методов — всё, чему нашёлся аналог у платформы: сообщения и
правки, вложения всех типов, клавиатуры и callback'и, группы с участниками и
пинами, вебхук, команды бота, разметка. Каждый проверен на реальном боте, а
не только тестами.

## Как это работает

Весь исходящий трафик aiogram проходит через одну функцию:

```
BaseSession.make_request(bot, method: TelegramMethod, timeout) -> TelegramType
```

`Bot` не ходит в сеть сам: он собирает типизированный объект метода
(`SendMessage`, `GetUpdates`, `AnswerCallbackQuery`, …) и отдаёт его сессии.
Мы подменяем сессию — и перехватываем поток целиком:

```
Бот на aiogram  (код не меняется)
  Dispatcher / Router / фильтры / FSM
  Bot.send_message(...)
        │  TelegramMethod
        ▼
  MaxSession(BaseSession)     ← вся библиотека здесь
        │  HTTP
        ▼
  MAX Bot API
```

Long polling тоже работает штатно: aiogram сам зовёт `GetUpdates`, а сессия
ходит в `GET /updates?marker=` и собирает из событий MAX валидные
aiogram-`Update`.

Отличие от [obabot](https://github.com/Korean-DOG/obabot), решающего похожую
задачу: там своя система типов (`obabot.Message`, `obabot.FSMContext`) и
импорты в боте меняются. Здесь типы остаются aiogram-овскими, подменяется
только транспорт.

## Что уже проверено тестами

| Что | Как проверено |
| --- | --- |
| MAX-событие → валидный aiogram `Update` | `test_get_updates_returns_aiogram_updates` |
| Роутер + фильтр `Command` | `test_dispatcher_routes_message_to_handler` |
| Inline-клавиатура → MAX `inline_keyboard` | там же, сверка тела запроса |
| Callback + магический фильтр `F.data` | `test_callback_query_flows_through_aiogram` |
| FSM (`StatesGroup`, `FSMContext`) | `test_fsm_state_survives_platform_swap` |
| Родной `Dispatcher.start_polling` | `test_native_polling_loop_delivers_max_events` |
| Правка сообщения (`seq` ↔ `mid`) | `test_edit_message_uses_max_mid` |
| Неподдерживаемый метод | `test_unsupported_method_raises_in_strict_mode` |
| Живые payload'ы MAX | `tests/test_live_fixtures.py` (6 тестов на снятых с API событиях) |
| Потеря кнопки без аналога | `test_dropped_button_warns_but_keeps_the_rest` |
| Маппинг parse_mode / notify / reply | `test_supported_params_are_mapped_not_dropped` |
| Вложения: загрузка и отправка | `test_send_photo_uploads_and_attaches_token` |
| Ожидание обработки файла на стороне MAX | `test_send_photo_waits_until_attachment_is_ready` |
| MarkdownV2 → html (включая подчёркивание) | `tests/test_markup.py`, 11 тестов |
| `entities` → html без потерь | `test_entities_are_no_longer_dropped_silently` |
| `MarkupPolicy.RAW` не трогает текст | `test_raw_policy_leaves_text_alone` |

## Неподдерживаемое в MAX

У MAX нет части возможностей Telegram. Политика задаётся при создании бота:

```python
make_bot(token)  # WARN, по умолчанию
make_bot(token, unsupported=UnsupportedPolicy.STRICT)
```

Расхождения бывают трёх видов:

1. **Метода нет вовсе** (`SendPoll`, `SendDice`). `WARN` — предупреждение и
   пропуск, `STRICT` — `UnsupportedByMax` с именем метода.
2. **Метод есть, а параметра нет** (кнопка `web_app`, `switch_inline_query`).
   Такая кнопка выбрасывается, остальные остаются: `WARN` пишет в лог что
   именно потерялось, `STRICT` падает. Молча не выбрасываем никогда —
   «кнопка исчезла, а бот не упал» ищется потом часами.
3. **Семантика другая** — это работа слоя конвертации, а не политики:
   `message_id` (int) ↔ MAX `mid` (str) сшиваются через `seq`.

Где аналог есть — параметр переводится, а не теряется:

| aiogram | MAX |
| --- | --- |
| `parse_mode="HTML"` | `format: html` |
| `parse_mode="MarkdownV2"` | конвертация в html, см. «Разметка» |
| `entities=[...]` | конвертация в html, см. «Разметка» |
| `disable_notification=True` | `notify: false` |
| `reply_to_message_id` | `link: {type: reply, mid}` |

Отдельный случай — пустой `callback.answer()`. В Telegram он снимает
индикатор загрузки на кнопке и стоит почти в каждом хендлере; у MAX такой
семантики нет, `POST /answers` с пустым телом отвечает `400 proto.payload`
(«`message` or `notification` required»). Запрос не уходит вовсе. С текстом —
`answer("Принято")` — уходит как `notification`.

## Разметка

Работает из коробки: бот шлёт привычный телеграмный `parse_mode` или
`entities`, библиотека переводит это в html, который MAX понимает.

```python
await bot.send_message(chat_id, "*жирный* и __подчёркнутый__", parse_mode="MarkdownV2")
# в MAX уедет: {"text": "<b>жирный</b> и <u>подчёркнутый</u>", "format": "html"}
```

Форматируете под MAX сами — отключите посредника:

```python
make_bot(token, markup=MarkupPolicy.RAW)  # текст уйдёт как есть
```

**Почему html, а не markdown.** MAX принимает оба формата, но markdown у него
CommonMark, а в CommonMark `__текст__` — это жирный. В MarkdownV2 та же запись
означает подчёркивание, и перевод «markdown в markdown» молча превратил бы
одно в другое. Тип `underline` MAX отдаёт только за `<u>`. Вдобавок html не
требует правил экранирования, которых в MarkdownV2 полтора десятка, причём
внутри кодовых спанов они другие.

**Что MAX действительно поддерживает.** Официальная страница «Форматирование»
отдаёт 404, поэтому таблица снята с живого API: бот отправил пробу, MAX
вернул разобранную разметку.

| Разметка | `format: html` | `format: markdown` |
| --- | --- | --- |
| жирный | `<b>` → `strong` | `**x**` → `strong` |
| курсив | `<i>` → `emphasized` | `*x*`, `_x_` → `emphasized` |
| подчёркнутый | `<u>` → `underline` | **нет**: `__x__` даёт `strong` |
| зачёркнутый | `<s>` → `strikethrough` | `~~x~~` → `strikethrough` |
| моноширинный | `<code>`, `<pre>` → `monospaced` | `` `x` `` → `monospaced` |
| ссылка | `<a href>` → `link` | `[x](url)` → `link` |
| заголовок | не проверялся | `# x` → `heading` |
| цитата | `<blockquote>` — **не распознаётся** | `> x` — **не распознаётся** |
| списки, спойлер | нет | нет |

Чего у MAX нет вовсе — спойлер, цитата, кастомные эмодзи — проходит через ту
же политику `unsupported`: `WARN` пишет в лог что именно потерялось, `STRICT`
падает. Текст при этом сохраняется, теряется только оформление.

## Грабли MAX, которые стоит знать

* **Индикатор набора у MAX есть**, вопреки расхожему мнению: работает и в
  личке, и в группе. Только называется иначе — телеграмное `typing` он не
  понимает, нужно `typing_on`, и библиотека переводит сама.
* **Вложение не готово сразу после заливки.** Отправка отвечает
  `400 attachment.not.ready`, пока MAX обрабатывает файл. Библиотека ждёт и
  повторяет сама, но знать об этом стоит: в Telegram такого шага нет.
* **`recipient.user_id` — это получатель сообщения, а не собеседник.** В
  событии от пользователя там лежит id бота, в сообщении бота пользователю —
  id пользователя. Подставите его как `chat_id` — бот начнёт молча отвечать
  сам себе. `chat_id` в диалоге MAX присылает, брать надо только его.
* **Позиция в ленте — `marker`, и она же `update_id`.** MAX отдаёт в ответе
  «следующую ожидаемую» позицию, что совпадает по смыслу с телеграмным
  `offset`. Поэтому `update_id` события — его позиция в ленте (`marker - 1`
  для последнего события пачки), а не порядковый номер внутри процесса.
  Для бота с дедупом по `update_id` это принципиально: счётчик из памяти
  после рестарта начинается заново, и свежие события выглядят уже
  обработанными.
* **`seq` — не маленький счётчик.** Живое значение: `116993690454357274`.
  В `int64` влезает и aiogram переваривает, но узкое поле БД (`integer`
  вместо `bigint`) на этом сломается.
* **`/start` приходит обычным `message_created`**, а не отдельным событием —
  фильтр `Command` работает без спецобработки.
* **Идентификатор сообщения двойной**: aiogram знает `message_id` (int из
  `seq`), MAX правит и удаляет по строковому `mid`. Соответствие держит
  сессия в памяти (последние 10 000 сообщений), поэтому правка переживает
  рестарт только в пределах жизни процесса. Обратного преобразования
  `mid → seq` MAX не даёт, так что иначе никак.

## Что ещё не сделано
* Webhook (у MAX это рекомендованный для прода транспорт).
* Разметка подписей (`caption`, `caption_entities`) — вместе с вложениями.
* Покрыто 40 методов из 185 в aiogram — всё, чему есть аналог у MAX. Что уже работает, что можно добавить и
  чего в MAX нет вовсе — в [docs/method-coverage.md](docs/method-coverage.md).

## Установка

```bash
pip install aiogram-max      # после публикации; пока — pip install -e .
```

Python 3.12+, единственные зависимости — `aiogram` и `httpx`. Библиотека
типизирована (`py.typed`), mypy strict проходит без ignore'ов.

## Разработка

```bash
uv venv && uv pip install -e ".[dev]"
.venv/bin/ruff check . && .venv/bin/mypy && .venv/bin/pytest -q
```

Как добавить метод и чем `NotImplementedYet` отличается от `UnsupportedByMax` —
в [CONTRIBUTING.md](CONTRIBUTING.md).

## Лицензия

MIT
