Files
2026-08-07 14:08:49 +08:00

87 lines
2.3 KiB
Python

"""Serialize, sign and send one webhook event."""
from __future__ import annotations
from dataclasses import dataclass
import hashlib
import hmac
import json
import time
from typing import Any
import httpx
from services.webhooks.security import validate_webhook_url
WEBHOOK_TIMEOUT_SECONDS = 10.0
@dataclass(frozen=True)
class DeliveryResult:
ok: bool
status_code: int | None
latency_ms: int
error: str = ""
def canonical_json(payload: dict[str, Any]) -> bytes:
return json.dumps(
payload,
ensure_ascii=False,
separators=(",", ":"),
sort_keys=True,
).encode("utf-8")
def webhook_signature(secret: str, timestamp: str, body: bytes) -> str:
signed = timestamp.encode("ascii") + b"." + body
digest = hmac.new(secret.encode("utf-8"), signed, hashlib.sha256).hexdigest()
return f"v1={digest}"
def should_retry(status_code: int | None) -> bool:
return status_code is None or status_code in {408, 429} or status_code >= 500
async def deliver_webhook(
*,
url: str,
secret: str,
event_id: str,
payload: dict[str, Any],
) -> DeliveryResult:
"""Deliver without following redirects; callers decide whether to retry."""
safe_url = await validate_webhook_url(url)
body = canonical_json(payload)
timestamp = str(int(time.time()))
headers = {
"Content-Type": "application/json",
"X-AIVideo-Event-Id": event_id,
"X-AIVideo-Timestamp": timestamp,
"X-AIVideo-Signature": webhook_signature(secret, timestamp, body),
}
started = time.monotonic()
try:
async with httpx.AsyncClient(
timeout=WEBHOOK_TIMEOUT_SECONDS,
follow_redirects=False,
) as client:
response = await client.post(safe_url, content=body, headers=headers)
latency_ms = round((time.monotonic() - started) * 1000)
if 200 <= response.status_code < 300:
return DeliveryResult(True, response.status_code, latency_ms)
return DeliveryResult(
False,
response.status_code,
latency_ms,
f"目标服务返回 HTTP {response.status_code}",
)
except httpx.HTTPError as exc:
return DeliveryResult(
False,
None,
round((time.monotonic() - started) * 1000),
str(exc)[:2048],
)