В прошлом посте я вскользь упомянул blast-news — бота, который по утрам присылает мне дайджест новостей. С тех пор он пережил несколько переписываний, пару неприятных багов и одну загадку с логином, из-за которой я какое-то время всерьёз подозревал Telegram, Docker и самого себя. Пора рассказать про него отдельно.

Утренний дайджест в Telegram

Откуда он взялся

Подписок в Telegram у меня больше, чем я физически могу прочитать. Чат Radio-T, Хабр, пара каналов про AI, каналы про Кипр, где я живу, DTF, чат с друзьями. Плюс Hacker News и Lobste.rs, которые я открывал «на пять минут» и закрывал через час.

Читать всё это каждый день невозможно. Не читать тоже не получается, FOMO никто не отменял. Компромисс вышел такой: пусть кто-то прочитает за меня и расскажет, что было важного. Этим кем-то стал бот.

Как это устроено

Два контейнера на той же Raspberry Pi.

Первый — сам бот на Python. Он логинится в Telegram как обычный пользователь через Telethon (не как бот, потому что ботам не дают читать чужие каналы), забирает непрочитанное, ходит по веб-источникам и отправляет результат мне в личку уже через обычный Bot API.

Второй — маленький Flask-сервис, внутри которого стоит Claude Code. Да, тот самый CLI для программирования. Я запускаю его в headless-режиме: claude -p, промпт в stdin, ответ в stdout. Никакого API-ключа, работает та же подписка, что и у чат-бота из прошлого поста. Мне так проще и заметно дешевле.

Каждое утро в 10:01 по кипрскому времени бот:

  1. проходит по 15 каналам и забирает всё непрочитанное за последние сутки
  2. тянет топ Hacker News, Lobste.rs и рассылку TLDR Tech
  3. по каждому источнику отдельно просит Claude сделать выжимку
  4. присылает мне серию сообщений, по одному на источник
  5. в конце просит Claude написать «итог дня» по всем выжимкам сразу

Одно сообщение выглядит примерно так:

🟠 Hacker News

AI/LLM:
▸ Заголовок на русском, одно предложение о чём — ссылка

▸ Ещё один — ссылка

Железо/DIY:
▸ ...

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

Непрочитанное, а не «за 24 часа»

Первая версия брала всё за последние сутки. Проблема: если дайджест падал на середине (а он падал), половина новостей за тот день терялась навсегда. Теперь бот опирается на счётчик непрочитанного в самом Telegram: берёт всё после последнего прочитанного сообщения и помечает канал прочитанным только если выжимка реально ушла мне. Упал — значит завтра дочитает и за вчера.

Побочный эффект: если я сам открою канал и пролистаю его, в дайджест это уже не попадёт. Меня устраивает: прочитал, значит прочитал.

Когда сообщений слишком много

В чате Radio-T за сутки бывает и тысяча сообщений. В модель они влезают, но это долго, дорого, и саммари от этого не становится лучше. Первая версия решала вопрос просто: messages[:600] и text[:40000]. То есть отрезала конец. А конец — это самые свежие новости. Гениально.

Сейчас каждому сообщению считается грубый «вес»: реакции, репосты, ответы, просмотры и длина текста. Если сообщений больше лимита, выбрасываются самые лёгкие, обычно это односложный флуд и мемы. Оставшиеся возвращаются в хронологический порядок, без него саммари получается рваным.

Плюс дедупликация: репосты одного и того же текста внутри канала схлопываются, а ссылки с веб-источников запоминаются на неделю. Одна и та же история с Hacker News и Lobste.rs больше не приходит два раза.

Свой промпт на каждый источник

Самая «ручная» часть. Один общий промпт на всё давал одинаковую серую кашу, пришлось писать под каждый канал отдельно:

  • DeepState группируется по направлениям фронта, и если изменений нет, ответ должен быть пустым
  • каналу про Кипр объяснено, что читатель планирует натурализацию, поэтому всё про ПМЖ и гражданство идёт первым
  • Хабру, HN и Lobste.rs выдан «профиль читателя»: AI, Python, Raspberry Pi, Flipper, механические клавиатуры, пайка. Не для фильтрации, а чтобы эти темы ставились выше остальных

Есть и общие правила: только plain text без Markdown (Telegram ломает его в самых неожиданных местах), всё переводить на русский, к каждому пункту ссылка, а если в сообщениях кто-то советует книгу или сериал, выносить в отдельный блок «Рекомендации». Последнее я добавил после того, как в чате друзей пару раз пропустил хорошие советы.

Поиск по архиву

Побочная фича, которой я пользуюсь чаще, чем ожидал. Команда /search radio_t_chat механические клавиатуры 6m пролистывает канал за полгода, находит сообщения, где есть все слова из запроса, и отдаёт их Claude с просьбой собрать из этого внятный ответ.

Первая версия искала по «или»: любое сообщение со словом «механические» или со словом «клавиатуры». В выдачу лезло всё подряд и выталкивало полезное. Поменял на «и», стало нормально.

Ещё есть /news, чтобы получить дайджест прямо сейчас, и /digest <канал> для одного источника.

Грабли

Несколько историй, чтобы вы на них не наступали.

E2BIG. Промпт с дайджестом изначально передавался аргументом командной строки. Linux ограничивает длину одного аргумента 128 КБ, а кириллица в UTF-8 весит два байта на букву. Насыщенный день на Хабре, и вместо саммари прилетает Argument list too long. Теперь промпт идёт через stdin.

Утренний «Not logged in». Самая противная. Раз в несколько дней оба бота одновременно переставали отвечать с ошибкой «Please run /login». Захожу на малинку, логинюсь, всё работает. Через несколько дней опять.

Оказалось вот что. Чат-бот, новостной бот и мои сессии в терминале делят один каталог ~/.claude, а значит, один refresh-токен. Токен живёт восемь часов. Утренний дайджест — это залп почти из двух десятков запросов подряд, и если он попадал ровно на момент истечения токена, два процесса пытались обновить его одновременно. Кто успел первым, тот молодец. Второй получал invalid_grant, после чего CLI честно стирал креды. Для всех.

Лечится файловым замком: обновление ведёт один процесс, остальные ждут пару секунд, пока токен не станет свежим, и идут дальше. Заодно прикрутил уведомление в Telegram на случай, если Claude не отвечает из-за логина, чтобы узнавать об этом не по отсутствию дайджеста.

4096 символов. Telegram отклоняет сообщения длиннее этого, и длинные выжимки просто молча терялись с ошибкой в логе. Теперь текст режется по абзацам, а если HTML-разметка ломается на стыке кусков, кусок уходит ещё раз без разметки.

Что с этим сейчас

На днях я обновил всё до Python 3.14, пересадил бота на Opus 5.5 и написал тесты. Покрытие 100%, что для пет-проекта звучит смешно, но зато я перестал бояться его трогать.

Стоило ли? Да. Утром я читаю одно длинное сообщение вместо сорока чатов и знаю, что не пропустил ничего важного из того, что важно именно мне. А в сами чаты захожу, когда хочется, а не потому что «надо проверить».

Код, как и всё здесь, вайбкод с ручной доводкой. Если интересно, как именно устроен замок на токен или подсчёт «веса» сообщений, пишите в комментариях, распишу подробнее.