SMeetBot API

SMeet Bot API

Фото и документы

Боты получают и отправляют фото и документы. Каждый файл проверяется антивирусом, прежде чем бот сможет им воспользоваться, в обе стороны, и ждать проверки можно без частого опроса.

Когда доступны файлы#

Работа с файлами включается для установки SMeet целиком, вместе с антивирусной проверкой. getMe сообщает об этом программе: "can_send_files": true.

Пока она выключена, uploadFile, getFile, скачивание, sendPhoto и sendDocument отвечают 400 INVALID_REQUEST, а сообщение с файлами приходит боту с "has_unsupported_content": true и без attachments.

Получение файла#

Файлы, которые отправил человек, перечислены в attachments сообщения:

JSON
{
  "update_id": "57",
  "event_id": "9a7c1e2b-3d4f-4a5b-8c6d-7e8f9a0b1c2d",
  "type": "message",
  "date": "2026-09-26T10:00:00Z",
  "message": {
    "message_id": "7301",
    "chat": {"id": "550", "type": "private"},
    "from": {"id": "812", "display_name": "Anna", "is_bot": false},
    "date": "2026-09-26T10:00:00Z",
    "text": "Here is the invoice",
    "attachments": [
      {"file_id": "f_Qm9vdGZpbGVfMDAwMDAx", "kind": "document", "file_name": "invoice.pdf",
       "mime_type": "application/pdf", "size": 48213, "processing_status": "scanning"}
    ]
  }
}
  • file_id служит непрозрачным идентификатором, который работает только для вашего бота.
  • kind равен photo для картинки JPEG, PNG, WebP или HEIC до 10 МиБ, иначе document. «Фото», содержимое которого оказалось не картинкой, после проверки становится документом.
  • file_name очищено: без пути, управляющих символов и < > : " | ? *, не длиннее 120 символов. Его безопасно показывать и использовать при сохранении.
  • mime_type и size в сообщении указаны так, как их заявило приложение человека при загрузке; их ещё никто не проверял. После проверки getFile возвращает тип, определённый по содержимому, и размер сохранённой копии: опирайтесь на них. Файл не отклоняется только потому, что содержимое не совпало с заявленным типом: он хранится с определённым типом, а проверку не проходят только запрещённые типы (Как проверяются файлы).
  • processing_status обычно равен scanning. Файл больше 20 МиБ по size во вложении приходит уже в статусе rejected с "reason": "too_large" во вложении, и проверки не будет.

Ожидание проверки#

С файлом в статусе scanning ничего не делайте. Когда проверка заканчивается, SMeet присылает событие file_status_changed с собственным event_id:

JSON
{
  "update_id": "58",
  "event_id": "2f0c6b8e-1a2b-4c3d-9e4f-5a6b7c8d9e0f",
  "type": "file_status_changed",
  "date": "2026-09-26T10:00:05Z",
  "file_status": {"file_id": "f_Qm9vdGZpbGVfMDAwMDAx", "status": "ready", "message_id": "7301",
                  "chat": {"id": "550", "type": "private"}}
}
  • ready: вызовите getFile один раз; в ответе будет download_path.
  • rejected или scan_failed: reason объясняет причину (Коды причин). Сообщите человеку об этом короткой фразой.
  • Файл, который сканер не смог проверить (scan_failed с scanner_unavailable, scanner_outdated или scan_timeout), проверяется снова примерно через час, всего до трёх раундов. В это время getFile снова показывает scanning, а потом приходит ещё один file_status_changed.
  • file_status_changed не отправляется в чат, где человек остановил или заблокировал бота либо из которого вышел, и для удалённого сообщения.
  • Для файла, который пришёл с уже законченной проверкой, например rejected из-за размера больше 20 МиБ, file_status_changed не будет: действуйте по самому сообщению. Причина есть во вложении, в поле reason.

Так на оба события реагирует python/file_bot.py:

Pythondocs/bots/examples/python/file_bot.py
def handle(client: smeet.Client, update: dict) -> None:
    if update["type"] == "file_status_changed":
        change = update["file_status"]
        if "message_id" not in change or "chat" not in change:
            return  # the check of one of our own uploads; wait_for_file follows those with getFile
        chat_id, message_id = change["chat"]["id"], change["message_id"]
        if change["status"] == "ready":
            send_receipt(client, chat_id, message_id, change["file_id"])
        elif change["status"] in REFUSED:
            explain_refusal(client, chat_id, message_id, change["file_id"], change.get("reason"))
        return  # any other status, including ones added later, needs nothing

    if update["type"] != "message":
        return  # button presses, edits, access changes and unknown types need nothing here
    message = update["message"]
    chat_id, message_id = message["chat"]["id"], message["message_id"]
    attachments = message.get("attachments") or []
    if not attachments:
        client.send_message(chat_id, HINT, idempotency_key=smeet.action_key(update["event_id"], "hint"))
        return
    for attachment in attachments:
        file_id, status = attachment["file_id"], attachment["processing_status"]
        if status == "scanning":
            log.info("File %s is being checked; file_status_changed will say when it is done", file_id)
        elif status == "ready":
            send_receipt(client, chat_id, message_id, file_id)
        elif status in REFUSED:
            # Refused before any check (too large, for example): no file_status_changed follows, and
            # the attachment carries the reason itself.
            explain_refusal(client, chat_id, message_id, file_id, attachment.get("reason"))

Скачивание#

Shell
curl -s https://messenger.scrile.com/bot-api/v1/getFile \
  -H "Authorization: Bearer $SMEET_BOT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"file_id": "f_Qm9vdGZpbGVfMDAwMDAx"}'
JSON
{"ok": true, "result": {"file_id": "f_Qm9vdGZpbGVfMDAwMDAx", "kind": "document", "file_name": "invoice.pdf",
 "mime_type": "application/pdf", "size": 48213, "status": "ready", "download_path": "/files/f_Qm9vdGZpbGVfMDAwMDAx/content"}}

download_path указан относительно базового адреса API, и при каждом вызове нужен токен:

Shell
curl -s -o invoice.pdf \
  https://messenger.scrile.com/bot-api/v1/files/f_Qm9vdGZpbGVfMDAwMDAx/content \
  -H "Authorization: Bearer $SMEET_BOT_TOKEN"
  • Ответом служит сам файл, всегда как вложение (Content-Disposition: attachment с именем файла), с X-Content-Type-Options: nosniff и Cache-Control: private, no-store. Фото сохраняет тип картинки; документ сохраняет свой тип, только если это изображение, аудио, видео, PDF или обычный текст, иначе приходит как application/octet-stream.
  • Публичной или постоянной ссылки нет: права проверяются при каждом скачивании.
  • 409 FILE_NOT_READY (с retry_after), пока файл проверяется, 422 FILE_REJECTED (с reason) для отклонённого файла, 404 FILE_NOT_FOUND, когда доступ закончился (Когда заканчивается доступ).
  • Скачивание ограничено 5 запросами в секунду на бота (всплеск 10).

Загрузка файла#

uploadFile принимает multipart/form-data и Idempotency-Key:

ПолеСодержимое
fileСами байты. Обязательно и не пустое.
kindphoto или document (по умолчанию).
file_nameДо 255 символов; если его нет, берётся имя загруженной части. Имя очищается и укорачивается до 120 символов.
Shell
curl -s https://messenger.scrile.com/bot-api/v1/uploadFile \
  -H "Authorization: Bearer $SMEET_BOT_TOKEN" \
  -H "Idempotency-Key: receipt-A-1001" \
  -F kind=document -F file_name=receipt.pdf -F file=@receipt.pdf
JSON
{"ok": true, "result": {"file_id": "f_Rm9yVGhlUmVjZWlwdDAx", "kind": "document", "file_name": "receipt.pdf",
 "mime_type": "application/pdf", "size": 20480, "status": "scanning", "retry_after": 5}}
  • Фото до 10 МиБ и по содержимому должно быть картинкой JPEG, PNG, WebP или HEIC; иначе 422 FILE_REJECTED с not_a_photo. Документ до 20 МиБ. Более крупный файл получает 413 PAYLOAD_TOO_LARGE с limit в байтах. Пустое поле file получает 400 INVALID_REQUEST с "field": "file".
  • Запрещённые типы (см. Как проверяются файлы) сразу получают 422 FILE_REJECTED с type_not_allowed.
  • Каждый бот может загрузить 200 МиБ за сутки UTC. Учитывается каждая загрузка, в том числе файлы, которые проверка потом отклонит. Сверх квоты приходит 429 QUOTA_EXCEEDED с retry_after до полуночи UTC.
  • Загрузка ограничена 2 запросами в секунду на бота (всплеск 5).
  • Загруженный файл хранится 7 дней; отправьте его в течение этого срока.
  • Тот же Idempotency-Key с теми же байтами, видом и именем возвращает тот же файл в его текущем состоянии; с любым отличием приходит 409 IDEMPOTENCY_CONFLICT.

Ожидание проверки своей загрузки#

Загрузка начинается в статусе scanning с retry_after. Спросите getFile снова ровно через столько секунд, а не в плотном цикле, пока статус не станет ready, rejected или scan_failed. Когда проверка загрузки заканчивается, SMeet тоже присылает file_status_changed, но без chat и message_id; примеры опираются на getFile и это событие пропускают.

Pythondocs/bots/examples/python/smeet.py
def wait_for_file(client: Client, file: dict, max_wait: float = 600.0) -> dict:
    """Wait until the check of a file THIS BOT UPLOADED is over; return it as getFile describes it.

    `file` is what uploadFile returned. SMeet scans every file before a bot may use it. While the
    status is "scanning" the answer carries retry_after, and we sleep exactly that long before
    asking getFile again, instead of asking in a tight loop. The result has status ready,
    rejected or scan_failed. For a file a user sent you do not need this: SMeet sends
    file_status_changed when the check of a received attachment is over (see file_bot.py).
    """
    deadline = time.monotonic() + max_wait
    info = file
    while info["status"] == "scanning":
        delay = max(1, min(int(info.get("retry_after") or 5), 60))
        if time.monotonic() + delay > deadline:
            raise TimeoutError(f"file {file['file_id']} is still {info['status']} after {max_wait:.0f} s")
        log.info("File %s is %s, asking again in %d s", file["file_id"], info["status"], delay)
        client.sleep(delay)
        info = client.get_file(file["file_id"])
    return info

Production-примеры не ждут вовсе: python/bot_logic.py бросает smeet.RetryLater(retry_after), рабочий поток откладывает это событие и тем временем отвечает в других чатах.

Отправка файла#

sendPhoto и sendDocument отправляют готовый файл обычным вложением:

JSON
{"chat_id": "550", "file_id": "f_Rm9yVGhlUmVjZWlwdDAx", "caption": "Ваша квитанция", "reply_to_message_id": "7301"}
  • file_id указывает на файл, который загрузил ваш бот, или на вложение сообщения, которое ваш бот получил в том же пространстве, пока доступ к нему сохраняется. Файл другого пространства получает 404 FILE_NOT_FOUND.
  • caption до 1024 символов; reply_to_message_id и reply_markup работают как в sendMessage.
  • Файл должен быть в статусе ready. Пока он проверяется, вызов получает 409 FILE_NOT_READY с retry_after: повторите тот же запрос с тем же ключом позже, ключ не расходуется. Отклонённый файл получает 422 FILE_REJECTED с reason.
  • sendPhoto нужен файл вида photo; любой другой файл получает 400 INVALID_REQUEST с "field": "file_id", отправьте его через sendDocument.
  • Документ всегда приходит человеку как файл для скачивания и никогда не открывается прямо в приложении.

Как проверяются файлы#

  • Каждый файл проверяется до того, как бот сможет им воспользоваться: файл от человека до того, как бот сможет его скачать, файл бота до того, как его можно отправить.
  • Байты хранятся в закрытом хранилище без публичной ссылки. Файл от человека копируется туда до проверки, и проверяется и отдаётся боту только эта копия, поэтому последующие изменения в другом месте до неё не доходят.
  • Тип определяется по содержимому, а не по имени и не по заявленному типу, и после проверки заменяет заявленный. Разметка, которую может выполнить просмотрщик (HTML, SVG, XML), скрипты, начинающиеся с #!, и программы (исполняемые файлы Windows, Linux и macOS) отклоняются, как и имена с опасными расширениями, например .html, .svg, .js, .exe, .sh, .py, .jar или .apk.
  • Антивирус (ClamAV) проверяет содержимое по сигнатурам не старше 3 дней. Чистый результат для точно такого же содержимого используется повторно, пока сигнатуры свежие.
  • Если сканер недоступен, его базы устарели или проверка не укладывается в 60 секунд, она повторяется до 3 раз с паузами 30 секунд и 2 минуты, после чего файл получает scan_failed. Примерно через час его проверяют снова, всего до трёх раундов. Без чистой проверки ни один файл не становится ready.
  • Во время технической паузы платформы проверки ждут и продолжаются после неё.

Коды причин#

reason у файла в статусе rejected или scan_failed, а также error.reason у FILE_REJECTED:

КодЗначение
malware_detectedАнтивирус нашёл вредоносное содержимое.
type_not_allowedРазметка, программа, другой запрещённый тип или опасное расширение.
not_a_photoТолько для загрузок: kind=photo, но содержимое не картинка JPEG, PNG, WebP или HEIC.
too_largeБольше допустимого: 20 МиБ для файла от человека; 10 МиБ для фото и 20 МиБ для документа, которые загружает бот. Такой файл получает rejected. Файл, который сам сканер отказался проверять из-за размера, получает scan_failed с этой причиной и повторно не проверяется.
scanner_unavailableСканер недоступен или завершился с ошибкой. Файл будет проверен позже.
scanner_outdatedСигнатуры сканера старше 3 дней. Файл будет проверен позже.
scan_timeoutПроверка не уложилась во время. Файл будет проверен позже.
source_missingФайл исчез до того, как его успели проверить.
emptyВ файле нет содержимого, например человек отправил пустой файл.

Новые коды могут появиться в любой момент. Известные превращайте в понятную фразу для человека, а для остальных используйте общую, как делает smeet.file_reason_text в примерах.

Когда заканчивается доступ#

ФайлБот может им пользоваться
Отправлен человекомПока существует сообщение, чат запущен и человек остаётся в пространстве; не дольше 30 дней.
Загружен ботом7 дней.
  • Удаление сообщения, остановка, блокировка и выход из пространства сразу прекращают доступ: getFile и скачивание отвечают 404 FILE_NOT_FOUND, и file_status_changed для таких файлов не отправляется.
  • Копию, которую ваш сервер уже скачал, SMeet отозвать не может. Удалите её по просьбе человека и напишите в описании бота, что бот хранит.

Повторы и ключи#

  • Тот же Idempotency-Key никогда не создаёт второй файл или второе сообщение.
  • FILE_NOT_READY и другие ошибки до выполнения действия ключ не расходуют: повторите тот же запрос позже.
  • К одному и тому же действию могут привести два разных события: сообщение, файл которого уже проверен, и пришедший позже file_status_changed этого файла. Стройте ключи такого действия из file_id, а не из event_id, как делает python/file_bot.py с smeet.action_key(file_id, "receipt"). Тогда повтор любого из двух событий ничего нового не отправит.

Ограничения#

ЧтоОграничение
Фото, которое загружает ботДо 10 МиБ; JPEG, PNG, WebP или HEIC по содержимому
Документ, который загружает ботДо 20 МиБ
Файл от человека, который бот может получитьДо 20 МиБ; более крупные приходят в статусе rejected с too_large
Объём загрузок200 МиБ на бота за сутки UTC (429 QUOTA_EXCEEDED)
Тело запроса21 МиБ (413 PAYLOAD_TOO_LARGE с limit)
Хранение загрузки7 дней
Доступ к полученному файлуПока это позволяют сообщение и чат, не дольше 30 дней
Загрузка и скачивание2 загрузки в секунду на бота (всплеск 5), 5 скачиваний в секунду на бота (всплеск 10)
Имя файлаОчищается и укорачивается до 120 символов