# 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 ```bash # Новый сервис 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 ```bash # .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 ```json { "card": {"text": "...", "media": "/path/to/photo.jpg", "direction": "linux", "lang": "ru"}, "channels": ["@dedinit_vesti"] // опционально; default = VESTI_BOT_CHANNELS } ``` Ответ 200: ```json { "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 ```json {"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 ```bash 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": "Тест", "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)