diff --git a/src/backend/core/services/livekit_events.py b/src/backend/core/services/livekit_events.py index 980c49c9..ce9d2604 100644 --- a/src/backend/core/services/livekit_events.py +++ b/src/backend/core/services/livekit_events.py @@ -2,6 +2,7 @@ # pylint: disable=no-member +import re import uuid from enum import Enum from logging import getLogger @@ -92,6 +93,17 @@ class LiveKitEventsService: self.telephony_service = TelephonyService() self.recording_events = RecordingEventsService() + self._filter_regex = None + if settings.LIVEKIT_WEBHOOK_EVENTS_FILTER_REGEX: + try: + self._filter_regex = re.compile( + settings.LIVEKIT_WEBHOOK_EVENTS_FILTER_REGEX + ) + except re.error: + logger.exception( + "Invalid LIVEKIT_WEBHOOK_EVENTS_FILTER_REGEX. Webhook filtering disabled." + ) + def receive(self, request): """Process webhook and route to appropriate handler.""" @@ -106,6 +118,10 @@ class LiveKitEventsService: except Exception as e: raise InvalidPayloadError("Invalid webhook payload") from e + if self._filter_regex and not self._filter_regex.search(data.room.name): + logger.info("Filtered webhook event for room '%s'", data.room.name) + return + try: webhook_type = LiveKitWebhookEventType(data.event) except ValueError as e: diff --git a/src/backend/core/tests/services/test_livekit_events.py b/src/backend/core/tests/services/test_livekit_events.py index 24c2c82f..67ebeb0b 100644 --- a/src/backend/core/tests/services/test_livekit_events.py +++ b/src/backend/core/tests/services/test_livekit_events.py @@ -343,3 +343,99 @@ def test_receive_unsupported_event(mock_receive, service): UnsupportedEventTypeError, match="Unknown webhook type: unsupported_event" ): service.receive(mock_request) + + +@mock.patch.object(api.WebhookReceiver, "receive") +@mock.patch.object(LiveKitEventsService, "_handle_room_started") +def test_receive_no_filter_processes_all_events( + mock_handle_room_started, mock_receive, mock_livekit_config, settings +): + """Should process all events when filter regex is not configured.""" + settings.LIVEKIT_WEBHOOK_EVENTS_FILTER_REGEX = None + + mock_request = mock.MagicMock() + mock_request.headers = {"Authorization": "test_token"} + mock_request.body = b"{}" + + mock_data = mock.MagicMock() + mock_data.room.name = "!JIfCxVLcKKkWrmVBOb:your-domain.com" + mock_data.event = "room_started" + mock_receive.return_value = mock_data + + service = LiveKitEventsService() + service.receive(mock_request) + + mock_handle_room_started.assert_called_once() + + +@mock.patch.object(api.WebhookReceiver, "receive") +@mock.patch.object(LiveKitEventsService, "_handle_room_started") +def test_receive_invalid_filter_regex_processes_all_events( + mock_handle_room_started, mock_receive, mock_livekit_config, settings +): + """Should process all events when filter regex is invalid (fail-safe).""" + settings.LIVEKIT_WEBHOOK_EVENTS_FILTER_REGEX = "(abc" + + mock_request = mock.MagicMock() + mock_request.headers = {"Authorization": "test_token"} + mock_request.body = b"{}" + + mock_data = mock.MagicMock() + mock_data.room.name = "!JIfCxVLcKKkWrmVBOb:your-domain.com" + mock_data.event = "room_started" + mock_receive.return_value = mock_data + + service = LiveKitEventsService() + service.receive(mock_request) + + mock_handle_room_started.assert_called_once() + + +@mock.patch.object(api.WebhookReceiver, "receive") +@mock.patch.object(LiveKitEventsService, "_handle_room_started") +def test_receive_filter_drops_non_matching_events( + mock_handle_room_started, mock_receive, mock_livekit_config, settings +): + """Should drop events when room name does not match filter regex.""" + settings.LIVEKIT_WEBHOOK_EVENTS_FILTER_REGEX = ( + r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}" + ) + + mock_request = mock.MagicMock() + mock_request.headers = {"Authorization": "test_token"} + mock_request.body = b"{}" + + mock_data = mock.MagicMock() + mock_data.room.name = "!JIfCxVLcKKkWrmVBOb:your-domain.com" + mock_data.event = "room_started" + mock_receive.return_value = mock_data + + service = LiveKitEventsService() + service.receive(mock_request) + + mock_handle_room_started.assert_not_called() + + +@mock.patch.object(api.WebhookReceiver, "receive") +@mock.patch.object(LiveKitEventsService, "_handle_room_started") +def test_receive_filter_processes_matching_events( + mock_handle_room_started, mock_receive, mock_livekit_config, settings +): + """Should process events when room name matches filter regex.""" + settings.LIVEKIT_WEBHOOK_EVENTS_FILTER_REGEX = ( + r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}" + ) + + mock_request = mock.MagicMock() + mock_request.headers = {"Authorization": "test_token"} + mock_request.body = b"{}" + + mock_data = mock.MagicMock() + mock_data.room.name = str(uuid.uuid4()) + mock_data.event = "room_started" + mock_receive.return_value = mock_data + + service = LiveKitEventsService() + service.receive(mock_request) + + mock_handle_room_started.assert_called_once() diff --git a/src/backend/meet/settings.py b/src/backend/meet/settings.py index f835d209..12a345d0 100755 --- a/src/backend/meet/settings.py +++ b/src/backend/meet/settings.py @@ -517,6 +517,10 @@ class Base(Configuration): LIVEKIT_VERIFY_SSL = values.BooleanValue( True, environ_name="LIVEKIT_VERIFY_SSL", environ_prefix=None ) + # Regex to filter webhook events by room name. Only matching events are processed. + LIVEKIT_WEBHOOK_EVENTS_FILTER_REGEX = values.Value( + None, environ_name="LIVEKIT_WEBHOOK_EVENTS_FILTER_REGEX", environ_prefix=None + ) RESOURCE_DEFAULT_ACCESS_LEVEL = values.Value( "public", environ_name="RESOURCE_DEFAULT_ACCESS_LEVEL", environ_prefix=None )