"""Отправка в системные webhooks (s2s): подпись app/signature.py, docs/05.
Секрет цели — per-target: channel_targets.config = {"endpoint": …, "token_ref":
"navi-rei"}; значение секрета — в окружении (S2S_SECRET_NAVI_REI, .env /
gnexus-creds), в БД только ссылка. Ротация секрета цели не трогает правила.
"""
import json
import os
import time
import httpx
from app.models import ChannelTarget, Event
from app.signature import make_signature
#: Референс-получателю достаточно этих трёх; остальное — настройка цели.
S2S_TIMEOUT = httpx.Timeout(10.0)
def secret_env_name(token_ref: str) -> str:
"""token_ref «navi-rei» → переменная окружения S2S_SECRET_NAVI_REI."""
cleaned = token_ref.upper().replace("-", "_").replace(".", "_")
return f"S2S_SECRET_{cleaned}"
def resolve_secret(token_ref: str) -> str | None:
return os.environ.get(secret_env_name(token_ref)) or None
def send_s2s(target: ChannelTarget, event: Event, source_name: str) -> tuple[bool, str | None]:
"""POST конверта события в endpoint цели, подписанный по схеме gnexus-auth.
Тело — конверт без изменений + event_id (см. docs/05):
описание источника в него не входит. Успех — любой 2xx; прочее —
провал попытки (воркер поставит ретрай).
"""
cfg = target.config or {}
endpoint = cfg.get("endpoint")
token_ref = cfg.get("token_ref")
if not endpoint:
return False, "s2s: в config цели нет endpoint"
secret = resolve_secret(token_ref) if token_ref else None
if not secret:
return False, (
f"s2s: секрет цели не задан в окружении "
f"({secret_env_name(token_ref) if token_ref else 'нет token_ref в config'})"
)
envelope = {
"event_id": str(event.id),
"source": source_name,
"subject": event.subject,
"action": event.action,
"priority": event.priority,
"payload": dict(event.payload or {}),
}
body = json.dumps(envelope, ensure_ascii=False, separators=(",", ":")).encode()
headers = {
"Content-Type": "application/json",
"X-Gnexus-Event-Id": str(event.id),
"X-Gnexus-Event-Type": f"{source_name}.{event.subject}.{event.action}",
"X-Gnexus-Event-Timestamp": str(int(time.time())),
"X-Gnexus-Signature": make_signature(body, secret),
"X-Synapse-Source": source_name,
"User-Agent": "Synapse/0.1 (gnexus notification hub)",
}
try:
response = httpx.post(
endpoint,
content=body,
headers=headers,
timeout=S2S_TIMEOUT,
verify=cfg.get("verify_tls", True),
follow_redirects=False,
)
except httpx.HTTPError as err:
return False, f"s2s: сеть: {err}"
if 200 <= response.status_code < 300:
return True, None
return False, f"s2s: HTTP {response.status_code} · {response.text[:200]}"