From f0a17b1ce11ad8c8c72c5e93ecc25b025a4fd8a2 Mon Sep 17 00:00:00 2001 From: lebaudantoine Date: Tue, 15 Jul 2025 17:20:52 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8(backend)=20add=20dedicated=20service?= =?UTF-8?q?=20for=20LiveKit=20recording=20webhook=20events?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Create new service to handle recording-related webhooks, starting with limit reached events. Will expand to enhance UX by notifying backend of other LiveKit events. Doesn't fit cleanly with existing recording package - may need broader redesign. Chose dedicated service over mixing responsibilities. --- .../core/recording/services/__init__.py | 0 .../recording/services/recording_events.py | 49 +++++++++++++ .../core/tests/recording/service/__init__.py | 0 .../service/test_recording_events.py | 72 +++++++++++++++++++ 4 files changed, 121 insertions(+) create mode 100644 src/backend/core/recording/services/__init__.py create mode 100644 src/backend/core/recording/services/recording_events.py create mode 100644 src/backend/core/tests/recording/service/__init__.py create mode 100644 src/backend/core/tests/recording/service/test_recording_events.py diff --git a/src/backend/core/recording/services/__init__.py b/src/backend/core/recording/services/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/backend/core/recording/services/recording_events.py b/src/backend/core/recording/services/recording_events.py new file mode 100644 index 00000000..f24a7b02 --- /dev/null +++ b/src/backend/core/recording/services/recording_events.py @@ -0,0 +1,49 @@ +"""Recording-related LiveKit Events Service""" + +from logging import getLogger + +from core import models, utils + +logger = getLogger(__name__) + + +class RecordingEventsError(Exception): + """Recording event handling fails.""" + + +class RecordingEventsService: + """Handles recording-related Livekit webhook events.""" + + @staticmethod + def handle_limit_reached(recording): + """Stop recording and notify participants when limit is reached.""" + + recording.status = models.RecordingStatusChoices.STOPPED + recording.save() + + notification_mapping = { + models.RecordingModeChoices.SCREEN_RECORDING: "screenRecordingLimitReached", + models.RecordingModeChoices.TRANSCRIPT: "transcriptionLimitReached", + } + + notification_type = notification_mapping.get(recording.mode) + if not notification_type: + return + + try: + utils.notify_participants( + room_name=str(recording.room.id), + notification_data={"type": notification_type}, + ) + except utils.NotificationError as e: + logger.exception( + "Failed to notify participants about recording limit reached: " + "room=%s, recording_id=%s, mode=%s", + recording.room.id, + recording.id, + recording.mode, + ) + raise RecordingEventsError( + f"Failed to notify participants in room '{recording.room.id}' about " + f"recording limit reached (recording_id={recording.id})" + ) from e diff --git a/src/backend/core/tests/recording/service/__init__.py b/src/backend/core/tests/recording/service/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/backend/core/tests/recording/service/test_recording_events.py b/src/backend/core/tests/recording/service/test_recording_events.py new file mode 100644 index 00000000..408f7f70 --- /dev/null +++ b/src/backend/core/tests/recording/service/test_recording_events.py @@ -0,0 +1,72 @@ +""" +Test RecordingEventsService service. +""" + +# pylint: disable=W0621 + +from unittest import mock + +import pytest + +from core.factories import RecordingFactory +from core.recording.services.recording_events import ( + RecordingEventsError, + RecordingEventsService, +) +from core.utils import NotificationError + +pytestmark = pytest.mark.django_db + + +@pytest.fixture +def service(): + """Initialize RecordingEventsService.""" + return RecordingEventsService() + + +@pytest.mark.parametrize( + ("mode", "notification_type"), + ( + ("screen_recording", "screenRecordingLimitReached"), + ("transcript", "transcriptionLimitReached"), + ), +) +@mock.patch("core.utils.notify_participants") +def test_handle_limit_reached_success(mock_notify, mode, notification_type, service): + """Test handle_limit_reached stops recording and notifies participants.""" + + recording = RecordingFactory(status="active", mode=mode) + service.handle_limit_reached(recording) + + assert recording.status == "stopped" + mock_notify.assert_called_once_with( + room_name=str(recording.room.id), notification_data={"type": notification_type} + ) + + +@pytest.mark.parametrize( + ("mode", "notification_type"), + ( + ("screen_recording", "screenRecordingLimitReached"), + ("transcript", "transcriptionLimitReached"), + ), +) +@mock.patch("core.utils.notify_participants") +def test_handle_limit_reached_error(mock_notify, mode, notification_type, service): + """Test handle_limit_reached raises RecordingEventsError when notification fails.""" + + mock_notify.side_effect = NotificationError("Error notifying") + + recording = RecordingFactory(status="active", mode=mode) + + with pytest.raises( + RecordingEventsError, + match=r"Failed to notify participants in room '.+' " + r"about recording limit reached \(recording_id=.+\)", + ): + service.handle_limit_reached(recording) + + assert recording.status == "stopped" + mock_notify.assert_called_once_with( + room_name=str(recording.room.id), notification_data={"type": notification_type} + )