Files
vesti/services/publisher/app/main.py
T

119 lines
4.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# publisher-service — изолированный FastAPI-микросервис публикации в Telegram.
# POST /api/v1/publish — публикация карточки в канал(ы). GET /healthz — статус.
# Единственная точка, знающая токен бота, прокси и каналы.
import os
from pathlib import Path
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field
from .channels import resolve_channels
from .config import settings
from . import telegram
app = FastAPI(title="VESTI Publisher Service", version="0.1.0")
# путь к .env для test/curl (вне контейнера) — читаем через config (load_dotenv уже там)
BASE = Path(__file__).resolve().parent.parent.parent
class Card(BaseModel):
text: str = Field(..., min_length=1, max_length=4096)
media: str | None = None # путь к локальному файлу (если есть)
direction: str = "linux"
lang: str = "ru"
class PublishRequest(BaseModel):
card: Card
channels: list[str] | None = None # MAY переопределять конфиг
dry_run: bool = False # тестовый режим: не уходит в Telegram
class ChannelResult(BaseModel):
message_id: int = 0
media_message_id: int | None = None
views: int = 0
error: str | None = None
class PublishResponse(BaseModel):
ok: bool
results: dict[str, ChannelResult]
dry_run: bool = False
@app.get("/healthz")
def healthz():
"""Статус сервиса: бот, прокси, каналы, токен."""
me_name = ""
try:
me = telegram.get_me()
me_name = me.get("username", "")
except Exception:
me_name = ""
return {
"status": "ok",
"bot": me_name or ("не проверен" if settings.bot_token else "нет токена"),
"token_set": bool(settings.bot_token),
"proxy": settings.tg_proxy,
"channels": settings.channels,
}
@app.post("/api/v1/publish", response_model=PublishResponse)
def publish(req: PublishRequest):
"""Публикует «богатый репост» в каналы. Возвращает message_id по каждому каналу.
Порядок: медиа первым (sendPhoto/sendVideo) → текст вторым (sendMessage с
link_preview_options disabled, чтобы не было предпросмотра ссылки).
"""
channels = resolve_channels(req.channels)
if not channels:
raise HTTPException(status_code=400, detail="Нет каналов для публикации")
# dry_run — тестовый режим: НЕ уходит в Telegram, возвращает эмуляцию результата
if req.dry_run:
results: dict[str, ChannelResult] = {
ch: ChannelResult() for ch in channels
}
return PublishResponse(ok=True, results=results, dry_run=True)
results: dict[str, ChannelResult] = {}
all_ok = True
for ch in channels:
res = ChannelResult()
try:
if req.card.media and Path(req.card.media).exists():
# медиа первым (caption короткий: первые 1000 символов, т.к. лимит 1024)
media_msg = telegram.send_media(ch, req.card.media, caption=req.card.text[:1000])
res.media_message_id = media_msg
# ПОЛНЫЙ текст отдельным сообщением (без предпросмотра ссылки)
msg = telegram.send_message(ch, req.card.text)
res.message_id = msg
else:
# без медиа: текст вторым; предпросмотр ссылки отключён (богатый репост)
msg = telegram.send_message(ch, req.card.text)
res.message_id = msg
res.views = telegram.get_views(ch, res.message_id)
except telegram.TelegramError as e:
res.error = e.message
all_ok = False
results[ch] = res
if not all_ok:
# одна ошибка не роняет остальные; статус 502 если все упали
if all(r.error for r in results.values()):
raise HTTPException(status_code=502, detail={ch: r.error for ch, r in results.items()})
return PublishResponse(ok=all_ok, results=results)
@app.get("/api/v1/views/{channel}/{message_id}")
def views(channel: str, message_id: int):
"""Views по каналу/сообщению (метрики)."""
return {"channel": channel, "message_id": message_id, "views": telegram.get_views(channel, message_id)}
if __name__ == "__main__":
import uvicorn
uvicorn.run("services.publisher.app.main:app", host="127.0.0.1", port=settings.port)