SMeet Bot API
Фото и документы
Боты получают и отправляют фото и документы. Каждый файл проверяется антивирусом, прежде чем бот сможет им воспользоваться, в обе стороны, и ждать проверки можно без частого опроса.
Когда доступны файлы#
Работа с файлами включается для установки SMeet целиком, вместе с антивирусной проверкой. getMe сообщает об этом программе: "can_send_files": true.
Пока она выключена, uploadFile, getFile, скачивание, sendPhoto и sendDocument отвечают 400 INVALID_REQUEST, а сообщение с файлами приходит боту с "has_unsupported_content": true и без attachments.
Получение файла#
Файлы, которые отправил человек, перечислены в attachments сообщения:
{
"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:
{
"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:
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"))Скачивание#
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"}'{"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, и при каждом вызове нужен токен:
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 | Сами байты. Обязательно и не пустое. |
kind | photo или document (по умолчанию). |
file_name | До 255 символов; если его нет, берётся имя загруженной части. Имя очищается и укорачивается до 120 символов. |
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{"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 и это событие пропускают.
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 infoProduction-примеры не ждут вовсе: python/bot_logic.py бросает smeet.RetryLater(retry_after), рабочий поток откладывает это событие и тем временем отвечает в других чатах.
Отправка файла#
sendPhoto и sendDocument отправляют готовый файл обычным вложением:
{"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 символов |