mirror of
https://gitverse.ru/kpa39l/vesti.git
synced 2026-09-29 09:55:03 +00:00
fediverse: публикация в GoToSocial (@vesti@dedinit.ru) из publisher-service
- gotosocial.py: клиент GtS (httpx): post_status (markdown), upload_media v2, verify_credentials, delete_status - config.py: gt_social_url/token/visibility/format/channels - channels.py: fediverse-каналы (VESTI_GTS_CHANNELS, префикс gt:), gt: уходит только в fediverse - main.py: _publish_gotosocial + healthz-блок gotosocial + dry_run; status_id (ULID) в ответе - .env.example: GT_SOCIAL_* и VESTI_GTS_CHANNELS - STATUS.md/WALKTHROUGH.md: fediverse-раздел, хронология - openspec: change gotosocial-publisher (proposal/design/tasks/specs), validate чисто
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
# Клиент GoToSocial (Mastodon-совместимый REST API).
|
||||
# GtS 0.22.x: POST /api/v1/statuses, POST /api/v2/media, GET /api/v1/accounts/verify_credentials,
|
||||
# DELETE /api/v1/statuses/{id}. Аутентификация: Bearer-токен пользователя (scope write).
|
||||
# GoToSocial доступен напрямую (внутренняя сеть WG → vps02 → bigbox), прокси НЕ используется
|
||||
# (в отличие от Telegram, который из РФ доступен только через SOCKS5).
|
||||
import httpx
|
||||
|
||||
from .config import settings
|
||||
|
||||
TIMEOUT = 25.0
|
||||
MAX_MEDIA = 6 # лимит GtS на вложения
|
||||
MAX_TEXT = 5000 # лимит GtS на длину статуса
|
||||
|
||||
|
||||
class GoToSocialError(Exception):
|
||||
"""Ошибка GtS API / сети. Код для HTTP-ответа сервиса (по образцу TelegramError)."""
|
||||
|
||||
def __init__(self, message: str, http_code: int = 502, detail: str = ""):
|
||||
super().__init__(message)
|
||||
self.message = message
|
||||
self.http_code = http_code
|
||||
self.detail = detail
|
||||
|
||||
|
||||
def _client() -> httpx.Client:
|
||||
"""Клиент с Bearer-токеном. Проверяет обязательные настройки ДО запроса."""
|
||||
if not settings.gt_social_url:
|
||||
raise GoToSocialError("GT_SOCIAL_URL не задан", http_code=500)
|
||||
if not settings.gt_social_token:
|
||||
raise GoToSocialError("GT_SOCIAL_ACCESS_TOKEN не задан", http_code=500)
|
||||
return httpx.Client(
|
||||
base_url=settings.gt_social_url.rstrip("/"),
|
||||
headers={"Authorization": f"Bearer {settings.gt_social_token}"},
|
||||
timeout=TIMEOUT,
|
||||
)
|
||||
|
||||
|
||||
def _handle(r: httpx.Response, what: str) -> None:
|
||||
if r.status_code not in (200, 201):
|
||||
raise GoToSocialError(
|
||||
f"{what}: HTTP {r.status_code} {r.text[:200]}",
|
||||
http_code=403 if r.status_code == 403 else 502,
|
||||
detail=r.text[:200],
|
||||
)
|
||||
|
||||
|
||||
def post_status(text: str, media_ids: list[str] | None = None, visibility: str = "public") -> str:
|
||||
"""Создаёт статус. Возвращает id статуса (str из JSON GtS).
|
||||
|
||||
format=markdown — GtS поддерживает markdown-разметку статусов
|
||||
(см. /api/v1/instance: supported_mime_types включает text/markdown).
|
||||
"""
|
||||
if not text:
|
||||
raise GoToSocialError("text пуст", http_code=400)
|
||||
payload = {
|
||||
"status": text[:MAX_TEXT],
|
||||
"visibility": visibility,
|
||||
"format": settings.gt_social_format, # "markdown" | "plain"
|
||||
}
|
||||
if media_ids:
|
||||
payload["media_ids"] = media_ids[:MAX_MEDIA]
|
||||
with _client() as c:
|
||||
r = c.post("/api/v1/statuses", json=payload)
|
||||
_handle(r, "statuses")
|
||||
return str(r.json()["id"])
|
||||
|
||||
|
||||
def upload_media(path: str) -> str:
|
||||
"""Загружает медиафайл (multipart), возвращает media_id."""
|
||||
if not path:
|
||||
raise GoToSocialError("media path пуст", http_code=400)
|
||||
try:
|
||||
with _client() as c, open(path, "rb") as f:
|
||||
r = c.post("/api/v2/media", files={"file": f}, data={"description": ""})
|
||||
except FileNotFoundError:
|
||||
raise GoToSocialError(f"медиафайл не найден: {path}", http_code=400)
|
||||
_handle(r, "media")
|
||||
return str(r.json()["id"])
|
||||
|
||||
|
||||
def verify_credentials() -> dict | None:
|
||||
"""Проверка токена: GET /api/v1/accounts/verify_credentials. None при ошибке."""
|
||||
try:
|
||||
with _client() as c:
|
||||
r = c.get("/api/v1/accounts/verify_credentials")
|
||||
if r.status_code == 200:
|
||||
return r.json()
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def delete_status(status_id: str) -> bool:
|
||||
"""Удаляет статус. True если GtS вернул 200 (или 404 — уже удалён)."""
|
||||
try:
|
||||
with _client() as c:
|
||||
r = c.delete(f"/api/v1/statuses/{status_id}")
|
||||
return r.status_code in (200, 404)
|
||||
except Exception:
|
||||
return False
|
||||
Reference in New Issue
Block a user