Skip to content
troyan-dyPublic

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

TG RSS Aggregator Bot

Бот ведёт несколько Telegram-каналов на автопилоте. Для каждого канала — своя RSS-лента, свой тон постов и своё расписание публикаций. Для каждого канала по его расписанию бот:

  1. Читает заданную для канала RSS-ленту.
  2. Отбрасывает уже обработанные записи (дедуп по id в PostgreSQL, отдельно на каждый канал — одна новость может выйти в разные каналы).
  3. Отправляет заголовки и краткие описания новых записей в DeepSeek, который выбирает самую важную/актуальную новость (подробнее — в разделе ниже).
  4. DeepSeek генерирует из неё пост для Telegram в тоне этого канала.
  5. Бот публикует пост в канал.

Каналы и их настройки управляются через личный чат с ботом (только админ). Канал добавляется присланной ссылкой — бот резолвит чат и проверяет, что он в нём администратор с правом публикации. Команды 📡 Лента, 🎨 Тон, 🕒 Часы, 🚀 Запустить действуют на выбранный (активный) канал.

Алгоритм выбора новости

Чтобы лента была разнообразной и не зацикливалась на одной теме, выбор делается с учётом того, что уже выходило:

  1. Из ленты берутся свежие записи, ещё не показанные боту (дедуп по id).
  2. Если свежих нет — fallback: берутся записи из ленты, которые уже были показаны боту, но ещё не опубликованы в канал (storage.published_among исключает только опубликованное). Так ручной запуск почти всегда находит, что опубликовать; порядок при этом может не совпадать с хронологией — это допустимо. Статус «нет новостей» возникает, только когда в ленте всё уже опубликовано.
  3. Из базы поднимаются заголовки, опубликованные за последние 24 часа (storage.recent_published_titles — записи с published=true и posted_at в пределах суток, новые первыми).
  4. DeepSeek (deepseek.pick_most_relevant) получает оба списка — уже опубликованное и кандидатов — и инструкцию: выбрать одну важную и актуальную новость, которая при этом разнообразит ленту по теме и сюжету и не повторяет недавние публикации. Если несколько кандидатов про одно и то же — предпочесть новость из другой тематики.
  5. Ответ — индекс выбранной новости (строгий JSON). При сбое парсинга берётся первый кандидат, чтобы прогон всё равно опубликовал что-то осмысленное.

Окно «последних суток» завязано на время публикации (posted_at), а не на расписание запусков, поэтому остаётся корректным при любом числе прогонов в день.

Стек

  • aiogram 3 — Telegram-бот, управление через reply-клавиатуру
  • feedparser — разбор RSS/Atom
  • openai SDK → DeepSeek (OpenAI-совместимый API) — отбор и генерация
  • APScheduler — запуск пайплайна по расписанию (cron)
  • SQLAlchemy (async) + PostgreSQL — хранение каналов, их настроек и виденных записей
  • Alembic — миграции схемы (применяются на старте)

Управление ботом

Управление — только эмодзи-клавиатурой, без slash-команд и без инлайн-кнопок под сообщениями. Единственная команда — /start (встроенная кнопка «Start» в новом чате): она показывает справку и клавиатуру. Клавиатура двухуровневая и повторяет навигацию «выбор канала → настройка канала».

Экран 1 — каналы. Клавиатура показывает по кнопке на каждый канал (тап по названию открывает канал, Экран 2), а также «➕ Добавить канал», «📊 Статус» и «❓ Помощь». «➕ Добавить канал» запускает диалог: пришли ссылку (@username, https://t.me/имя или числовой id) — бот резолвит чат и проверяет, что он администратор канала с правом публиковать сообщения. Пригласительные ссылки (t.me/+…) не подходят.

Экран 2 — настройка канала. Открывается при выборе канала и показывает его текущие настройки, а клавиатура переключается на действия над ним: «🚀 Запустить», «👁 Предпросмотр», «📡 Лента», «📝 Сменить ленту», «🎨 Тон», «🕒 Часы», «🖼 Только с медиа / 📝 Разрешить текст» (фильтр публикаций, см. ниже), «⏸ Выключить / ▶️ Включить» (пауза/возобновление автопостинга) и «🗑 Удалить» (с подтверждением кнопками; вместе с каналом удаляется его история публикаций). Кнопка «⬅️ К каналам» возвращает на Экран 1.

Кнопка «🕒 Часы» открывает сетку 0–23 на клавиатуре: тап включает/выключает час (✅/▫️) для активного канала, изменения применяются сразу; «✅ Готово» — назад. Кнопка «🎨 Тон» открывает выбор пресета подачи поста (текущий помечен ✅).

Фильтр «🖼 Только с медиа». Когда включён, в канал идут лишь новости, у которых есть картинка или видео; чисто текстовые записи пропускаются. Если у новости несколько картинок — публикуются все (альбомом). Переключается по каналу, «📝 Разрешить текст» возвращает к публикации любых постов.

Пресеты тона (как DeepSeek пишет пост): news — сухо и нейтрально (по умолчанию), expert — факты плюс вывод «почему важно», casual — разговорный, ironic — с лёгкой иронией, hype — энергично, digest — тезисами. Выбранный пресет применяется сразу; сравнить вживую удобно через «👁 Предпросмотр».

При каждом запуске сервиса админу приходит сообщение «♻️ Сервис перезапущен» со сводкой по всем каналам (лента, часы, тон каждого, активный помечен ▶️) — чтобы тихий рестарт после сбоя не остался незамеченным.

Запуск через Docker (всё сразу)

Поднимает PostgreSQL и бота одной командой.

cp .env.example .env   # BOT_TOKEN, ADMIN_ID, DEEPSEEK_API_KEY
docker compose up --build -d
docker compose logs -f bot

DATABASE_URL для контейнера бота переопределяется в compose на хост db, так что значение в .env для Docker не важно. Остановить: docker compose down.

CI/CD (GitHub Actions)

Пайплайн .github/workflows/ci.yml:

  • Job test (на каждый push и PR в main): ruff → WPS → mypy → pytest через uv.
  • Job build-push (только push в main, после зелёных тестов): собирает образ bot и пушит в GHCR — ghcr.io/troyan-dy/tg_agg с тегами latest и <sha> (sha — для отката на конкретный коммит). Сборка идёт в Actions, не на сервере, и мультиарх — linux/amd64 (linux-сервер) + linux/arm64 (Apple Silicon), через QEMU + buildx. docker pull сам выбирает нужную архитектуру.

Сборка использует кэш слоёв GitHub Actions (type=gha), так что повторные сборки быстрые. Для пуша в GHCR прав хватает встроенного GITHUB_TOKEN.

Авто-деплой на сервере (cron)

Скрипт deploy/auto-update.sh раз в несколько минут пуллит готовый образ из GHCR (сборки на сервере больше нет) и поднимает docker-compose.prod.yml. Пересоздаётся лишь сервис bot (старый контейнер гасится через SIGTERM с grace-period — long polling останавливается штатно); db не трогается, данные сохраняются. Если ни образ, ни compose-файл не изменились — скрипт тихо выходит, ничего не делая.

docker-compose.prod.yml отличается от dev-овского только сервисом bot: вместо build: . — image: ghcr.io/troyan-dy/tg_agg:${BOT_TAG:-latest} (можно зафиксировать BOT_TAG=<sha> для отката).

Предпосылки на сервере

  • Установлены Docker и плагин compose; пользователь добавлен в группу docker (чтобы docker compose работал без sudo — иначе cron не сможет).
  • Репозиторий склонирован по HTTPS — на сервере он нужен только ради docker-compose.prod.yml, .env и самого скрипта (код приезжает образом):
    cd ~ && git clone https://github.com/troyan-dy/tg_agg.git
    cd ~/tg_agg && cp .env.example .env   # заполнить секреты
  • Доступ к GHCR. Если пакет приватный — один раз залогиниться (PAT с правом read:packages); если сделать пакет публичным в настройках GitHub — логин не нужен:
    echo <PAT> | docker login ghcr.io -u troyan-dy --password-stdin
  • Первый запуск — вручную: docker compose -f docker-compose.prod.yml up -d. Дальше обновления подхватывает cron.
  • Миграции применяются автоматически на старте (alembic upgrade head в app.main), отдельный шаг не нужен — на чистой БД схема создаётся сама.

Настройка cron

crontab -e

Добавить (подставив своё имя пользователя вместо USER):

PATH=/usr/local/bin:/usr/bin:/bin
*/5 * * * * /home/USER/tg_agg/deploy/auto-update.sh >> /home/USER/tg_agg/deploy/cron.log 2>&1
  • */5 * * * * — проверка каждые 5 минут.
  • Строка PATH=... обязательна: окружение cron урезанное, без неё не найдутся docker/git/flock.
  • >> cron.log ловит ошибки самого запуска (например, недоступен GitHub). Штатную работу скрипт пишет в deploy/auto-update.log.

Проверить, что обновления приходят:

tail -f ~/tg_agg/deploy/auto-update.log

Локальный запуск (uv)

Зависимостями управляет uv.

# 1. PostgreSQL
docker compose up -d db

# 2. Окружение (uv создаст .venv по pyproject.toml/uv.lock)
uv sync
cp .env.example .env   # BOT_TOKEN, ADMIN_ID, DEEPSEEK_API_KEY

# 3. Старт (на старте сам применит миграции: alembic upgrade head)
uv run python -m app.main

Полезное:

  • uv add <pkg> / uv remove <pkg> — изменить зависимости
  • uv sync --group dev — поставить и dev-зависимости (SQLite для офлайн-тестов)

Затем в чате с ботом: «➕ Добавить канал» (пришли ссылку) → «📝 Сменить ленту» → настрой часы/тон → «🚀 Запустить».

Миграции (Alembic)

Схему ведёт Alembic; app.main на старте сам гонит alembic upgrade head, так что обычно руками ничего делать не нужно. При изменении моделей:

uv run alembic revision --autogenerate -m "что изменилось"   # сгенерировать миграцию
uv run alembic upgrade head      # применить
uv run alembic downgrade -1      # откатить на шаг
uv run alembic check             # проверить, что модели и миграции не разошлись

URL базы и metadata берутся из app.config/app.models (migrations/env.py), а не из alembic.ini — секретов в ini нет.

Проверки (тесты, линтеры, типы)

Все технические проверки одной командой (Makefile):

make check        # ruff → WPS → mypy → pytest (останавливается на первой ошибке)

Отдельные цели, если нужно прогнать что-то одно:

make lint         # uv run ruff check .
make wps          # uv run flake8 . --select=WPS (wemake-python-styleguide, конфиг в setup.cfg)
make types        # uv run mypy
make test         # uv run pytest -q (покрытие включено, падает ниже порога)

В облачных сессиях Claude Code зависимости ставит SessionStart-хук (.claude/hooks/session-start.sh, uv sync --frozen), так что make check работает сразу. Локально хук ничего не делает.

Подготовка

  1. Бот у @BotFather → BOT_TOKEN.
  2. Свой ADMIN_ID — у @userinfobot.
  3. Ключ DeepSeek → DEEPSEEK_API_KEY (https://platform.deepseek.com).
  4. Каналы добавляются уже из чата: для каждого добавь бота администратором с правом публикации и пришли его ссылку через «➕ Добавить канал».
  5. RUN_HOURS — часы публикации (в TIMEZONE), напр. 9,13,18, и POST_TONE — пресет тона (news/expert/casual/ironic/hype/digest): это дефолты для нового канала. Дальше у каждого канала свои часы (кнопка «🕒 Часы») и тон (кнопка «🎨 Тон»).

Структура

app/
├── main.py              # точка входа: бот + планировщик
├── config.py            # настройки из .env
├── db.py                # движок, run_migrations (alembic upgrade head)
├── models.py            # Channel (канал + его настройки, в т.ч. require_media), SeenItem (дедуп), Setting (активный канал, FSM-состояние)
├── storage.py           # доступ к данным: каналы (CRUD), выбор активного, дедуп
├── tone.py              # пресеты тона постов (реестр + дефолт)
├── pipeline.py          # один прогон канала: RSS → отбор → генерация → публикация
├── bot/
│   ├── handlers.py      # управление каналами и настройками (только админ)
│   └── fsm_storage.py   # FSM-состояние в БД (переживает рестарт)
├── services/
│   ├── rss.py           # загрузка и нормализация ленты (картинки/видео)
│   └── deepseek.py      # выбор новости и генерация поста
└── scheduler/
    └── worker.py        # APScheduler: ежечасный тик, прогоняет «дозревшие» каналы
migrations/              # Alembic: env.py + versions/ (0001_initial — вся схема)

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages