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 МиБ,voiceдля голосового сообщения (Голосовые сообщения), иначеdocument. «Фото», содержимое которого оказалось не картинкой, после проверки становится документом.file_nameочищено: без пути, управляющих символов и< > : " | ? *, не длиннее 120 символов. Его безопасно показывать и использовать при сохранении.mime_typeиsizeв сообщении указаны так, как их заявило приложение человека при загрузке; их ещё никто не проверял. После проверки getFile возвращает тип, определённый по содержимому, и размер сохранённой копии: опирайтесь на них. Файл не отклоняется только потому, что содержимое не совпало с заявленным типом: он хранится с определённым типом, а проверку не проходят только запрещённые типы (Как проверяются файлы).processing_statusобычно равенscanning. Файл больше 20 МиБ поsizeво вложении приходит уже в статусеrejectedс"reason": "too_large"во вложении, и проверки не будет.
Голосовые сообщения#
Голосовое сообщение приходит сообщением с одним вложением вида voice и обычно без text:
"attachments": [
{"file_id": "f_Qm9vdGZpbGVfMDAwMDAy", "kind": "voice", "file_name": "voice.ogg",
"mime_type": "audio/ogg", "size": 18422, "processing_status": "scanning",
"duration": 4, "transcription": "Во сколько вы открываетесь?"}
]durationсодержит длительность в целых секундах, как её измерило приложение человека.transcriptionесть, только если приложение человека распознало речь. Это удобство, а не гарантия: если нужна точность, скачайте запись и распознайте её сами.- Запись проходит ту же проверку, что любой файл (Ожидание проверки), и скачивается так же. После проверки
getFileназывает звуковой тип по содержимому: запись из браузера имеет типaudio/webm, с телефона обычноaudio/mp4. - Голосовое сообщение не считается
has_unsupported_content. Видеосообщение в кружке по-прежнему считается.
Ожидание проверки#
С файлом в статусе 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 символов |