diff --git a/src/backend/core/api/viewsets.py b/src/backend/core/api/viewsets.py index bae5442f..4f85ba59 100644 --- a/src/backend/core/api/viewsets.py +++ b/src/backend/core/api/viewsets.py @@ -47,6 +47,8 @@ from core.recording.worker.mediator import ( from . import permissions, serializers +from livekit import api as livekit_api + # pylint: disable=too-many-ancestors logger = getLogger(__name__) @@ -347,7 +349,35 @@ class RoomViewSet( {"message": f"Recording stopped for room {room.slug}."} ) - # todo - support a callback endpoint when a room is finished, to invalidate cached key + @decorators.action( + detail=False, + methods=["post"], + url_path="livekit-webhook", + permission_classes=[], + authentication_classes=[], + ) + def handle_livekit_webhook(self, request, pk=None): # pylint: disable=unused-argument + """Handle LiveKit webhook events.""" + auth_token = request.headers.get("Authorization") + if not auth_token: + return drf_response.Response( + {"error": "Missing LiveKit authentication token"}, + status=drf_status.HTTP_401_UNAUTHORIZED + ) + + token_verifier = livekit_api.TokenVerifier() + webhook_receiver = livekit_api.WebhookReceiver(token_verifier) + + webhook_data = webhook_receiver.receive(request.body.decode("utf-8"), auth_token) + + # Todo - livekit triggers a webhook for all events, see if we can restrict webhook to a limited number of events. + # Todo - handle Egress stopped / aborted events. + + if webhook_data.event == "room_finished": + room_id = webhook_data.room.name + utils.clear_cache_passphrase(room_id) + + return drf_response.Response({"message": f"Event processed"}) class ResourceAccessListModelMixin: diff --git a/src/backend/core/utils.py b/src/backend/core/utils.py index 2ff28135..53a035a4 100644 --- a/src/backend/core/utils.py +++ b/src/backend/core/utils.py @@ -28,25 +28,33 @@ def generate_random_passphrase(length=26): alphabet = string.ascii_letters + string.digits return ''.join(secrets.choice(alphabet) for _ in range(length)) -def get_cached_passphrase(room_id): - """Get or generate a cached passphrase for a room slug""" +def build_room_passphrase_key(room_id: str) -> str: + """Build cache key for room passphrase.""" + return f"room_passphrase:{room_id}" + +def get_cached_passphrase(room_id: str) -> str: + """Get or generate encrypted passphrase for a room. + + Retrieves existing passphrase from cache or generates, + encrypts and caches a new one if not found. + """ cypher = Fernet(settings.PASSPHRASE_ENCRYPTION_KEY.encode()) - - # todo - discuss this hardcoded key prefix - encrypted_passphrase = cache.get(f"room_passphrase:{room_id}") + cache_key = build_room_passphrase_key(room_id) + encrypted_passphrase = cache.get(cache_key) if encrypted_passphrase is None: passphrase = generate_random_passphrase() encrypted_passphrase = cypher.encrypt(passphrase.encode()).decode() - - # Store in cache with a timeout (e.g., 24 hours = 86400 seconds) - cache.set(f"room_passphrase:{room_id}", encrypted_passphrase, timeout=86400) - + cache.set(cache_key, encrypted_passphrase, timeout=86400) # 24 hours return passphrase return cypher.decrypt(encrypted_passphrase.encode()).decode() +def clear_room_passphrase(room_id: str) -> None: + """Remove room passphrase from cache.""" + cache.delete(build_room_passphrase_key(room_id)) + def generate_color(identity: str) -> str: """Generates a consistent HSL color based on a given identity string. diff --git a/src/helm/env.d/dev/values.livekit.yaml.gotmpl b/src/helm/env.d/dev/values.livekit.yaml.gotmpl index 1671e2b0..1168ea3d 100644 --- a/src/helm/env.d/dev/values.livekit.yaml.gotmpl +++ b/src/helm/env.d/dev/values.livekit.yaml.gotmpl @@ -17,6 +17,10 @@ livekit: udp_port: 443 domain: livekit.127.0.0.1.nip.io loadBalancerAnnotations: {} + webhook: + api_key: devkey + urls: + - https://meet.127.0.0.1.nip.io/api/v1.0/rooms/livekit-webhook/ loadBalancer: