⚡ События (Push-уведомления)
Сервер MAX поддерживает постоянное TCP-соединение и сам толкает пакеты при новых событиях (входящие сообщения, статусы, уведомления о наборе и т.д.). Никакого polling'а не нужно.
1. Обычные API-методы вроде
send_message(), get_chat_info(), search() возвращают сырой dict сервера.2. События в декораторах
@client.on_message, @client.on_presence, @client.on_reaction приходят как Python-объекты (Message, PresenceEvent, ReactionEvent).Поэтому у событий нужно читать атрибуты через точку:
msg.text, msg.chat_id, event.user_id, а не через .get().
Как это работает
- Клиент подключается и логинится —
await client.connect() - Регистрируются обработчики через декораторы
@client.on_message/@client.on(opcode) - Вызывается
await client.run_until_disconnected()— блокирует и слушает - При входящем пакете библиотека строит типизированный объект и вызывает обработчик в отдельной задаче
asyncio.create_task().
Это позволяет делать await внутри хендлера (например await msg.reply())
не блокируя чтение следующих пакетов.
Базовый шаблон юзербота
my_session.json должен уже существовать.
Если его нет, сначала пройди авторизацию через auth или быстрый старт.
MaxClient(...) передаётся имя сессии без расширения: правильно MaxClient("my_session"), неправильно MaxClient("my_session.json"). Клиент сам дописывает .json.import asyncio from maxapi import MaxClient from maxapi.types import Message client = MaxClient("my_session") # загружает my_session.json @client.on_message async def handler(msg: Message): if not msg.is_outgoing: await msg.reply("Привет от MAX API!") asyncio.run(client.run()) # connect() + run_until_disconnected()
@client.on_message
NOTIF_MESSAGE · opcode 128
Декоратор для обработки входящих и исходящих сообщений.
Передаёт объект Message с удобными полями.
dict, а экземпляр класса Message. Правильно: msg.text, msg.chat_id, msg.sender_id. Неправильно: msg.get("text").@client.on_message async def on_msg(msg: Message): if msg.is_outgoing: return # игнорируем собственные print(f"Новое от {msg.sender_id}: {msg.text}") await msg.reply("Получено!")
@client.on(opcode)
любой NOTIF_* opcode
Универсальный декоратор — регистрирует обработчик для любого opcode.
Для NOTIF_TYPING передаёт объект TypingEvent, для NOTIF_PRESENCE — PresenceEvent, для NOTIF_MSG_REACTIONS_CHANGED — ReactionEvent, для остальных — сырой dict.
@client.on(opcode) тип аргумента зависит от opcode. Для typed-событий используйте атрибуты объекта, для остальных уведомлений — ключи словаря.from maxapi import OpCode from maxapi import TypingEvent, PresenceEvent, ReactionEvent @client.on(OpCode.NOTIF_TYPING) async def on_typing(event: TypingEvent): print(f"Печатает: {event.sender_id} в чате {event.chat_id}") @client.on_presence async def on_presence(event: PresenceEvent): print(f"{event.user_id} {'онлайн' if event.is_online else 'оффлайн'}") @client.on_reaction async def on_reaction(event: ReactionEvent): print(f"Реакция {event.top_reaction} на сообщение {event.message_id}")
Можно вешать несколько хендлеров на один opcode
@client.on_message async def logger(msg): print(msg) @client.on_message async def responder(msg): if not msg.is_outgoing: await msg.reply("Авто-ответ")
run_until_disconnected()
блокирует до разрыва
Блокирует выполнение до тех пор, пока не оборвётся соединение или не будет вызван Ctrl+C.
await client.connect() await client.run_until_disconnected()
Класс Message
Объект передаваемый в @on_message хендлер.
msg.raw. Это полезно, когда надо обратиться к нестандартному полю, которое ещё не вынесено в отдельный атрибут класса.Message из событий с объектами, которые возвращают обычные API-методы. У события поля нормализованы в snake_case: chat_id, sender_id, is_outgoing. У сырых серверных dict часто остаются wire-ключи вроде chatId или accountStatus.| Атрибут | Тип | Описание |
|---|---|---|
id | int | Уникальный ID сообщения (назначает сервер) |
chat_id | int | ID чата |
sender_id | int | UID отправителя |
text | str | Текст сообщения |
time_ms | int | Время отправки (Unix, миллисекунды) |
elements | list[dict] | Форматирование текста (STRONG, EMPHASIZED, LINK и др.) |
attaches | list[dict] | Вложения (PHOTO, STICKER, FILE, VIDEO и др.) |
is_sticker | bool | True — если вложение является стикером |
sticker | dict | None | Объект стикера (stickerType, url, lottieUrl, stickerId, setId, tags) или None |
link | dict | None | Объект ссылки: REPLY (цитата) или FORWARD (пересылка) |
is_outgoing | bool | True — если сообщение отправили мы |
is_reply | bool | True — если это ответ (цитата) на другое сообщение |
is_forward | bool | True — если это пересланное сообщение |
reply_to_message | dict | None | Объект исходного сообщения при REPLY |
forwarded_message | dict | None | Объект пересланного сообщения при FORWARD |
raw | dict | Полный dict из сервера (params пакета) |
Методы
| Метод | Описание |
|---|---|
await msg.reply(text) |
Ответить в тот же чат с цитированием. Принимает str или FormattedText. |
await msg.forward(to_chat_id) |
Переслать это сообщение в другой чат. Комментарий при пересылке сервер не поддерживает. |
Класс TypingEvent
Объект передаваемый в @client.on(OpCode.NOTIF_TYPING) хендлер.
| Атрибут | Тип | Описание |
|---|---|---|
chat_id | int | ID чата, в котором печатают |
sender_id | int | UID пользователя, который печатает |
typing_type | str | None | Тип набора: None (текст), "STICKER" и т.д. |
raw | dict | Полный dict из сервера |
Класс PresenceEvent
Объект передаваемый в @client.on_presence хендлер (NOTIF_PRESENCE, opcode 132).
| Атрибут | Тип | Описание |
|---|---|---|
user_id | int | UID пользователя |
seen | int | Unix-timestamp последнего появления (секунды) |
status | int | None | 1 = онлайн, None = оффлайн |
is_online | bool | True — если пользователь сейчас онлайн |
raw | dict | Полный dict из сервера |
@client.on_presence async def on_presence(event: PresenceEvent): if event.is_online: print(f"Пользователь {event.user_id} вошёл онлайн")
Класс ReactionEvent
Объект передаваемый в @client.on_reaction хендлер (NOTIF_MSG_REACTIONS_CHANGED, opcode 155).
| Атрибут | Тип | Описание |
|---|---|---|
chat_id | int | ID чата |
message_id | int | ID сообщения с реакцией |
counters | list[dict] | Список реакций: [{"reaction": "🤣", "count": 1}, ...] |
total_count | int | Общее количество реакций |
top_reaction | str | None | Реакция с наибольшим количеством |
raw | dict | Полный dict из сервера |
@client.on_reaction async def on_reaction(event: ReactionEvent): print(f"Реакция {event.top_reaction} на сообщение {event.message_id}")
Push-опкоды сервера
Все входящие уведомления имеют opcode ≥ 128 и никогда не являются ответами на наши запросы.
| OpCode | Константа | Описание | Объект |
|---|---|---|---|
128 | NOTIF_MESSAGE | Новое / обновлённое сообщение ✅ | Message |
129 | NOTIF_TYPING | Пользователь печатает ✅ | TypingEvent |
130 | NOTIF_MARK | Сообщения прочитаны | dict |
131 | NOTIF_CONTACT | Изменение контакта | dict |
132 | NOTIF_PRESENCE | Статус онлайн/оффлайн ✅ | PresenceEvent |
134 | NOTIF_CONFIG | Обновление конфигурации | dict |
135 | NOTIF_CHAT | Изменение чата | dict |
136 | NOTIF_ATTACH | Новое вложение | dict |
137 | NOTIF_CALL_START | Входящий звонок | dict |
142 | NOTIF_MSG_DELETE | Удаление сообщения | dict |
152 | NOTIF_DRAFT | Черновик сохранён | dict |
155 | NOTIF_MSG_REACTIONS_CHANGED | Реакции изменились ✅ | ReactionEvent |
292 | NOTIF_BANNERS | Баннеры/рекламные уведомления | dict |
Wire-формат NOTIF_MESSAGE (opcode 128)
Пример реального пакета, пойманного во время теста:
{
"chatId": 63530148,
"message": {
"id": 116273637059727804,
"time": 1774194113898,
"type": "USER",
"sender": 7162153,
"cid": 567919839690875,
"text": "Привет!",
"attaches": []
},
"prevMessageId": 116273584104764698,
"ttl": false,
"unread": 0,
"mark": 1774194113898
}