✉️ Сообщения

Отправка, редактирование, удаление, реакции, уведомления о наборе.

async send_message(chat_id, text, reply_to=None) MSG_SEND · opcode 64

Отправляет текстовое сообщение. Подтверждено в бою ✅

ПараметрТипОписание
chat_idintID чата (из списка чатов)
textstrТекст сообщения
reply_tointНеобязательно. ID сообщения для ответа (цитата)
→ dict с полями: chatId, message (полный объект), unread, mark

Структура wire-запроса

Сервер ожидает вложенный объект message, а не плоские параметры:

{
  "chatId": 34540359,
  "message": {
    "cid":         1774194113898,   # уникальный client ID = int(time.time()*1000)
    "text":        "Привет!",
    "detectShare": false,
    "isLive":      false
  }
}
⚠️
cid — клиентский ID для дедупликации. Если передавать одно и то же значение (например chatId), сервер вернёт старое сообщение не отправляя новое. Библиотека использует int(time.time() * 1000) — уникальный на каждый вызов.

Форматирование текста (elements)

Параметр text принимает как строку, так и объект FormattedText. При передаче FormattedText библиотека автоматически формирует массив elements.

from maxapi.formatting import FormattedText

fmt = (FormattedText()
    .bold("Жирный")
    .add(" и ")
    .italic("курсив")
    .add(" + ")
    .link("ссылка", "https://example.com")
)
await client.send_message(chat_id, fmt)

Поддерживаемые типы форматирования (подтверждено в бою ✅):

МетодТип элементаОписание
.bold(text)STRONGЖирный текст
.italic(text)EMPHASIZEDКурсив
.underline(text)UNDERLINEПодчёркнутый
.strike(text)STRIKETHROUGHЗачёркнутый
.code(text)MONOSPACEDМоноширинный (код)
.heading(text)HEADINGЗаголовок
.quote(text)QUOTEЦитата-блок
.link(text, url)LINKГиперссылка
.add(text)Обычный текст (без форматирования)
ℹ️
Живая проверка в личном чате Избранное подтвердила не только отдельные типы, но и смешанное форматирование по нескольким кускам текста. Сервер сохранил elements и в прямом ответе send_message(), и в get_chat_history().

Wire-формат elements

Массив elements передаётся внутри объекта message. Каждый элемент — это объект с полями:

{
  "type": "STRONG",        # тип форматирования
  "from": 15,              # смещение в символах (опускается если 0)
  "length": 6,            # длина диапазона
  "attributes": { ... }   # только для LINK (содержит url)
}
⚠️
Поле "from" не передаётся если смещение = 0. Сервер не принимает "from": 0 — форматирование не отображается. Библиотека обрабатывает это автоматически.
ℹ️
Отдельно проверен zero-offset кейс: если первый форматированный фрагмент начинается с первого символа, библиотека корректно опускает поле from, а сервер всё равно возвращает рабочую разметку.
async forward_message(to_chat_id, from_chat_id, message_id) MSG_SEND · opcode 64 (link.type=FORWARD)

Пересылает сообщение в другой (или тот же) чат. Подтверждено в бою ✅

ПараметрТипОписание
to_chat_idintID чата-получателя
from_chat_idintID чата-источника
message_idintID пересылаемого сообщения
→ dict — ответ MSG_SEND
⚠️
Комментарий при пересылке не поддерживается. Если отправить непустой text вместе с link.type="FORWARD", сервер возвращает errors.forward.text.not-empty. Поэтому библиотека всегда шлёт пустой текст.
await client.forward_message(
    to_chat_id=34540359,
    from_chat_id=63530148,
    message_id=116278582161789412
)

Wire-формат пересылки

{
  "chatId": 34540359,
  "message": {
    "cid": 1774269905000,
    "text": "",
    "link": {
      "type": "FORWARD",
      "messageId": 116278505421542491,
      "chatId": 63530148
    }
  }
}
async send_photo(chat_id, image, caption="", reply_to=None) PHOTO_UPLOAD + MSG_SEND · opcode 80/64

Загружает изображение на сервер и отправляет его в чат. Двухшаговый процесс: сначала получает одноразовый URL загрузки (opcode 80), потом делает HTTP multipart POST, и наконец отправляет MSG_SEND со вложением. Подтверждено в бою ✅

ПараметрТипОписание
chat_idintID чата
imagestr | bytesПуть к файлу или байты изображения (JPEG/PNG)
captionstrНеобязательно. Подпись под фото
reply_tointНеобязательно. ID сообщения для ответа
→ dict — полный ответ MSG_SEND с прикреплённым объектом фото

Протокол загрузки (два шага)

Шаг 1 — запрос одноразового URL (opcode 80 PHOTO_UPLOAD):

# Запрос
{ "chatId": 55474168, "count": 1 }

# Ответ сервера
{
  "url": "https://iu.oneme.ru/uploadImage?X-Amz-Algorithm=...",
  "photoIds": "8361541005"
}

Шаг 2 — HTTP multipart POST на полученный URL:

# Content-Type: multipart/form-data
# поле: file = <байты JPEG>, filename=photo.jpg, content_type=image/jpeg

# Ответ
{
  "photos": {
    "8361541005": { "token": "bso0sDwO...==" }
  }
}

Шаг 3 — MSG_SEND с attaches:

{
  "chatId": 55474168,
  "message": {
    "cid": 1774199151982,
    "text": "Привет от MAX",
    "detectShare": false,
    "isLive": false,
    "attaches": [
      { "_type": "PHOTO", "photoToken": "bso0sDwO...==" }
    ],
    "link": { "type": "REPLY", "messageId": 116273915609219087 }
  }
}
⚠️
Ключ вложения — "_type": "PHOTO" (с подчёркиванием), а токен передаётся как "photoToken". Поля "type" (без подчёркивания) или "token" сервер не принимает и возвращает Invalid attachment _type: null.

Ответ сервера (MSG_SEND)

{
  "chatId": 55474168,
  "message": {
    "id": 116273915651779748,
    "time": 1774199152401,
    "type": "USER",
    "sender": 61091213,
    "text": "Привет от MAX",
    "attaches": [
      {
        "_type": "PHOTO",
        "photoId": 8361541005,
        "photoToken": "bso0sDwO...==",
        "baseUrl": "https://i.oneme.ru/i?r=BTExG6...",
        "width": 1,
        "height": 1
      }
    ],
    "link": {
      "type": "REPLY",
      "message": { "id": 116273915609219087, "text": "Привет", /* ... */ },
      "chatId": 0
    }
  },
  "unread": 0
}

Пример — юзербот: отвечаем картинкой на любое сообщение

from maxapi import MaxClient, Message

client = MaxClient("my_session")

@client.on_message
async def on_message(msg: Message):
    if msg.is_outgoing:
        return
    await client.send_photo(
        msg.chat_id,
        "banner.jpg",
        caption="Привет!",
        reply_to=msg.id,
    )
async upload_photo(chat_id, image_data) PHOTO_UPLOAD + HTTP · opcode 80

Нижнеуровневый метод: загружает байты изображения и возвращает photoToken — строку, пригодную для вставки в attaches вручную. Используется внутри send_photo(), но может вызываться напрямую. Подтверждено в бою ✅

ПараметрТипОписание
chat_idintID чата (используется при получении URL загрузки)
image_databytesБайты JPEG или PNG изображения
→ str — base64-токен фото (photoToken)
with open("photo.jpg", "rb") as f:
    data = f.read()

token = await client.upload_photo(chat_id=55474168, image_data=data)

# Вставить токен вручную в attaches
await client.send_message(
    chat_id=55474168,
    text="Смотри:",
    attaches=[{ "_type": "PHOTO", "photoToken": token }],
)

Для ответа (reply) добавляется поле link:

{
  "chatId": 34540359,
  "message": {
    "cid":  1774194901424,  # int(time.time()*1000)
    "text": "Ответ на твоё сообщение",
    "detectShare": false,
    "isLive": false,
    "link": { "type": "REPLY", "messageId": 116273064558352190 }
  }
}

Пример

# Обычное сообщение
result = await client.send_message(chat_id=34540359, text="Тест")
    msg = result["message"]
    msg_id = msg.get("msgId") or msg.get("id")

# Ответ (цитата)
await client.send_message(chat_id=34540359, text="Ответ!", reply_to=msg_id)
ℹ️
В ответах сервера встречаются оба ключа идентификатора сообщения: id и msgId. Это касается не только истории, но и результатов send_message() / forward_message().

Ответ сервера

{
  "chatId": 34540359,
  "message": {
    "id":      116273301732920444,  # назначенный id
    "time":    1774189784743,       # Unix ms
    "type":    "USER",
    "sender":  61091213,
    "cid":     34540359,
    "text":    "Тест",
    "attaches": []
  },
  "unread": 0,
  "mark":   1774189784743
}
async edit_message(chat_id, msg_id, text) MSG_EDIT · opcode 67

Редактирует текст уже отправленного сообщения.

ПараметрТипОписание
chat_idintID чата
msg_idintID сообщения
textstrНовый текст
→ dict
await client.edit_message(
    chat_id=34540359,
    msg_id=116273301732920444,
    text="Исправленный текст"
)
async delete_messages(chat_id, msg_ids, delete_for_all=False) MSG_DELETE · opcode 66

Удаляет одно или несколько сообщений. Подтверждены оба режима: локальное удаление и удаление у всех ✅

ПараметрТипОписание
chat_idintID чата
msg_idslist[int]Список ID сообщений
delete_for_allboolЕсли True, удаляет сообщение у всех участников. По умолчанию False — удалить только у текущего аккаунта
→ dict
ℹ️
Локальный режим подтверждён ответом вида {"chatId": 63530148, "messageIds": [116279557369692993]}: сообщение исчезает из истории текущего аккаунта.
ℹ️
Режим delete_for_all=True тоже подтверждён в живой проверке: сообщение было удалено у обеих сторон.
await client.delete_messages(
    chat_id=34540359,
    msg_ids=[116273301732920444, 116273301732920445]
)
await client.delete_messages(
    chat_id=34540359,
    msg_ids=[116273301732920444],
    delete_for_all=True
)
async send_typing(chat_id) MSG_TYPING · opcode 65

Отправляет уведомление «печатает…». Запрос без ожидания ответа (wait_response=False).

ПараметрТип
chat_idint
await client.send_typing(chat_id=34540359)
async mark_read(chat_id, msg_id) CHAT_MARK · opcode 50

Помечает сообщения как прочитанные (до указанного ID включительно).

ПараметрТипОписание
chat_idintID чата
msg_idintID последнего прочитанного сообщения
→ dict
async react(chat_id, msg_id, reaction) MSG_REACTION · opcode 178

Ставит реакцию на сообщение. Fire-and-forget (ответ не ожидается).

ПараметрТипОписание
chat_idintID чата
msg_idintID сообщения
reactionstrЭмодзи реакции, например "👍"
await client.react(chat_id=63530148, msg_id=1234, reaction="🤣")
async cancel_reaction(chat_id, msg_id) MSG_CANCEL_REACTION · opcode 179

Убирает реакцию с сообщения. Fire-and-forget.

ПараметрТипОписание
chat_idintID чата
msg_idintID сообщения
await client.cancel_reaction(chat_id=63530148, msg_id=1234)
async send_sticker(chat_id, sticker_id) MSG_SEND · opcode 64

Удобная обёртка для отправки стикера. Эквивалентна send_message(chat_id, "", attaches=[…]). Подтверждено в бою ✅

ПараметрТипОписание
chat_idintID чата
sticker_idintID стикера
→ dict — ответ сервера с полем message
ℹ️
Живая проверка на sticker_id=1083866299 вернула сообщение со вложением attaches[0]._type="STICKER" и тем же stickerId.
r = await client.send_sticker(chat_id=63530148, sticker_id=1083866299)
async get_link_info(url) LINK_INFO · opcode 89

Получает превью ссылки (title, description, thumbnail).

ℹ️
На wire-уровне метод отправляет поле link, хотя Python-аргумент называется url. Если превью не найдено, сервер обычно возвращает обычный ответ с error="link.not.found", а не исключение.
→ dict
info = await client.get_link_info("https://example.com")

📡 Входящие уведомления (NOTIF)

Сервер присылает push-уведомления без запроса. Коды из OpCode:

КонстантаOpcodeСобытие
NOTIF_MESSAGE128Новое входящее сообщение
NOTIF_TYPING129Собеседник набирает текст
NOTIF_MARK130Собеседник прочитал сообщения
NOTIF_MSG_DELETE142Сообщение удалено
NOTIF_MSG_DELETE_RANGE140Удалён диапазон сообщений
NOTIF_MSG_REACTIONS_CHANGED155Реакции изменились
NOTIF_MSG_YOU_REACTED156Реакция поставлена
NOTIF_DRAFT152Черновик сохранён
NOTIF_DRAFT_DISCARD153Черновик сброшен
NOTIF_MSG_DELAYED154Отложенное сообщение
NOTIF_CALLBACK_ANSWER143Ответ на callback (боты)