⚡ События (Push-уведомления)

Сервер MAX поддерживает постоянное TCP-соединение и сам толкает пакеты при новых событиях (входящие сообщения, статусы, уведомления о наборе и т.д.). Никакого polling'а не нужно.

✅ Подтверждено в бою — события приходят мгновенно, push-архитектура работает. Opcode push-пакетов всегда ≥ 128.
⚠️
У библиотеки два разных слоя данных:
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().

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

  1. Клиент подключается и логинится — await client.connect()
  2. Регистрируются обработчики через декораторы @client.on_message / @client.on(opcode)
  3. Вызывается await client.run_until_disconnected() — блокирует и слушает
  4. При входящем пакете библиотека строит типизированный объект и вызывает обработчик в отдельной задаче
⚠️
Каждый обработчик запускается через 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()
decorator @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("Получено!")
decorator @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("Авто-ответ")
async run_until_disconnected() блокирует до разрыва

Блокирует выполнение до тех пор, пока не оборвётся соединение или не будет вызван Ctrl+C.

await client.connect()
await client.run_until_disconnected()

Класс Message

Объект передаваемый в @on_message хендлер.

ℹ️
Если нужен исходный серверный payload, он лежит в msg.raw. Это полезно, когда надо обратиться к нестандартному полю, которое ещё не вынесено в отдельный атрибут класса.
ℹ️
Не путайте Message из событий с объектами, которые возвращают обычные API-методы. У события поля нормализованы в snake_case: chat_id, sender_id, is_outgoing. У сырых серверных dict часто остаются wire-ключи вроде chatId или accountStatus.
АтрибутТипОписание
idintУникальный ID сообщения (назначает сервер)
chat_idintID чата
sender_idintUID отправителя
textstrТекст сообщения
time_msintВремя отправки (Unix, миллисекунды)
elementslist[dict]Форматирование текста (STRONG, EMPHASIZED, LINK и др.)
attacheslist[dict]Вложения (PHOTO, STICKER, FILE, VIDEO и др.)
is_stickerboolTrue — если вложение является стикером
stickerdict | NoneОбъект стикера (stickerType, url, lottieUrl, stickerId, setId, tags) или None
linkdict | NoneОбъект ссылки: REPLY (цитата) или FORWARD (пересылка)
is_outgoingboolTrue — если сообщение отправили мы
is_replyboolTrue — если это ответ (цитата) на другое сообщение
is_forwardboolTrue — если это пересланное сообщение
reply_to_messagedict | NoneОбъект исходного сообщения при REPLY
forwarded_messagedict | NoneОбъект пересланного сообщения при FORWARD
rawdictПолный dict из сервера (params пакета)

Методы

МетодОписание
await msg.reply(text) Ответить в тот же чат с цитированием. Принимает str или FormattedText.
await msg.forward(to_chat_id) Переслать это сообщение в другой чат. Комментарий при пересылке сервер не поддерживает.

Класс TypingEvent

Объект передаваемый в @client.on(OpCode.NOTIF_TYPING) хендлер.

АтрибутТипОписание
chat_idintID чата, в котором печатают
sender_idintUID пользователя, который печатает
typing_typestr | NoneТип набора: None (текст), "STICKER" и т.д.
rawdictПолный dict из сервера

Класс PresenceEvent

Объект передаваемый в @client.on_presence хендлер (NOTIF_PRESENCE, opcode 132).

АтрибутТипОписание
user_idintUID пользователя
seenintUnix-timestamp последнего появления (секунды)
statusint | None1 = онлайн, None = оффлайн
is_onlineboolTrue — если пользователь сейчас онлайн
rawdictПолный 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_idintID чата
message_idintID сообщения с реакцией
counterslist[dict]Список реакций: [{"reaction": "🤣", "count": 1}, ...]
total_countintОбщее количество реакций
top_reactionstr | NoneРеакция с наибольшим количеством
rawdictПолный dict из сервера
@client.on_reaction
async def on_reaction(event: ReactionEvent):
    print(f"Реакция {event.top_reaction} на сообщение {event.message_id}")

Push-опкоды сервера

Все входящие уведомления имеют opcode ≥ 128 и никогда не являются ответами на наши запросы.

OpCodeКонстантаОписаниеОбъект
128NOTIF_MESSAGEНовое / обновлённое сообщение ✅Message
129NOTIF_TYPINGПользователь печатает ✅TypingEvent
130NOTIF_MARKСообщения прочитаныdict
131NOTIF_CONTACTИзменение контактаdict
132NOTIF_PRESENCEСтатус онлайн/оффлайн ✅PresenceEvent
134NOTIF_CONFIGОбновление конфигурацииdict
135NOTIF_CHATИзменение чатаdict
136NOTIF_ATTACHНовое вложениеdict
137NOTIF_CALL_STARTВходящий звонокdict
142NOTIF_MSG_DELETEУдаление сообщенияdict
152NOTIF_DRAFTЧерновик сохранёнdict
155NOTIF_MSG_REACTIONS_CHANGEDРеакции изменились ✅ReactionEvent
292NOTIF_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
}