Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
f95948a
chore(deps): drop unused lxml, feedparser and fake-useragent
k0te1ch Sep 1, 2026
2988dce
refactor(bot): drop WordPress settings the bot no longer reads
k0te1ch Sep 1, 2026
af526cc
refactor(bot): drop dead telethon_methods package
k0te1ch Sep 1, 2026
a32550a
fix(cli): make makemigrations exit non-zero when alembic fails
k0te1ch Sep 1, 2026
7c6b03a
refactor(bot): drop restating comments and the chained .get in get_ve…
k0te1ch Sep 1, 2026
68b7565
fix(bot): stop awaiting the synchronous shutdown and restart helpers
k0te1ch Sep 1, 2026
d45f57e
fix(bot): only delete the pin notices the bot made itself
k0te1ch Sep 1, 2026
f30c84b
fix(bot): split items that exceed the message limit on their own
k0te1ch Sep 10, 2026
1b57ebd
chore(deps): patch the high-severity alerts and lock the Boosty publi…
k0te1ch Sep 10, 2026
956aa10
build: install from the committed locks and pin poetry
k0te1ch Sep 10, 2026
c141de2
docs(boosty): explain why the upload host is a constant
k0te1ch Sep 10, 2026
cfd3347
docs: stop documenting a Patreon publisher that does not exist
k0te1ch Sep 10, 2026
6b8d466
fix(bot): read FTP_POSTSHOW_DIR instead of hardcoding the folder
k0te1ch Sep 10, 2026
6e1e489
chore(bot): clear stale TODO notes and two unused constants
k0te1ch Sep 10, 2026
1bd76d6
refactor(bot): split bot_methods into three focused modules
k0te1ch Sep 10, 2026
94abe04
refactor(wordpress): store the cookie jar as JSON instead of pickle
k0te1ch Sep 10, 2026
3675124
refactor(wordpress): split the client into transport, Podlove and post
k0te1ch Sep 10, 2026
ef9aa39
feat(bot): publish /start in the command menu and make the pin silenc…
k0te1ch Sep 10, 2026
6983401
chore: rewrite the Makefile against the paths that actually exist
k0te1ch Sep 10, 2026
2da5b24
ci: add a dependabot config and pin the Makefile to LF
k0te1ch Sep 10, 2026
eb385b7
test: move to pytest 9 and pytest-asyncio 1.x
k0te1ch Sep 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,17 @@ TELEGRAM_API_TOKEN = "1234567890:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
SKIP_UPDATES = "True"
#! Username публичной группы/канала для форварда анонсов (можно без @)
FORWARD_CHAT_USERNAME = "@my_group"
#! Закреплять анонс молча (без уведомления подписчикам)
FORWARD_PIN_SILENT = "False"


# FTP SETTINGS
FTP_SERVER = "test.io"
FTP_LOGIN = "user"
FTP_PASSWORD = "password"
#! Подпапка для послешоу: туда льёт FTP-публишер, оттуда бот читает
#! последний номер эпизода
FTP_POSTSHOW_DIR = "postshow"


# WORDPRESS
Expand Down
9 changes: 9 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Только точечные правила: без общего `* text=auto`, чтобы не спровоцировать
# перенормализацию окончаний строк во всём репозитории.

# Makefile требует настоящих табов и ломается от CR в рецептах.
Makefile text eol=lf
*.sh text eol=lf

# Локи генерируются — читать по ним diff незачем.
poetry.lock -diff linguist-generated
47 changes: 47 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Каталогов пять только потому, что каждый сервис — отдельный poetry-проект.
# Корня в списке нет: pyproject.toml/poetry.lock в корне репозитория не
# существует, и алерты, которые Dependabot заводил на "/", относились к
# файлу, удалённому ещё раньше.
version: 2
updates:
- package-ecosystem: pip
directories:
- /app/bot
- /app/publishers/FTP
- /app/publishers/WordPress
- /app/publishers/Boosty
schedule:
interval: weekly
open-pull-requests-limit: 5
groups:
# Один PR на все патч/минор-бампы: иначе четыре сервиса с почти
# одинаковым списком зависимостей дают четыре PR на каждый aiohttp.
pip:
patterns:
- "*"
update-types:
- minor
- patch
commit-message:
prefix: chore
include: scope

- package-ecosystem: github-actions
directory: /
schedule:
interval: monthly
commit-message:
prefix: ci

- package-ecosystem: docker
directories:
- /app/bot
- /app/kafka
- /app/schema-watcher
- /app/publishers/FTP
- /app/publishers/WordPress
- /app/publishers/Boosty
schedule:
interval: monthly
commit-message:
prefix: build
37 changes: 22 additions & 15 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,15 @@ jobs:
with:
python-version: "3.12"
- name: Install Poetry
run: python -m pip install --upgrade pip poetry
# Pin poetry: the committed locks were generated by this version, and an
# unpinned `--upgrade` has twice downgraded virtualenv mid-install and
# broken poetry's own files (flaky 'Cannot install ...' failures).
run: python -m pip install --upgrade pip && python -m pip install poetry==2.3.4
- name: Install bot dependencies (system env, mirrors Dockerfile)
working-directory: app/bot
# --only main,testing: the dev group (a non-optional group, so installed
# by default) pulls poetry-plugin-up -> an old poetry -> dulwich<0.22,
# which with virtualenvs.create false clobbers Poetry's own dulwich
# mid-install and breaks fetching the dialog-engine git dependency
# ("No module named dulwich.worktree"). Tests need pytest-cov, which now
# lives in the testing group.
# --only main,testing mirrors the Dockerfile (main) plus what the suite
# needs (pytest, pytest-cov). The dev group holds ruff and pre-commit,
# which have their own pinned install in the lint job.
run: |
poetry config virtualenvs.create false
poetry install --only main,testing --no-root
Expand All @@ -65,14 +65,17 @@ jobs:
with:
python-version: "3.12"
- name: Install Poetry
run: python -m pip install --upgrade pip poetry
# Pin poetry: the committed locks were generated by this version, and an
# unpinned `--upgrade` has twice downgraded virtualenv mid-install and
# broken poetry's own files (flaky 'Cannot install ...' failures).
run: python -m pip install --upgrade pip && python -m pip install poetry==2.3.4
- name: Install FTP publisher dependencies (system env, mirrors Dockerfile)
working-directory: app/publishers/FTP
run: |
poetry config virtualenvs.create false
poetry install --only main --no-root
- name: Install test tooling
run: python -m pip install "pytest>=8.3.2,<9" "pytest-asyncio>=0.24,<0.25"
run: python -m pip install "pytest>=9.0.3,<10" "pytest-asyncio>=1.4,<2"
- name: Run FTP publisher tests
run: pytest -c app/bot/pyproject.toml -o addopts="" tests/unit/publishers/ftp

Expand All @@ -84,15 +87,17 @@ jobs:
with:
python-version: "3.12"
- name: Install Poetry
run: python -m pip install --upgrade pip poetry
# Pin poetry: the committed locks were generated by this version, and an
# unpinned `--upgrade` has twice downgraded virtualenv mid-install and
# broken poetry's own files (flaky 'Cannot install ...' failures).
run: python -m pip install --upgrade pip && python -m pip install poetry==2.3.4
- name: Install WordPress publisher dependencies (system env, mirrors Dockerfile)
working-directory: app/publishers/WordPress
run: |
poetry config virtualenvs.create false
poetry lock
poetry install --only main --no-root
- name: Install test tooling
run: python -m pip install "pytest>=8.3.2,<9" "pytest-asyncio>=0.24,<0.25"
run: python -m pip install "pytest>=9.0.3,<10" "pytest-asyncio>=1.4,<2"
- name: Run WordPress publisher tests
run: pytest -c app/bot/pyproject.toml -o addopts="" tests/unit/publishers/wordpress

Expand All @@ -104,14 +109,16 @@ jobs:
with:
python-version: "3.12"
- name: Install Poetry
run: python -m pip install --upgrade pip poetry
# Pin poetry: the committed locks were generated by this version, and an
# unpinned `--upgrade` has twice downgraded virtualenv mid-install and
# broken poetry's own files (flaky 'Cannot install ...' failures).
run: python -m pip install --upgrade pip && python -m pip install poetry==2.3.4
- name: Install Boosty publisher dependencies (system env, mirrors Dockerfile)
working-directory: app/publishers/Boosty
run: |
poetry config virtualenvs.create false
poetry lock
poetry install --only main --no-root
- name: Install test tooling
run: python -m pip install "pytest>=8.3.2,<9" "pytest-asyncio>=0.24,<0.25"
run: python -m pip install "pytest>=9.0.3,<10" "pytest-asyncio>=1.4,<2"
- name: Run Boosty publisher tests
run: pytest -c app/bot/pyproject.toml -o addopts="" tests/unit/publishers/boosty
99 changes: 66 additions & 33 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,51 +1,84 @@
# NOT WORKING IN *NIX
# Локальная разработка. Каждый сервис — отдельный poetry-проект, поэтому почти
# всё делается через `cd <dir> && poetry run ...`. Прод поднимается через
# docker compose (см. README), а не отсюда.
#
# Две причины, почему рецепты выглядят так, а не короче:
# * `poetry -C <dir> run` выполняет команду с cwd=<dir>, поэтому относительные
# пути к tests/ из корня ломаются — отсюда `cd` и $(CURDIR);
# * корневого pyproject.toml нет, и конфиг ruff (line-length 119) подхватывается
# только из app/bot. Из корня ruff берёт дефолтные 88 и форматирует иначе,
# чем проверит CI.

pip-tools:
python -m pip install -U pip
python -m pip install -U poetry
poetry add poetry-plugin-up --group dev
poetry add pre-commit --group dev
BOT := app/bot
PUBLISHERS := app/publishers/FTP app/publishers/WordPress app/publishers/Boosty

requirements: pip-tools
poetry install --with=dev,test
.PHONY: install install-publishers lock update lint format test test-publishers check run migrate makemigrations docker-build docker-up docker-stop docker-logs

run:
python app/main.py run
# --- зависимости ------------------------------------------------------------

install:
poetry -C $(BOT) install --with testing,dev

install-publishers:
@for d in $(PUBLISHERS); do echo "--- $$d"; poetry -C $$d install || exit 1; done

lock:
poetry -C $(BOT) lock
@for d in $(PUBLISHERS); do poetry -C $$d lock || exit 1; done

update:
# poetry-plugin-up — плагин самого poetry, а не зависимость проекта: как
# зависимость он пинил древний poetry и тянул уязвимый dulwich.
poetry self add poetry-plugin-up
poetry -C $(BOT) up
poetry -C $(BOT) run pre-commit autoupdate

# --- проверки ---------------------------------------------------------------

async-makemigrations:
python app/main.py makemigrations -s False
lint:
cd $(BOT) && poetry run ruff check "$(CURDIR)/app" "$(CURDIR)/tests"
cd $(BOT) && poetry run ruff format --check "$(CURDIR)/app" "$(CURDIR)/tests"

async-migrate:
python app/main.py migrate -s False
format:
cd $(BOT) && poetry run ruff check --fix "$(CURDIR)/app" "$(CURDIR)/tests"
cd $(BOT) && poetry run ruff format "$(CURDIR)/app" "$(CURDIR)/tests"

# Тесты публишеров требуют зависимостей своих сервисов (asyncssh, aioprometheus,
# boosty), которых нет в окружении бота — поэтому отдельная цель, как и в CI.
test:
poetry run pytest --cov=.
cd $(BOT) && poetry run python -m pytest -c pyproject.toml --rootdir "$(CURDIR)" -o addopts="--cov=$(CURDIR)/app -m 'not e2e'" --ignore "$(CURDIR)/tests/e2e" --ignore "$(CURDIR)/tests/unit/publishers" "$(CURDIR)/tests"

test-publishers:
cd app/publishers/FTP && poetry run python -m pytest -c "$(CURDIR)/$(BOT)/pyproject.toml" --rootdir "$(CURDIR)" -o addopts="" "$(CURDIR)/tests/unit/publishers/ftp"
cd app/publishers/WordPress && poetry run python -m pytest -c "$(CURDIR)/$(BOT)/pyproject.toml" --rootdir "$(CURDIR)" -o addopts="" "$(CURDIR)/tests/unit/publishers/wordpress"
cd app/publishers/Boosty && poetry run python -m pytest -c "$(CURDIR)/$(BOT)/pyproject.toml" --rootdir "$(CURDIR)" -o addopts="" "$(CURDIR)/tests/unit/publishers/boosty"

check:
poetry run pre-commit run --show-diff-on-failure --color=always --all-files
poetry -C $(BOT) run pre-commit run --show-diff-on-failure --color=always --all-files

update: pip-tools
poetry update
poetry run poetry up
poetry run pre-commit autoupdate
# --- запуск бота локально ---------------------------------------------------

lock:
poetry lock
# main.py импортирует свои модули голыми именами (config, handlers, ...), так что
# запускать только из app/bot.
run:
cd $(BOT) && poetry run python main.py run

docker-build:
docker-compose --env-file .env build
migrate:
cd $(BOT) && poetry run python main.py migrate -s False

makemigrations:
cd $(BOT) && poetry run python main.py makemigrations -s False

docker-test: docker-build
docker-compose run test-runner pytest
# --- docker -----------------------------------------------------------------

docker-run:
docker-compose up -d --force-recreate
docker-build:
docker compose --env-file .env build

docker-up: docker-build
docker-compose up -d --force-recreate
docker-up:
docker compose up -d

docker-stop:
docker-compose down
docker compose down

docker-rm: docker-stop
docker-compose rm
docker-logs:
docker compose logs -f --tail=200 bot publisher_ftp publisher_wordpress publisher_boosty
25 changes: 12 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@ Telegram-бот для подкаст-команды [«Разговорный
и chapters, пересылка анонса в чат.

Стек микросервисный: бот собирает запрос → шлёт в Kafka → отдельные
publisher-сервисы (FTP, WordPress, Boosty, Patreon) забирают и выполняют →
publisher-сервисы (FTP, WordPress, Boosty) забирают и выполняют →
возвращают результат через Kafka → бот апдейтит сообщение в Telegram. Полное
наблюдение через Prometheus + Loki + Grafana.

Boosty и Patreon — платные площадки: на них уходит только aftershow
(послешоу), пост публикуется на платном уровне/tier подписки (см.
`BasePublisher.is_paywalled`). FTP и WordPress — для основного эпизода.
Boosty — платная площадка: туда уходит только aftershow (послешоу), пост
публикуется на платном уровне подписки (см. `BasePublisher.is_paywalled`).
FTP и WordPress — для основного эпизода. Patreon заложен в `BasePublisher`
тем же способом, но сервиса под него ещё нет.

## Что внутри

Expand All @@ -23,8 +24,7 @@ app/
│ ├── FTP/ # ftp upload эпизода на хостинг
│ ├── WordPress/ # WP post-new form + Podlove REST API (Application Password)
│ │ # для метаданных и chapters
│ ├── Boosty/ # пост на платном уровне (mp3 + обложка-тизер), internal API — aftershow
│ └── Patreon/ # пост для патронов на платном tier (official API) — aftershow
│ └── Boosty/ # пост на платном уровне (mp3 + обложка-тизер), internal API — aftershow
├── kafka/ # kafka-init: создаёт топики из topics.yaml при старте кластера
├── schema-watcher/ # регистрирует Avro-схемы в Schema Registry
└── shared/
Expand All @@ -43,13 +43,13 @@ utils/bootstrap.sh # первичный деплой на чистый pr
Telegram user
podboxbot_bot ──► Kafka topic publisher.{ftp,wordpress,boosty,patreon}.upload
podboxbot_bot ──► Kafka topic publisher.{ftp,wordpress,boosty}.upload
podboxbot_publisher_{ftp,wordpress,boosty,patreon}
podboxbot_publisher_{ftp,wordpress,boosty}
Kafka topic publisher.{ftp,wordpress,boosty,patreon}.result
Kafka topic publisher.{ftp,wordpress,boosty}.result
podboxbot_bot (Telegram edit_message)
Expand Down Expand Up @@ -153,7 +153,7 @@ docker compose version # должно показать v2.x

```bash
git pull
docker compose build --no-cache bot publisher_ftp publisher_wordpress publisher_boosty publisher_patreon
docker compose build --no-cache bot publisher_ftp publisher_wordpress publisher_boosty
docker compose up -d
```

Expand Down Expand Up @@ -188,7 +188,6 @@ Schema Registry. Защита выстроена в три слоя:
- `cd app/publishers/FTP && poetry install`
- `cd app/publishers/WordPress && poetry install`
- `cd app/publishers/Boosty && poetry install`
- `cd app/publishers/Patreon && poetry install`
- Lint: `poetry -C app/bot run ruff check app`
- Unit-тесты бота:
`poetry -C app/bot run pytest -c app/bot/pyproject.toml --ignore=tests/unit/publishers -q`
Expand All @@ -198,8 +197,8 @@ Schema Registry. Защита выстроена в три слоя:
`poetry -C app/publishers/FTP run pytest -c app/bot/pyproject.toml -o addopts="" tests/unit/publishers/ftp -q`
- CI (GitHub Actions, `.github/workflows/ci.yml`): ruff lint + format,
тесты бота и отдельная джоба на каждый publisher-сервис
(`test-publisher-ftp` / `test-publisher-wordpress` / `test-publisher-boosty`
/ `test-publisher-patreon`). `main` всегда зелёный.
(`test-publisher-ftp` / `test-publisher-wordpress` /
`test-publisher-boosty`). `main` всегда зелёный.
- Ветки: GitHub flow — ветка на задачу (`feat/...`, `fix/...`,
`chore/...`) → PR в `main` → squash-merge. Напрямую в `main` не пушим;
`main` всегда releasable.
Expand Down
2 changes: 1 addition & 1 deletion app/bot/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ ENV LC_ALL=ru_RU.UTF-8

# Устанавливаем Poetry и необходимые инструменты сборки
RUN python -m pip install --upgrade pip setuptools wheel && \
python -m pip install --upgrade poetry
python -m pip install poetry==2.3.4

# Копируем файлы зависимостей и устанавливаем их
COPY app/bot/pyproject.toml app/bot/poetry.lock /app/
Expand Down
14 changes: 9 additions & 5 deletions app/bot/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,10 @@ async def _run():
await init_scheduler_jobs()
logger.success("Schedulers init jobs configurated and scheduler started!")

# Добавляем команды в бота
from aiogram.types import BotCommandScopeAllPrivateChats

# TODO BotCommand или в env, или формировать при помощи handlers
# COMMANDS живёт рядом с роутерами (handlers/__init__.py), чтобы список
# команд и их обработчики правились в одном месте.
await main.bot.set_my_commands(
commands=COMMANDS,
scope=BotCommandScopeAllPrivateChats(),
Expand Down Expand Up @@ -127,11 +127,15 @@ def makemigrations(message, sync):
version_path=None,
rev_id=None,
)
print("Alembic revision")
click.echo("Alembic revision")
except CommandError as err:
print(f"Alembic Command Error: {err}")
msg = f"Alembic Command Error: {err}"
if str(err) == "Target database is not up to date.":
print('Run "python main.py migrate"')
msg += ' Run "python main.py migrate" first.'
# ClickException prints to stderr and exits 1. The command used to
# print the failure and still exit 0, so a failed revision read as
# success to anything checking the exit code.
raise click.ClickException(msg) from err


@logger.catch
Expand Down
Loading
Loading