Существует продуктовый паттерн, который я редко вижу разобранным в технических статьях на русском: бот в групповом чате, который реагирует не на команды, а на содержимое обычных сообщений участников. Юзер кидает в чат ссылку на Instagram Reels — бот молча присылает видео файлом под этой ссылкой. Никаких /download, никаких упоминаний @bot, никаких inline-режимов.

Звучит просто. На практике — десяток подводных камней: Telegram Bot API в группах работает иначе, чем в личках; privacy mode ломает половину очевидных решений; flood-control прибьёт наивную реализацию на третьем активном чате; и есть отдельная проблема — как не превратить бота в спам-машину, которая реагирует на каждый https-ссылку в чате и раздражает участников.

Эту статью пишу как разработчик такого бота. Цифры из моего прода маленькие — 31 групповой чат, 380 пользователей в личке за месяц жизни — но проблемы в коде ровно те же, что были бы и при 31000 чатов. Хочу разобрать архитектурные решения, к которым пришёл, и услышать, как делали бы вы.

Проблема 1: privacy mode

Первое, обо что спотыкается каждый, кто пишет такого бота — это privacy mode, включённый у телеграм-ботов по умолчанию.

В privacy mode бот в группе получает от Bot API только следующие апдейты:

  • сообщения, начинающиеся с команды (/start, /help)

  • сообщения, в которых упомянут сам бот (@my_bot ...)

  • ответы на сообщения бота

  • service-сообщения (вступления, выходы)

Все остальные сообщения участников — бот их не видит. Если бот должен реагировать на ссылку, которую пишет произвольный участник, нужно privacy mode выключить.

Делается через @BotFatherBot SettingsGroup PrivacyDisable. После этого бот в группах получает все сообщения, и API начинает доставлять полный поток.

И тут начинается интересное.

Проблема 2: после отключения privacy mode на бота сваливается ВСЁ

Это не очевидно, пока не посмотришь на трафик. В активном чате на 50 человек, где люди обсуждают что-то живое, — это десятки сообщений в минуту. Все они теперь приходят твоему боту. Каждое нужно распарсить, проверить на наличие ссылки, отфильтровать поддерживаемые домены, и в подавляющем большинстве случаев — проигнорировать.

Наивная реализация выглядит так:

@dp.message()
async def handle_any_message(message: Message):
    if message.text and contains_supported_url(message.text):
        url = extract_url(message.text)
        video = await download_video(url)
        await message.reply_video(video)

Эта штука работает в одном тестовом чате. На пятом активном — ты упираешься в три проблемы одновременно:

1. Telegram flood-control. API ограничивает бота: примерно 1 сообщение в секунду на чат, 30 сообщений в секунду суммарно по всем чатам, и отдельный лимит на одинаковые операции. При параллельной активности в нескольких чатах ты словишь RetryAfter exceptions, и бот начнёт пропускать видео.

2. Скачивание блокирует обработку. Если download_video занимает 5–20 секунд (а для Instagram это нормальный диапазон), то синхронный обработчик превращает бота в очередь из одного человека.

3. Дубликаты. Один и тот же рилс может прилететь от трёх человек в трёх чатах подряд, и ты будешь его качать три раза вместо одного.

Решение: разделение на три уровня

Я пришёл к архитектуре, где обработка сообщения разбита на три стадии с разными требованиями к латентности:

Стадия 1 — Filter (синхронно, мгновенно). Регулярка по тексту сообщения, проверка домена. Тут нельзя делать ничего, что может занять больше миллисекунды. Если ссылки нет или домен не поддерживается — выходим, забываем.

Стадия 2 — Dedup + Queue (синхронно, быстро). Если ссылка есть и валидна — кладём задачу в очередь. Перед этим проверяем: не качали ли мы уже этот URL за последние N минут (Redis с TTL). Если качали — берём готовый file_id из кэша и сразу отправляем reply_video(file_id), не качая заново.

Стадия 3 — Download + Send (асинхронно, медленно). Воркер из очереди берёт задачу, скачивает видео, отправляет в чат, кладёт file_id в кэш для будущих дубликатов.

Псевдокод обработчика:

@dp.message()
async def handle_message(message: Message):
    # Стадия 1: фильтр
    url = extract_supported_url(message.text)
    if not url:
        return
    
    # Стадия 2: проверка кэша
    cached_file_id = await cache.get(url_hash(url))
    if cached_file_id:
        await message.reply_video(cached_file_id)
        return
    
    # Стадия 3: задача в очередь
    await queue.enqueue(DownloadTask(
        url=url,
        chat_id=message.chat.id,
        reply_to_id=message.message_id,
    ))

Воркер:

async def worker():
    while True:
        task = await queue.dequeue()
        try:
            video_path = await download_video(task.url)
            sent = await bot.send_video(
                chat_id=task.chat_id,
                video=FSInputFile(video_path),
                reply_to_message_id=task.reply_to_id,
            )
            # Кэшируем file_id, чтобы в следующий раз не качать
            await cache.set(
                url_hash(task.url), 
                sent.video.file_id, 
                ttl=timedelta(days=7),
            )
        except TelegramRetryAfter as e:
            await asyncio.sleep(e.retry_after)
            await queue.requeue(task)

Самый важный нюанс тут — file_id Telegram'а живёт долго и переиспользуется между чатами без перезагрузки бинарника. Это значит: один раз скачал рилс — отправил его 50 раз в 50 чатов за следующие сутки, потратив на это 50 API-запросов и ноль гигабайт трафика. У меня в проде доля «отправок из кэша» в часы пик доходит до значимой части всех ответов — точную цифру не считал, но субъективно бот в эти моменты работает «мгновенно», без видимой задержки на скачивание.

Проблема 3: как не быть навязчивым

Это уже не техническая, а продуктовая проблема, но она имеет техническое выражение в коде.

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

Я пришёл к нескольким эвристикам:

Тихий режим как опция чата. Админ группы может включить silent — тогда бот качает только по реплаю или по упоминанию, на голые ссылки не реагирует.

Антифлуд per-chat. Если в чате уже было N скачиваний за последнюю минуту — следующее ставится на паузу или скипается. Защищает от ситуации «кто-то скинул 20 ссылок подряд».

Только видео, не посты. Текстовые посты и галереи Instagram бот в групповом режиме игнорирует — они редко нужны всему чату, а в личке скачать никто не мешает.

В итоге получается такой компромисс: бот по умолчанию активный, потому что 90% пользователей именно этого хотят, но даёт админам ручки, чтобы тон подкрутить.

Проблема 4: эфемерность ссылок Instagram

Отдельная боль, которая стоила мне недели отладки.

Когда Instagram отдаёт URL медиа-файла после resolve'а ссылки на рилс — этот URL подписан временной меткой и живёт несколько часов. Если положить его в очередь и попытаться скачать через 30 минут (например, бот лежал, очередь копилась) — получишь 403.

Решение прямолинейное: разделять resolve и download нельзя, они должны быть атомарной операцией внутри одного воркера. И при failure не requeue'ить старую задачу с протухшей ссылкой, а делать resolve заново с нуля.

Звучит очевидно, когда написано. На практике я пару раз словил ситуацию, когда воркер «починил» зависшую очередь и попытался добить старые задачи — все упали с 403, и пользователи получили сообщения «не получилось» от запросов, которые они отправили ещё час назад.

Что в итоге

Бот живёт месяц. В групповом режиме стоит в 31 чате (большинство — небольшие, до 30 человек, самый большой — на 70). Аудитория групп ~296 уникальных пользователей. За день в часы пик — десятки скачиваний из чатов плюс гораздо больше из личек.

Цифры скромные, нагрузки на разрыв нет — но архитектурные решения выше выкристаллизовались именно из попытки масштабировать без переписываний. Когда придёт нагрузка — буду знать, что переживёт без рефакторинга, а что нет (например, in-memory очередь точно нужно будет менять на Redis Streams или RabbitMQ).

Что я хочу обсудить в комментариях, если есть желание:

1. file_id кэширование между чатами — кто-нибудь сталкивался с протуханием file_id? У меня TTL стоит 7 дней, и за это время не словил ни одного WrongFileId, но боюсь, что на больших объёмах это может выстрелить.

2. Очередь. Сейчас у меня in-process asyncio.Queue с одним воркером — этого хватает. Но интересно, на каком масштабе это перестаёт хватать, и как у вас решено в более нагруженных ботах.

3. Privacy и UX. Дилемма «бот по умолчанию активный vs тихий» — где граница между полезным и навязчивым? У меня по дефолту активный, потому что данные показывают, что юзеры этого хотят. Но это субъективно, и в комментах буду рад услышать другие подходы.

Если кто-то хочет потыкать референс — мой бот на скачивание видео живёт по адресу t.me/dicksavepleasebot (название специфическое, родилось как шутка, т.к. не думал, что он выйдет за пределы нашего чата с друзьями — переименовывать поздно, мигрировать лень). Аналитику для написания статьи я снимал именно с него. В личке тоже работает, но интереснее посмотреть как раз в групповом режиме — добавьте в любой свой чат и киньте туда любой рилс, увидите всё описанное в действии.

Так работает в групповом чате (хотя что тут демонстрировать по сути):

Комментарии (3)


  1. Dreams_and_magic
    27.05.2026 21:30

    Спасибо за статью, очень годный разбор архитектурных нюансов. Есть соображения на Ваши вопросы и пара наблюдений:

    1. Про file_id и "протухание"
    У file_id в Telegram Bot API нет официального TTL. Идентификатор привязан к токену бота и остаётся валидным, пока файл физически не удалён с серверов TG или не изменится его внутренняя сигнатура. Ваш TTL в 7 дней это корректная стратегия bounded cache для контроля потребления памяти/Redis, а не защита от автоматической инвалидации. На больших объёмах WrongFileId будет возникать именно при удалении контента или смене формата, а не "по времени". Можно оставить TTL, но добавить ленивую инвалидацию: при поимке InvalidFileId просто сбрасывать кэш по URL, перекачивать файл и сохранять новый file_id. Для продакшена кэш лучше выносить в Redis с maxmemory-policy и TTL.

    2. Очередь и масштабирование
    In-process asyncio.Queue с одним воркером отлично работает, пока нагрузка не упирается в лимиты API или I/O. "Стена" обычно наступает при стабильной глубине очереди > 50–100 задач, p95 латентности > 3 сек или частых 429 от ботов. До перехода на внешнюю очередь достаточно ввести контроль параллелизма через asyncio.Semaphore. Рекомендую два раздельных семафора: один на операции скачивания (ограничивает одновременные соединения к донорам и расход CPU/памяти), второй - на отправку в Telegram API (гарантирует, что вы не превысите глобальные лимиты ~30 req/sec и мягкие ограничения на чаты). В паре с exponential backoff + jitter это даёт стабильную работу при всплесках запросов без усложнения архитектуры. Когда метрики стабильно выходят за пороги - пайплайн стоит развязать: шлюз приёма → Redis/RabbitMQ → пул воркеров → отправка.

    3. Privacy и UX: активный по дефолту или тихий?
    Дилемма "полезный ↔ навязчивый" действительно субъективна. Но универсального решения нет: то, что удобно в ламповом чате, раздражает в крупном канале.

    Вместо бинарного выбора "активный/тихий" можно дать инструменты настройки админам чатов/каналов. Т.е. лёгкая "админ-панель» прямо в Телеграме, где админ может, например:

    • Включать/отключать авто-скачивание по источникам (только YouTube, без TikTok и т.д.).

    • Переключать режим: "авто" ↔ "только по команде" (например, /dl <ссылка>)

    • Настраивать "тихие часы" или лимиты: не больше N видео в час, не постить в ночь.

    • Добавлять чёрный список аккаунтов/хэштегов, если бот реагирует на спам.

    Реализация - через инлайн-кнопки или команды /settings, всё без отдельного веб-интерфейса.


    Кстати. А что вы используете для скачивания: yt-dlp, несколько специализированных библиотек типа instagrapi, или собственные скрипты? Для Instagram и YouTube на почти неизбежно требуется проброс cookies-файла с активной сессией (даже с ротацией аккаунтов), иначе быстро прилетают 403, капчи или блокировки Reels/Shorts.
    Я смотрел похожие проекты с yt-dlp, там решают этот вопрос по-разному: держат изолированные пулы сессий по донорам, делают автообновление библиотек, используют прокси-ротацию для снижения риска shadow-ban.

    Буду рад, если поделитесь деталями реализации. Спасибо за материал!


  1. K0Jlya9
    27.05.2026 21:30

    В телеграме недавно разрешили ботам общаться между собой, может быть теперь можно сделать группу из ботов, один(или даже не один) качает и складывает в группу-хранилище, другой только ссылки туда раздает.

    зы про высокую нагрузку не понял. 380 юзеров в 31 чате это звучит примерно как 0, стоит ли заморачиваться с какими то очередями или можно просто запускать на каждый запрос асинхронный обработчик который при получении "429 подождите 30 секунд и попробуйте снова" просто будет ждать 30 секунд и пробовать снова?


  1. UksusoFF
    27.05.2026 21:30

    А код открытый? У себя можно развернуть?