64 lines
2.5 KiB
Python
64 lines
2.5 KiB
Python
"""Приёмник webhook-событий LiveKit (без JWT — верификация подписью LiveKit)."""
|
||
|
||
import logging
|
||
from typing import Annotated, Any, cast
|
||
|
||
from fastapi import APIRouter, Depends, Header, HTTPException, Request, status
|
||
from livekit import api
|
||
from sqlalchemy import CursorResult
|
||
from sqlalchemy.dialects.postgresql import insert as pg_insert
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
|
||
from core.config import get_settings
|
||
from core.db import get_session
|
||
from models.webhook_event import LivekitWebhookEvent
|
||
from services.webhook_handlers import WebhookDispatcher
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
router = APIRouter(prefix="/api/v1/livekit", tags=["livekit"])
|
||
|
||
|
||
@router.post("/webhook")
|
||
async def receive_webhook(
|
||
request: Request,
|
||
session: Annotated[AsyncSession, Depends(get_session)],
|
||
authorization: Annotated[str | None, Header()] = None,
|
||
) -> dict[str, str]:
|
||
"""Принять, верифицировать и обработать webhook-событие LiveKit.
|
||
|
||
Дедупликация по `event.id`: `INSERT ... ON CONFLICT DO NOTHING` в
|
||
`livekit_webhook_events` в одной транзакции с эффектами обработчика —
|
||
при конфликте (дубль) эффекты пропускаются, но ответ всё равно 200.
|
||
"""
|
||
settings = get_settings()
|
||
raw_body = await request.body()
|
||
|
||
receiver = api.WebhookReceiver(
|
||
api.TokenVerifier(settings.livekit_api_key, settings.livekit_api_secret)
|
||
)
|
||
try:
|
||
event = receiver.receive(raw_body.decode(), authorization or "")
|
||
except Exception as exc: # noqa: BLE001 — SDK кидает generic Exception на невалидную подпись
|
||
raise HTTPException(
|
||
status_code=status.HTTP_401_UNAUTHORIZED, detail="invalid_signature"
|
||
) from exc
|
||
|
||
insert_result = cast(
|
||
CursorResult[Any],
|
||
await session.execute(
|
||
pg_insert(LivekitWebhookEvent)
|
||
.values(event_id=event.id, event_type=event.event)
|
||
.on_conflict_do_nothing(index_elements=["event_id"])
|
||
),
|
||
)
|
||
if insert_result.rowcount == 0:
|
||
# Дубль уже обработанного события — пропускаем эффекты, но отвечаем 200.
|
||
await session.commit()
|
||
return {"status": "duplicate"}
|
||
|
||
dispatcher = WebhookDispatcher(session)
|
||
await dispatcher.dispatch(event)
|
||
await session.commit()
|
||
return {"status": "ok"}
|