Legan Studio
Все статьи
~ 6 мин чтения

Загрузка и обработка файлов в боте MAX: фото, документы, голос

Как принимать и обрабатывать файлы в боте MAX: фото с ресайзом, PDF и Word, голосовые с STT, антивирус, лимиты, S3-хранение и защита от утечек.

  • MAX
  • файлы
  • разработка

Файлы в боте — больше, чем «принять картинку». Реальный продакшн обрабатывает фото с ресайзом и анализом, PDF/Word с OCR, голосовые с STT, видео с превью, документы с антивирусом, всё это сохраняет в S3 с подписанными URL и удаляет по требованию пользователя. В этой статье — полная анатомия работы с файлами в боте MAX: как принимать, валидировать, ресайзить, OCR'ить, обрабатывать через ИИ, сохранять, ссылаться, удалять, защищать от вирусов и утечек.

Типы файлов

ТипЛимит размераUse cases
photoдо 10–20 МБпроверка документов, foto проблемы, фото товара
documentдо 50 МБ (зависит от платформы)договор PDF, прайс XLSX, сертификат
voiceдо 50 МБголосовые вопросы, диктовка
videoдо 50 МБпроблема в действии, инструкция
audioдо 50 МБмузыка, подкасты

Приём файла

@bot.message_handler(content_types=["photo"])
async def on_photo(msg):
    photo = msg.photo[-1]                     # самое большое разрешение
    file_info = await bot.get_file(photo.file_id)
    raw = await bot.download_file(file_info.file_path)
    # raw — это байты файла

msg.photo — массив размеров; берите последний (largest). Для документов:

@bot.message_handler(content_types=["document"])
async def on_document(msg):
    if msg.document.file_size > 50 * 1024 * 1024:
        return await bot.send_message(msg.chat.id, "Файл слишком большой (макс. 50 МБ)")
    if msg.document.mime_type not in ALLOWED_MIMES:
        return await bot.send_message(msg.chat.id, f"Поддерживается: {', '.join(ALLOWED_MIMES)}")
    file = await bot.get_file(msg.document.file_id)
    raw = await bot.download_file(file.file_path)

Валидация до скачивания — не качайте 100 МБ-bomb просто чтобы отбить.

Whitelist MIME и расширений

ALLOWED_MIMES = {
    "image/jpeg", "image/png", "image/webp",
    "application/pdf",
    "application/vnd.openxmlformats-officedocument.wordprocessingml.document",  # docx
    "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",         # xlsx
    "audio/ogg", "audio/mpeg",
}

Проверяйте mime_type от платформы и дополнительно магические байты на сервере (через python-magic):

import magic
mime_real = magic.from_buffer(raw[:8192], mime=True)
if mime_real not in ALLOWED_MIMES:
    raise ValueError(f"MIME mismatch: declared={msg.document.mime_type}, real={mime_real}")

Атакующий может прислать .exe, переименованный в .pdf — без проверки магических байт это легко пропустить.

Антивирус: ClamAV

import clamd

cd = clamd.ClamdNetworkSocket(host="clamav", port=3310)

async def scan(raw: bytes) -> tuple[bool, str | None]:
    result = cd.instream(io.BytesIO(raw))
    status, signature = result["stream"]
    if status == "OK":
        return True, None
    return False, signature

ClamAV в отдельном контейнере, обновление баз раз в сутки. Файлы с положительным detect — отбиваем, логируем, не сохраняем.

Сохранение в S3

import boto3

s3 = boto3.client(
    "s3",
    endpoint_url="https://storage.yandexcloud.net",
    aws_access_key_id=os.environ["S3_KEY"],
    aws_secret_access_key=os.environ["S3_SECRET"],
)

async def save_to_s3(raw: bytes, ext: str, user_id: int) -> str:
    key = f"uploads/{user_id}/{uuid.uuid4()}.{ext}"
    s3.put_object(
        Bucket="botmax-files",
        Key=key,
        Body=raw,
        ServerSideEncryption="AES256",
        Metadata={"user_id": str(user_id)},
    )
    return key

Бакет — приватный. Доступ только через подписанные URL с TTL:

def signed_url(key: str, ttl: int = 3600) -> str:
    return s3.generate_presigned_url(
        "get_object",
        Params={"Bucket": "botmax-files", "Key": key},
        ExpiresIn=ttl,
    )

Ресайз фото

from PIL import Image
import io

def resize(raw: bytes, max_dim: int = 1200, quality: int = 80) -> bytes:
    img = Image.open(io.BytesIO(raw))
    img.thumbnail((max_dim, max_dim))
    out = io.BytesIO()
    img.save(out, format="WEBP", quality=quality)
    return out.getvalue()

Сохраняйте оригинал и thumb (400×400) для каталога. Это снижает трафик в 5–10 раз.

OCR для PDF и фото

Yandex Vision OCR — стандарт для русского языка:

async def ocr_image(raw: bytes) -> str:
    payload = {
        "mimeType": "image/jpeg",
        "languageCodes": ["ru", "en"],
        "model": "page",
        "content": base64.b64encode(raw).decode(),
    }
    async with httpx.AsyncClient() as cli:
        r = await cli.post(
            "https://vision.api.cloud.yandex.net/vision/v1/batchAnalyze",
            headers={"Authorization": f"Bearer {YC_IAM_TOKEN}"},
            json={"folderId": YC_FOLDER, "analyze_specs": [{"content": payload["content"], "features": [{"type": "TEXT_DETECTION", "text_detection_config": {"language_codes": ["ru", "en"]}}]}]},
        )
        return parse_ocr(r.json())

Для PDF — pdfplumber для текстовых, OCR для сканированных. Альтернативы: Tesseract (open source, хуже на русском), GigaChat Vision.

STT для голосовых

@bot.message_handler(content_types=["voice"])
async def on_voice(msg):
    file = await bot.get_file(msg.voice.file_id)
    raw = await bot.download_file(file.file_path)
    text = await stt(raw, mime="audio/ogg")
    await dispatch_text(msg.chat.id, msg.from_user.id, text)

async def stt(raw: bytes, mime: str = "audio/ogg") -> str:
    # YandexSpeechKit / GigaChat Audio / Whisper
    async with httpx.AsyncClient() as cli:
        r = await cli.post(
            "https://stt.api.cloud.yandex.net/speech/v1/stt:recognize",
            params={"folderId": YC_FOLDER, "lang": "ru-RU"},
            content=raw,
            headers={"Authorization": f"Bearer {YC_IAM_TOKEN}"},
        )
        return r.json().get("result", "")

Анализ изображений через ИИ

GPT-4o vision / GigaChat-Vision / Claude 3.5 для описания, классификации, проверки документов:

async def analyze_photo(url: str, prompt: str) -> str:
    response = await llm.chat.completions.create(
        model="gpt-4o",
        messages=[{
            "role": "user",
            "content": [
                {"type": "text", "text": prompt},
                {"type": "image_url", "image_url": {"url": url}},
            ],
        }],
        max_tokens=300,
    )
    return response.choices[0].message.content

Use cases: «опиши, что не так с счётом», «прочти показания счётчика», «определи марку машины», «проверь, что на фото человек, а не картинка».

Лимиты и квоты

async def check_upload_quota(user_id: int) -> bool:
    # Не больше 100 МБ или 50 файлов в день
    today = await db.fetch_one("""
        SELECT COALESCE(sum(size_bytes), 0) AS bytes,
               COUNT(*) AS files
        FROM uploads
        WHERE user_id = $1 AND created_at::date = current_date
    """, user_id)
    return today.bytes < 100 * 1024 * 1024 and today.files < 50

Без квот один пользователь зальёт 10 ГБ за вечер.

Удаление по требованию (152-ФЗ)

@bot.message_handler(commands=["delete_my_files"])
async def on_delete_files(msg):
    files = await db.fetch_all("SELECT s3_key FROM uploads WHERE user_id = $1", msg.from_user.id)
    for f in files:
        s3.delete_object(Bucket="botmax-files", Key=f.s3_key)
    await db.execute("DELETE FROM uploads WHERE user_id = $1", msg.from_user.id)
    await bot.send_message(msg.chat.id, f"Удалено {len(files)} файлов")

Бэкапы тоже надо чистить (или политикой retention < 90 дней).

CDN для отдачи

Для публичных файлов (фото товаров) — CDN с длинным cache TTL. Для приватных — подписанные URL с TTL 1 час, новый URL на каждый запрос.

Защита от утечек

  • Подписанные URL с коротким TTL (1 час).
  • Никаких прямых ссылок на S3 в чате — только через ваш /file/{token} редирект.
  • Бакет приватный, доступ только через service account.
  • Логирование выдачи каждого файла (audit trail).

Common pitfalls

  1. Доверие mime_type — атакующий шлёт exe под видом jpg.
  2. Файлы прямо в БД (BYTEA) — раздувает Postgres, медленные бэкапы. Только S3.
  3. Без антивируса — клиент скачает «договор», получит ransomware.
  4. Без лимитов — DoS заливкой.
  5. Подписанные URL с TTL 30 дней — утечка ссылки = доступ на месяц.
  6. Нет /delete_my_files — нарушение 152-ФЗ.

Итого

Работа с файлами в боте MAX: валидация по mime_type + магическим байтам, антивирус ClamAV в отдельном контейнере, сохранение в приватный S3-бакет с server-side encryption, выдача через подписанные URL с TTL 1 час, ресайз фото для thumbnails, OCR через Yandex Vision для PDF/изображений, STT для голосовых через YandexSpeechKit/GigaChat Audio, анализ через GPT-4o/GigaChat Vision для умных сценариев. Лимиты на пользователя (100 МБ / 50 файлов в день) против DoS, /delete_my_files для соответствия 152-ФЗ. Логирование выдачи для аудита. Грамотная обработка файлов — это пара недель работы, но без неё бот рано или поздно станет каналом утечки или заражения вирусами.

Частые вопросы

Какие лимиты на размер файлов в боте MAX?

Платформа MAX/Telegram-style обычно ограничивает: фото — 10–20 МБ, документы — 50 МБ, голосовые и видео — 50 МБ. Точные значения зависят от версии Bot API, проверяйте в документации. На своей стороне ставьте более строгие квоты: дневной лимит на пользователя (100 МБ и 50 файлов), общий бакет с ротацией. Проверку размера делайте до скачивания через msg.document.file_size — нет смысла качать 50 МБ, чтобы потом отбить.

Как защититься от вирусов в загружаемых файлах?

Поднимайте ClamAV в отдельном контейнере с обновлением баз раз в сутки. Каждый загружаемый файл прогоняется через clamd.instream до сохранения; при detect — отбиваете, логируете incident, не сохраняете. Это защищает не только пользователей бота от заражения, но и вашу инфраструктуру (если файл откроют операторы для проверки). Для PDF дополнительно проверяйте на наличие активных JavaScript через pdfid. На критичных сценариях (документы юридические, медицинские) — двойная проверка через два разных движка.

Где хранить загруженные файлы?

Только в S3-совместимом Object Storage (Yandex Object Storage, Selectel, MinIO), не в Postgres BYTEA — раздувает базу, замедляет бэкапы, дорогое хранение. Бакет приватный, server-side encryption AES256, доступ только service account с минимальными правами (put-object для бота, get-object для подписанных URL). Доступ пользователю — через подписанные URL с TTL 1 час, новый URL на каждый запрос. Никогда не публикуйте S3 URL напрямую в чат — только через ваш редирект /file/{token}, который проверяет права и логирует выдачу.

Как делать OCR на русском языке?

Лучший вариант для RU 2026 года — Yandex Vision OCR. Поддерживает русский, печатный и рукописный, таблицы, многостраничные PDF. Стоит около 1.5 копейки за страницу. Альтернативы: GigaChat Vision (хорошо для смешанных задач OCR + анализ), Tesseract (open source, бесплатно, но качество хуже на русском), OpenAI GPT-4o vision (отлично для сложных layout, дороже). Для простого PDF с текстовым слоем сначала пробуйте pdfplumber — это бесплатно и точно; OCR только для сканов и фото.

Как обрабатывать голосовые сообщения?

YandexSpeechKit — основной для русского (10 копеек за минуту, latency 1–2 секунды), GigaChat Audio — альтернатива. Для зарубежных языков и максимальной точности — Whisper API от OpenAI ($0.006/мин). Принимаете voice из msg.voice.file_id, скачиваете байты через get_file + download_file, отправляете в STT, получаете текст, дальше обрабатываете как обычное сообщение. Это даёт boost UX в 2–3 раза для пользователей в дороге, на ходу или с ограниченной возможностью печатать.

Как защитить файлы от утечек?

Подписанные URL с коротким TTL (1 час, не 30 дней). Никаких прямых ссылок на S3 в чате — только через редирект /file/{token}, где token связан с пользователем и проверяет права. Логирование каждой выдачи (user_id, file, IP, ts) для аудита. Бакет приватный, IAM с минимальными правами. Регулярная ротация ключей access. Алерт «один файл скачан 100+ раз за час» — возможная утечка ссылки. Для критичных файлов (медкарты, договоры) — водяной знак с user_id поверх PDF при выдаче.

Что нужно по 152-ФЗ для файлов с ПДн?

Согласие пользователя на обработку при первой загрузке файла с ПДн (паспорт, медкарта, договор). Шифрование при хранении (S3 SSE + опционально client-side AES256 для самых чувствительных). Логирование доступа (кто и когда смотрел файл). Команда /delete_my_files для удаления по требованию пользователя (включая бэкапы — или политикой retention ≤ 90 дней). Хранение и обработка — на серверах в РФ. Уведомление в РКН о начале обработки ПДн. Запрет передачи в зарубежные сервисы без анонимизации (для OCR — используйте Yandex Vision, не Google Vision).