惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

GbyAI
GbyAI
博客园 - 三生石上(FineUI控件)
S
Securelist
U
Unit 42
The Cloudflare Blog
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Simon Willison's Weblog
Simon Willison's Weblog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
B
Blog
T
Tenable Blog
The Hacker News
The Hacker News
The Register - Security
The Register - Security
IT之家
IT之家
博客园 - 【当耐特】
Spread Privacy
Spread Privacy
P
Privacy & Cybersecurity Law Blog
博客园_首页
T
Tailwind CSS Blog
人人都是产品经理
人人都是产品经理
C
Cybersecurity and Infrastructure Security Agency CISA
Know Your Adversary
Know Your Adversary
NISL@THU
NISL@THU
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
阮一峰的网络日志
阮一峰的网络日志
T
Tor Project blog
C
CERT Recently Published Vulnerability Notes
Apple Machine Learning Research
Apple Machine Learning Research
Stack Overflow Blog
Stack Overflow Blog
T
Threat Research - Cisco Blogs
T
The Exploit Database - CXSecurity.com
V
Vulnerabilities – Threatpost
A
Arctic Wolf
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
V
V2EX
aimingoo的专栏
aimingoo的专栏
大猫的无限游戏
大猫的无限游戏
Scott Helme
Scott Helme
L
LINUX DO - 热门话题
Cyberwarzone
Cyberwarzone
V
Visual Studio Blog
月光博客
月光博客
爱范儿
爱范儿
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
美团技术团队
G
GRAHAM CLULEY
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
H
Heimdal Security Blog
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Telegram-бот, который молча скачивает видео по ссылкам в групповых чатах: как это сделать, не ломая приватность
Константин · 2026-05-28 · via Все публикации подряд на Хабре

Простой

6 мин

9.2K

Существует продуктовый паттерн, который я редко вижу разобранным в технических статьях на русском: бот в групповом чате, который реагирует не на команды, а на содержимое обычных сообщений участников. Юзер кидает в чат ссылку на 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 (название специфическое, родилось как шутка, т.к. не думал, что он выйдет за пределы нашего чата с друзьями — переименовывать поздно, мигрировать лень). Аналитику для написания статьи я снимал именно с него. В личке тоже работает, но интереснее посмотреть как раз в групповом режиме — добавьте в любой свой чат и киньте туда любой рилс, увидите всё описанное в действии.

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