"""Приёмник 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"}