Files
vesti/openspec/changes/publisher-service/design.md
T

4.3 KiB

Design: publisher-service

Approach

Выносим публикацию в Telegram из веб-процесса в изолированный FastAPI-микросервис. Сервис — единственная точка, которая знает токен бота (прокси), каналы и Bot API. Остальные компоненты (веб, в будущем cron/боты) вызывают его по HTTP.

Схема:

vesti-web (:8400) ──POST /api/v1/publish──▶ publisher-service (:8410) ──Bot API (SOCKS5 127.0.0.1:1080)──▶ Telegram
   approve (человек)                          │  VESTI_BOT_TOKEN, VESTI_BOT_CHANNELS
                                               ▼
                                        Telegram: @dedinit_vesti (+ другие каналы)

Files

# Новый сервис
services/publisher/
├── app/
│   ├── __init__.py
│   ├── main.py            # FastAPI: POST /api/v1/publish, GET /healthz
│   ├── config.py          # env: VESTI_BOT_TOKEN, TG_PROXY, VESTI_BOT_CHANNELS, LISTEN_PORT
│   ├── telegram.py        # Bot API клиент (httpx + SOCKS5): send, get_views
│   └── channels.py        # разбор списка каналов (VESTI_BOT_CHANNELS)
├── requirements.txt       # fastapi, uvicorn, httpx[socks], pydantic, python-dotenv
├── Dockerfile             # python:slim, non-root, read-only fs
├── docker-compose.yml     # сервис, порт 8410, healthcheck, env из .env
├── .env.example           # без секретов
└── README.md              # API, порты, запуск, безопасность

# Изменения
web/app.py                  # approve → HTTP POST в publisher-service (вместо import publisher.bot)
.env.example                # + VESTI_BOT_CHANNELS, TG_PROXY
STATUS.md / TODO.md / WALKTHROUGH.md  # статус

Data / Config

# .env (реальные значения; НЕ коммитить)
VESTI_BOT_TOKEN=<токен @dedinit_controller_bot>   # уже в .env
TG_PROXY=socks5://127.0.0.1:1080                  # уже есть
VESTI_BOT_CHANNELS=@dedinit_vesti                 # список каналов, разделитель запятая
PUBLISHER_PORT=8410

API

POST /api/v1/publish

{
  "card": {"text": "...", "media": "/path/to/photo.jpg", "direction": "linux", "lang": "ru"},
  "channels": ["@dedinit_vesti"]          // опционально; default = VESTI_BOT_CHANNELS
}

Ответ 200:

{
  "ok": true,
  "results": {
    "@dedinit_vesti": {"message_id": 123, "media_message_id": 122, "views": 0}
  },
  "dry_run": false
}

Ошибки: 400 (невалидный card), 502 (Bot API / прокси недоступен), 403 (бот не админ).

GET /healthz

{"status": "ok", "bot": "@dedinit_controller_bot", "proxy": "socks5://127.0.0.1:1080", "channels": ["@dedinit_vesti"]}

Healthcheck: curl -f http://127.0.0.1:8410/healthz.

Commands

cd /opt/vesti
# dev (без Docker):
.venv/bin/pip install -r services/publisher/requirements.txt
.venv/bin/uvicorn services.publisher.app.main:app --host 127.0.0.1 --port 8410
curl -s http://127.0.0.1:8410/healthz

# prod (Docker):
docker compose -f services/publisher/docker-compose.yml up -d --build
curl -s http://127.0.0.1:8410/healthz
curl -s -X POST http://127.0.0.1:8410/api/v1/publish \
  -H 'Content-Type: application/json' \
  -d '{"card": {"text": "<b>Тест</b>", "direction": "linux", "lang": "ru"}}'

# переключение веба:
# в web/app.py заменить `from publisher.bot import publish_multi` на HTTP-клиент
# (или переменная окружения PUBLISHER_URL=http://127.0.0.1:8410)

Verification

  • openspec validate publisher-service → 0 ошибок
  • curl :8410/healthz → ok, бот, прокси, каналы
  • POST /api/v1/publish с тестовой карточкой → message_id в @dedinit_vesti
  • Бот не админ / прокси упал → понятная ошибка (4xx/502), веб показывает
  • approve в вебе → реальное сообщение в канале (микросервис вызван по HTTP)