Metadata-Version: 2.4
Name: onemix-bot
Version: 2.2.0
Summary: Async Python SDK for building bots on the ONEMIX messenger platform (aiogram-style)
Home-page: https://github.com/onemix/bot-sdk-python
Author: ONEMIX Team
Author-email: support@onemix.me
Keywords: onemix bot messenger api sdk async aiogram
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Communications :: Chat
Requires-Python: >=3.8
Description-Content-Type: text/markdown
Requires-Dist: httpx>=0.24.0
Provides-Extra: legacy
Requires-Dist: requests>=2.25.0; extra == "legacy"
Dynamic: author
Dynamic: author-email
Dynamic: classifier
Dynamic: description
Dynamic: description-content-type
Dynamic: home-page
Dynamic: keywords
Dynamic: provides-extra
Dynamic: requires-dist
Dynamic: requires-python
Dynamic: summary

# ONEMIX Bot SDK

Асинхронная Python-библиотека для создания ботов в мессенджере **ONEMIX**.
API намеренно повторяет [aiogram 3.x](https://docs.aiogram.dev/) — если вы
писали ботов для Telegram, здесь всё знакомо.

## Установка

```bash
pip install onemix-bot
```

## Получение токена

Откройте чат с **BotFather** в ONEMIX, отправьте `/newbot`, задайте имя и
username — получите токен. Токен также доступен по команде `/token`.

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

```python
import asyncio
from onemix_bot import Bot, Dispatcher
from onemix_bot.filters import Command, CommandStart, F
from onemix_bot.types import Message, InlineKeyboardBuilder

bot = Bot(token="ВАШ_ТОКЕН", api_url="https://api.onemix.me")
dp = Dispatcher()


@dp.message(CommandStart())
async def start(message: Message):
    kb = InlineKeyboardBuilder()
    kb.button(text="📖 Помощь", callback_data="help")
    kb.button(text="🌐 Сайт", url="https://onemix.me")
    kb.adjust(2)
    await message.answer(
        f"Привет, {message.from_user.full_name}! Я бот на ONEMIX.",
        reply_markup=kb.as_markup(),
    )


@dp.message(Command("help"))
async def help_handler(message: Message):
    await message.answer("Команды: /start, /help")


@dp.message(F.text)
async def echo(message: Message):
    await message.answer(f"Вы написали: {message.text}")


if __name__ == "__main__":
    dp.run_polling(bot)
```

## Фильтры

```python
from onemix_bot.filters import Command, CommandStart, Text, F

@dp.message(CommandStart())            # /start
@dp.message(Command("help", "info"))   # /help или /info
@dp.message(Text("привет"))            # точное совпадение текста
@dp.message(F.text)                    # любое текстовое сообщение
@dp.message(F.text.startswith("/"))    # текст, начинающийся с /
@dp.message(F.text.contains("ошибка")) # содержит подстроку
@dp.message(F.text.regexp(r"\d{4}"))   # совпадение по регулярке
```

Фильтры комбинируются через `&` и `|`:

```python
@dp.message(F.text.startswith("куп") & F.text.contains("билет"))
async def buy(message: Message):
    ...
```

## Инлайн-клавиатуры

```python
from onemix_bot.types import InlineKeyboardMarkup, InlineKeyboardButton, InlineKeyboardBuilder

# через билдер
kb = InlineKeyboardBuilder()
kb.button(text="Да", callback_data="yes")
kb.button(text="Нет", callback_data="no")
kb.adjust(2)                       # 2 кнопки в ряд
await message.answer("Подтвердить?", reply_markup=kb.as_markup())

# вручную
markup = InlineKeyboardMarkup(inline_keyboard=[
    [InlineKeyboardButton(text="Открыть сайт", url="https://onemix.me")],
])
```

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

```python
from onemix_bot.filters import Command
from onemix_bot.fsm import State, StatesGroup, FSMContext, StateFilter
from onemix_bot.types import Message


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


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


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


@dp.message(StateFilter(Form.age))
async def form_age(message: Message, state: FSMContext):
    data = await state.update_data(age=message.text)
    await state.clear()
    await message.answer(f"Готово! Имя: {data['name']}, возраст: {data['age']}")
```

## Роутеры

Раскладывайте хэндлеры по модулям, как в aiogram:

```python
# handlers/payments.py
from onemix_bot import Router
from onemix_bot.filters import Command

router = Router()

@router.message(Command("pay"))
async def pay(message):
    await message.answer("Оплата...")

# main.py
from onemix_bot import Dispatcher
from handlers import payments

dp = Dispatcher()
dp.include_router(payments.router)
```

## Команды бота (автоподсказки `/`)

```python
@dp.startup
async def on_start(bot: Bot):
    await bot.set_commands([
        {"command": "start", "description": "Запустить бота"},
        {"command": "help", "description": "Помощь"},
    ])
```

После этого команды показываются автоподсказками при вводе `/` в чате с ботом.

## API-методы

```python
me = await bot.get_me()
await bot.send_message(chat_id, "текст", reply_markup=..., parse_mode="markdown")
updates = await bot.get_updates(offset=0)          # 2.2.0: [{update_id, type, ...}]
await bot.answer_callback_query(query_id, "Готово")
await bot.edit_message_reply_markup(chat_id, message_id)   # убрать кнопки
await bot.set_commands([...])
```

## Группы и каналы (с версии 2.1.0)

Бот действует в группе или канале, куда его добавил администратор, и только в
рамках выданных прав (публикация, удаление, закрепление, бан, название).

```python
chats = await bot.get_my_chats()                      # где состоит бот и его права
msg = await bot.send_group_message(chat_id, "Привет, группа!")
await bot.pin_chat_message(chat_id, msg["message_id"])
await bot.delete_group_message(chat_id, msg["message_id"])
await bot.ban_chat_member(chat_id, user_id)           # удалить и не пускать обратно
await bot.unban_chat_member(chat_id, user_id)
await bot.set_chat_title(chat_id, "Новое название")
members = await bot.get_chat_members(chat_id)
```

## Что нового в 2.2.0

Нужен сервер ONEMIX от 27.10.2026. Со старым сервером SDK сам переходит на
прежний опрос (только сообщения личных чатов).

### Обновления с update_id

`getUpdates` теперь отдаёт очередь, как Telegram: `[{update_id, type, message |
channel_post | callback_query}]`, а `dp.run_polling` подтверждает прочитанное
через `offset`. Если бот был выключен, накопленное за сутки придёт при запуске;
не нужно — `dp.run_polling(bot, skip_updates=True)`.

> Изменение: `bot.get_updates()` возвращает обновления с `update_id`. Прежний
> список сообщений — `bot.get_legacy_messages()`.

### Нажатия кнопок

```python
from onemix_bot import CallbackQuery, Data, F

@dp.callback_query(F.data.startswith("buy:"))
async def buy(query: CallbackQuery):
    await query.answer("Добавлено в корзину")           # всплывающий текст
    await query.message.edit_text("✅ В корзине")        # заменить сообщение и убрать кнопки

@dp.callback_query(Data("help", "about"))               # точное совпадение
async def info(query: CallbackQuery):
    await query.answer("Справка", show_alert=True)       # окно с кнопкой «ОК»
```

Кнопки под сообщением не исчезают от нажатия — как в Telegram, их меняет или
убирает сам бот (`message.edit_reply_markup()`). Пока бот не ответил, кнопка
показывает загрузку (до 15 секунд) — отвечайте `query.answer()` всегда.

### Данные мини-приложения

Страница мини-приложения вызывает `window.OneMix.WebApp.sendData("…")` (до 4096
байт) — бот получает сообщение с `web_app_data`:

```python
@dp.message(F.web_app_data)            # или @dp.web_app_data()
async def order(message: Message):
    payload = message.web_app_data.data
```

`window.OneMix.WebApp.initDataRaw` (строка; `initData` — объект с темой) подписана токеном бота — сервер мини-приложения
проверяет её так:

```python
from onemix_bot import check_init_data
fields = check_init_data(BOT_TOKEN, init_data)   # None — подпись не сошлась
```

Главная кнопка внизу окна — как в Telegram:

```js
const app = window.OneMix.WebApp;
app.MainButton.setText("Заказать").show().onClick(() => app.sendData(JSON.stringify(cart)));
```

Мини-приложения открываются на телефоне, в программе для компьютера (встроенное
окно) и в веб-версии web.onemix.me. В веб-версии страница открывается во
фрейме: мост `window.OneMix.WebApp` появляется, если страница подключает
`<script src="https://web.onemix.me/onemix-webapp.js"></script>` (на телефоне и в
программе для компьютера скрипт ничего не делает — мост уже есть). Сайт не
должен запрещать встраивание (X-Frame-Options / frame-ancestors).

### Группы и каналы

Если администратор дал боту право «Читать сообщения», сообщения группы
приходят как `message` с `message.chat.type == "group"`, посты канала — в
`@dp.channel_post()`. `message.answer()` / `message.reply()` пишут в ту же
группу. Кнопки под сообщениями бота в группах и каналах работают так же, как
в личном чате.

```python
@dp.message(F.chat.type == "group", Command("weather"))
async def weather(message: Message):
    await message.reply("Сегодня солнечно")
```

Новые методы: `edit_group_message_text`, `edit_group_message_reply_markup`,
`set_chat_description`, `set_chat_photo`.

### Вебхук

Вместо опроса ONEMIX сам присылает каждое обновление POST-запросом на ваш
адрес (только `https`, порты 443/88/8443, адрес из интернета). В заголовке
`X-Onemix-Bot-Api-Secret-Token` — ваш секрет, в
`X-Onemix-Bot-Api-Signature: sha256=…` — HMAC тела этим секретом. Отвечайте
кодом 200 — иначе сервер повторит отправку (5 с, 15 с, 1 мин … до 6 ч).
Пока вебхук включён, `getUpdates` отвечает 409 (`ConflictError`).

Включить — командой `/setwebhook` в BotFather или из кода:

```python
await bot.set_webhook("https://bot.example.com/onemix-webhook", secret_token="длинный_секрет")
info = await bot.get_webhook_info()      # url, pending_update_count, last_error_message
await bot.delete_webhook()
```

Встроенный сервер без зависимостей (TLS — на nginx перед ним):

```python
dp.run_webhook(bot, host="127.0.0.1", port=8080, path="/onemix-webhook",
               secret_token="длинный_секрет",
               webhook_url="https://bot.example.com/onemix-webhook")
```

Или в своём веб-приложении (FastAPI):

```python
@app.post("/onemix-webhook")
async def hook(request: Request):
    ok = await dp.feed_webhook_update(bot, await request.body(), request.headers,
                                      secret_token="длинный_секрет")
    return Response(status_code=200 if ok else 403)
```

Пример целиком — `examples/buttons_and_webhook_bot.py`.

### Сценарии без программы

Простые ответы «команда или слово → текст и кнопки-ссылки» можно задать в
BotFather (`/scenarios`) — сервер ответит сам, даже когда программа бота
выключена. Сообщение, на которое ответил сценарий, программе не передаётся.

## Простой синхронный клиент (legacy)

Для несложных ботов доступен синхронный класс без async:

```python
from onemix_bot.legacy import OnemixBot

bot = OnemixBot(token="ВАШ_ТОКЕН", api_url="https://api.onemix.me")

@bot.command("start")
def on_start(msg):
    bot.reply(msg, "Привет!")

bot.run()
```

## Лицензия

MIT
