# obabot

**Универсальная асинхронная библиотека для ботов Telegram и Max с API, совместимым с aiogram.**

Напишите код бота один раз, запустите его на Telegram, Max или на обеих платформах одновременно!

## Возможности

* **Минимальные изменения при миграции** — меняются только импорты и инициализация
* **API, совместимый с aiogram** — используйте знакомые декораторы, фильтры и FSM
* **Поддержка нескольких платформ** — работайте на Telegram, Max или на обеих одновременно
* **Нативная производительность для Telegram** — без накладных расходов (прямой aiogram)
* **Прозрачные адаптеры для Max** — API Max автоматически преобразуется в интерфейс, похожий на aiogram

## Установка

```bash
pip install obabot
```

### Зависимости для платформ

После установки obabot необходимо установить библиотеки для платформ:

```bash
# Для поддержки Telegram
pip install aiogram>=3.0.0

# Для поддержки Max
pip install umaxbot>=0.1.7

# Или обе
pip install aiogram>=3.0.0 umaxbot>=0.1.7
```

**Рекомендуемая версия aiogram:** `3.24` (последняя стабильная версия 3.x, используется в тестах)

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

### Только Telegram

```python
from obabot import create_bot
from obabot.filters import Command

bot, dp, router = create_bot(tg_token="ВАШ_ТЕЛЕГРАМ_ТОКЕН")

@router.message(Command("start"))
async def start(message):
    await message.answer(f"Привет с {message.platform}!")

await dp.start_polling(bot)
```

### Только Max

```python
from obabot import create_bot
from obabot.filters import Command

# Просто измените аргумент токена!
bot, dp, router = create_bot(max_token="ВАШ_МАКС_ТОКЕН")

@router.message(Command("start"))
async def start(message):
    await message.answer(f"Привет с {message.platform}!")

await dp.start_polling(bot)
```

### Обе платформы (двойной режим)

```python
from obabot import create_bot
from obabot.filters import Command

# Передайте оба токена для режима двух платформ
bot, dp, router = create_bot(
    tg_token="ВАШ_ТЕЛЕГРАМ_ТОКЕН",
    max_token="ВАШ_МАКС_ТОКЕН"
)

@router.message(Command("start"))
async def start(message):
    # message.platform показывает, с какой платформы пришло сообщение
    await message.answer(f"Привет с {message.platform.upper()}!")

# Это запускает polling на ОБЕИХ платформах
await dp.start_polling(bot)
```

## Миграция с aiogram

Миграция существующего бота на aiogram очень проста — просто измените импорты и инициализацию!

### До (aiogram)

```python
from aiogram import Bot, Dispatcher, Router
from aiogram.filters import Command
from aiogram.fsm.state import State, StatesGroup
from aiogram.fsm.context import FSMContext

bot = Bot(token="TOKEN")
dp = Dispatcher()
router = Router()
dp.include_router(router)

@router.message(Command("start"))
async def start(message):
    await message.answer("Привет!")

await dp.start_polling(bot)
```

### После (obabot)

```python
from obabot import create_bot
from obabot.filters import Command
from obabot.fsm import State, StatesGroup, FSMContext

bot, dp, router = create_bot(tg_token="TOKEN")

@router.message(Command("start"))
async def start(message):
    await message.answer("Привет!")

await dp.start_polling(bot)
```

**Изменения:**

* ✅ Изменены импорты: `aiogram` → `obabot`
* ✅ Изменена инициализация: `Bot/Dispatcher/Router` → `create_bot()`
* ✅ **Всё остальное на 100% идентично!**

См. `examples/aiogram_original.py` и `examples/aiogram_migrated.py` для полного примера миграции.

## Справочник API

### `create_bot()`

Основная фабричная функция для создания бота.

```python
def create_bot(
    tg_token: str | None = None,
    max_token: str | None = None,
    fsm_storage: BaseStorage | None = None,
    test_mode: bool | None = None,
) -> tuple[ProxyBot | StubBot, ProxyDispatcher | Dispatcher, ProxyRouter | Router]:
    ...
```

**Аргументы:**

* `tg_token` - токен Telegram бота (опционально; в тестовом режиме не требуется)
* `max_token` - токен Max бота (опционально; в тестовом режиме не требуется)
* `fsm_storage` - хранилище для FSM состояний (опционально). Будет использоваться всеми платформами.
* `test_mode` - если `True`, включается тестовый режим (без токенов и сетевых вызовов). Если `None`, используется переменная окружения `TESTING=1`.

**Возвращает:** Кортеж `(bot, dispatcher, router)`

**Режимы:**

* Только `tg_token` → режим Telegram
* Только `max_token` → режим Max
* Оба токена → режим двух платформ
* `test_mode=True` или `TESTING=1` → тестовый режим

### Обработчики

Используйте те же декораторы, что и в aiogram:

```python
# Вариант 1: Использование router (рекомендуется)
@router.message(Command("start"))
async def cmd_start(message):
    await message.answer("Привет!")

# Вариант 2: Использование dispatcher (тоже работает, как в aiogram)
@dp.message(Command("start"))
async def cmd_start_v2(message):
    await message.answer("Привет!")

# Оба работают с фильтрами
@router.message(F.text)
async def text_handler(message):
    await message.answer(f"Вы сказали: {message.text}")

@router.callback_query(F.data == "button")
async def callback_handler(callback):
    await callback.answer("Кнопка нажата!")
    await callback.message.edit_text("Обновлено!")
```

### Объект Message

Все сообщения имеют эти свойства (как в aiogram):

```python
message.text          # Текст сообщения
message.from_user     # Пользователь, отправивший сообщение
message.chat          # Объект чата
message.message_id    # ID сообщения
message.platform      # "telegram" или "max"

# Методы
await message.answer("Текст ответа")
await message.reply("Ответить на это сообщение")
await message.delete()
await message.edit_text("Новый текст")
```

### FSM (Конечный автомат состояний)

```python
from obabot.fsm import State, StatesGroup, FSMContext

class Form(StatesGroup):
    name = State()
    age = State()

@router.message(Command("start"))
async def start(message, state: FSMContext):
    await state.set_state(Form.name)
    await message.answer("Как тебя зовут?")

@router.message(Form.name)
async def process_name(message, state: FSMContext):
    await state.update_data(name=message.text)
    await state.set_state(Form.age)
    await message.answer("Сколько тебе лет?")
```

### Клавиатуры

```python
from obabot.types import InlineKeyboardMarkup, InlineKeyboardButton

keyboard = InlineKeyboardMarkup(inline_keyboard=[
    [
        InlineKeyboardButton(text="Кнопка 1", callback_data="btn1"),
        InlineKeyboardButton(text="Кнопка 2", callback_data="btn2"),
    ]
])

await message.answer("Выберите:", reply_markup=keyboard)
```

### Фильтры

```python
from obabot.filters import Command, F, StateFilter

@router.message(Command("start", "help"))  # Несколько команд
@router.message(F.text.startswith("!"))     # Магический фильтр
@router.message(F.photo)                   # Сообщения с фото
@router.callback_query(F.data == "click") # Callback data
```

## Архитектура

```
obabot/
├── obabot/
│   ├── __init__.py          # create_bot, BPlatform
│   ├── factory.py           # Реализация create_bot()
│   ├── proxy/               # Proxy классы для мультиплексирования
│   │   ├── bot.py           # ProxyBot
│   │   ├── dispatcher.py    # ProxyDispatcher
│   │   └── router.py        # ProxyRouter
│   ├── adapters/            # Адаптеры Max → aiogram
│   │   ├── message.py       # MaxMessageAdapter
│   │   ├── max_callback.py  # MaxCallbackQuery
│   │   ├── telegram_callback.py # TelegramCallbackQuery
│   │   ├── user.py          # MaxUserAdapter, MaxChatAdapter
│   │   └── keyboard.py      # Конвертер клавиатур
│   ├── platforms/           # Реализации платформ
│   │   ├── base.py          # BasePlatform ABC
│   │   ├── telegram.py      # TelegramPlatform (нативный aiogram)
│   │   └── max.py           # MaxPlatform (адаптированный)
│   ├── filters.py           # Реэкспортированные фильтры aiogram
│   ├── fsm.py               # Реэкспортированные компоненты FSM
│   └── types.py             # Enum BPlatform, реэкспорты типов
└── examples/
    ├── aiogram_original.py      # Оригинальный бот на aiogram
    ├── aiogram_migrated.py      # Мигрированный на obabot
    ├── telegram_only.py         # Telegram с FSM и клавиатурами
    ├── max_only.py              # Max с FSM и клавиатурами
    └── dual_platform.py         # Обе платформы одновременно
└── tests/
    ├── test_test_mode.py        # Тесты тестового режима
    ├── test_basic.py            # Базовые тесты
    └── ...
```

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

1. **Telegram (нативный)**: Сообщения проходят напрямую к обработчикам aiogram. Добавляется только атрибут `message.platform`.
2. **Max (адаптированный)**: Сообщения оборачиваются в `MaxMessageAdapter`, который предоставляет интерфейс, совместимый с aiogram.
3. **Режим двух платформ**: Обе платформы работают параллельно. Каждый обработчик регистрируется на обеих платформах.

## Примеры

См. директорию `examples/` для полных рабочих примеров:

* **`aiogram_original.py`** - Полный пример бота на оригинальном aiogram 3.x
* **`aiogram_migrated.py`** - Тот же бот, мигрированный на obabot
* **`telegram_only.py`** - Бот для Telegram с FSM и клавиатурами
* **`max_only.py`** - Тот же бот для Max
* **`dual_platform.py`** - Бот для обеих платформ одновременно
* **`test_mode_example.py`** - Пример тестового режима

## Тестирование

Библиотека тестируется на разных версиях Python и aiogram:

* **Python:** 3.10, 3.13, 3.14
* **aiogram:** 3.0.0, 3.24, default (>=3.0.0)

```bash
# Установка dev-зависимостей
pip install -e ".[dev]"

# Запуск тестов
pytest

# С покрытием
pytest --cov=obabot --cov-report=html
```

## Лицензия

Proprietary License - см. файл LICENSE для деталей.

## Вклад в проект

Вклад приветствуется! Пожалуйста, не стесняйтесь создавать issues и pull requests.
