Обычно автоматический новостной канал в Telegram делают так: скрипт на feedparser, копеечный VPS, cron раз в час — и в канал падает безликая простыня текста со ссылкой. Через день от такого канала все отписываются.

Я решил сделать по‑другому и закрыть три проблемы сразу:

  • Качество контента. Не парсить всё подряд, а отбирать, переводить, сжимать длинные статьи до 3–5 предложений и подбирать картинку.

  • Частоту постов. Никакого спама каждый час — только точечные посты плюс структурированные дайджесты утром и вечером.

  • Стоимость. Хостинг, база и обработка текста — 0 рублей.

Получился скрипт на Python, который живёт в GitHub Actions, хранит состояние прямо в Git‑репозитории и работает на бесплатных лимитах LLM.

База данных — это просто Git

Главный вопрос при отказе от постоянного сервера: где хранить состояние? Бот должен помнить, какие статьи уже опубликовал и на каком RSS‑источнике остановился в прошлый раз (round‑robin по лентам).

Вместо облачной БД я использовал файловую систему самого репозитория:

  • позиции курсора по RSS‑лентам — в data/state.json;

  • хэши и URL уже опубликованных статей — в обычном текстовом файле data/seen_urls.txt (защита от дублей).

Скрипт выполняется на раннере GitHub Actions (бесплатный лимит — 2000 минут в месяц), а в конце воркфлоу идёт шаг, который коммитит изменённые файлы из data/ и пушит их обратно в main.

Для этого в настройках репозитория нужно выдать раннеру права на запись:

Settings → Actions → General → Workflow permissions → Read and write permissions

Чтобы два параллельных запуска не подрались за один и тот же файл состояния, в воркфлоу задан concurrency.group:

concurrency:
  group: post-news
  cancel-in-progress: false

Cron в GitHub Actions ненадёжен — пришлось подстраховаться снаружи

Встроенный cron в GitHub Actions — штука с сюрпризами: слот может опоздать на пару часов, а иногда запуск просто пропадает без объяснений.

Решение — workflow_dispatch плюс внешний бесплатный сервис cron‑job.org. Раз в час он шлёт POST‑запрос с токеном авторизации на API GitHub и принудительно запускает раннер вовремя. Один прогон занимает чуть больше минуты, так что лимита Actions хватает с огромным запасом.

Как устроен пайплайн

Логика одного запуска разбита на изолированные шаги
Логика одного запуска разбита на изолированные шаги

Отбор статьи

Бот собирает 5 свежих кандидатов из RSS‑лент, заданных в конфиге. Заголовки и короткие описания этих пяти статей уходят в LLM на OpenRouter — на бесплатном тарифе там нормально работают модели вроде nvidia/nemotron.

Промпт просит модель проанализировать список и вернуть строго один ID статьи — той, что потенциально интереснее IT‑аудитории. Если модель отвечает мусором или не попадает в регулярку — срабатывает фолбэк: берётся первая рабочая ссылка из списка по порядку.

Извлечение текста и сжатие

По ссылке‑победителю бот достаёт «голый» текст страницы без рекламы и сайдбаров через trafilatura. Этот текст одним запросом уходит в LLM, которая решает три задачи за раз:

  1. определяет язык источника и переводит на русский, если нужно;

  2. выдаёт заголовок и выжимку в 3–5 предложений;

  3. формирует короткий промпт на английском для генерации картинки.

Картинка — с фолбэком в четыре шага

Пост без картинки в Telegram почти не читают, а внешние сервисы генерации любят таймаутить. Поэтому в main.py — цепочка из четырёх попыток:

  1. картинка из самого RSS‑элемента;

  2. если её нет — og:image из мета‑тегов страницы (без повторного запроса к сайту, парсится заодно с текстом);

  3. если сайт закрыт для парсинга картинок — поиск по ключевым словам новости через Pexels API;

  4. если и это не сработало — сгенерированный промпт уходит в Pollinations.ai (без токенов и ключей).

Если падает вообще всё — пост уходит просто текстом. Картинка при этом качается напрямую в память и отправляется в Telegram как байты, а не как ссылка: на медленных прокси Telegram часто режет превью по ссылке, и пост выглядит криво.

Почему пост раз в час — плохая идея

Изначально бот молотил каждый час и сразу постил результат. Через сутки стало ясно: 15–20 постов в день превращают канал в спам, и люди жмут «Заглушить» почти сразу.

Логику распределения переписал на два режима:

  • Дневной режим. Бот запускается каждый час, но лимит на отправку (MAX_ARTICLES_PER_RUN) жёстко зажат — в канал за день уходит всего 3–4 отфильтрованных события.

  • Режим дайджеста. В 08:00 и 20:00 по Минску/Москве бот переключается на digest.py: собирает повестку из пула мировых медиа (BBC, CNN, Guardian), агрегирует и выдаёт один структурированный пост по 5 главным темам. Это закрывает потребность «быть в курсе», не заваливая ленту уведомлениями.

Сколько это стоит

Ничего, и без привязки зарубежной карты для трайлов:

  • хостинг скрипта — GitHub Actions, 0 ₽;

  • триггер по расписанию — cron‑job.org, 0 ₽;

  • текст и перевод — OpenRouter Free API (квоты хватает впритык на 16 сессий по ~3 запроса в сутки);

  • картинки — лимиты Pexels API и Pollinations.ai, 0 ₽.

Код открыт

Архитектура модульная: чтобы пересобрать пайплайн под другую тематику (крипта, гаджеты, региональные новости), достаточно поменять RSS‑фиды в config.py.

Репозиторий: github.com/BlackDeepSky/news_bot 

Живой канал: t.me/yourpocketnews

Буду рад фидбеку в комментариях. Главный вопрос, который меня самого интересует: насколько коммиты в Git — это адекватная замена БД для пет‑проектов, и какие подводные камни я мог упустить?