diff --git a/env.d/development/common.dist b/env.d/development/common.dist index 5018b1e4..3085e716 100644 --- a/env.d/development/common.dist +++ b/env.d/development/common.dist @@ -12,6 +12,10 @@ PYTHONPATH=/app # Mail DJANGO_EMAIL_HOST="mailcatcher" DJANGO_EMAIL_PORT=1025 +DJANGO_EMAIL_BRAND_NAME=La Suite Numérique +DJANGO_EMAIL_SUPPORT_EMAIL=test@yopmail.com +DJANGO_EMAIL_LOGO_IMG=http://localhost:3000/assets/logo-suite-numerique.png +DJANGO_EMAIL_DOMAIN=http://localhost:3000/ # Backend url MEET_BASE_URL="http://localhost:8072" @@ -42,3 +46,6 @@ LIVEKIT_API_SECRET=secret LIVEKIT_API_KEY=devkey LIVEKIT_API_URL=http://localhost:7880 ALLOW_UNREGISTERED_ROOMS=False + +# Recording +SCREEN_RECORDING_BASE_URL=http://localhost:3000/recordings diff --git a/src/backend/core/recording/event/notification.py b/src/backend/core/recording/event/notification.py index 49cf741c..32d2d030 100644 --- a/src/backend/core/recording/event/notification.py +++ b/src/backend/core/recording/event/notification.py @@ -1,8 +1,13 @@ """Service to notify external services when a new recording is ready.""" import logging +import smtplib from django.conf import settings +from django.core.mail import send_mail +from django.template.loader import render_to_string +from django.utils.translation import get_language, override +from django.utils.translation import gettext_lazy as _ import requests @@ -21,10 +26,7 @@ class NotificationService: return self._notify_summary_service(recording) if recording.mode == models.RecordingModeChoices.SCREEN_RECORDING: - logger.warning( - "Screen recording mode not implemented for recording %s", recording.id - ) - return False + return self._notify_user_by_email(recording) logger.error( "Unknown recording mode %s for recording %s", @@ -33,6 +35,59 @@ class NotificationService: ) return False + @staticmethod + def _notify_user_by_email(recording) -> bool: + """ + Send an email notification to recording owners when their recording is ready. + + The email includes a direct link that redirects owners to a dedicated download + page in the frontend where they can access their specific recording. + """ + + owner_accesses = models.RecordingAccess.objects.select_related("user").filter( + role=models.RoleChoices.OWNER, + recording_id=recording.id, + ) + + if not owner_accesses: + logger.error("No owner found for recording %s", recording.id) + return False + + language = get_language() + + context = { + "brandname": settings.EMAIL_BRAND_NAME, + "support_email": settings.EMAIL_SUPPORT_EMAIL, + "logo_img": settings.EMAIL_LOGO_IMG, + "domain": settings.EMAIL_DOMAIN, + "room_name": recording.room.name, + "recording_date": recording.created_at.strftime("%A %d %B %Y"), + "recording_time": recording.created_at.strftime("%H:%M"), + "link": f"{settings.SCREEN_RECORDING_BASE_URL}/{recording.id}", + } + + emails = [access.user.email for access in owner_accesses] + + with override(language): + msg_html = render_to_string("mail/html/screen_recording.html", context) + msg_plain = render_to_string("mail/text/screen_recording.txt", context) + subject = str(_("Your recording is ready")) # Force translation + + try: + send_mail( + subject.capitalize(), + msg_plain, + settings.EMAIL_FROM, + emails, + html_message=msg_html, + fail_silently=False, + ) + except smtplib.SMTPException as exception: + logger.error("notification to %s was not sent: %s", emails, exception) + return False + + return True + @staticmethod def _notify_summary_service(recording): """Notify summary service about a new recording.""" diff --git a/src/backend/core/tests/recording/event/test_notification.py b/src/backend/core/tests/recording/event/test_notification.py new file mode 100644 index 00000000..0fd8dcc7 --- /dev/null +++ b/src/backend/core/tests/recording/event/test_notification.py @@ -0,0 +1,167 @@ +""" +Test event notification. +""" + +# pylint: disable=E1128,W0621,W0613,W0212 + +import smtplib +from unittest import mock + +from django.contrib.sites.models import Site + +import pytest + +from core import factories, models +from core.recording.event.notification import NotificationService, notification_service + +pytestmark = pytest.mark.django_db + + +@pytest.fixture +def mocked_current_site(): + """Mocks the Site.objects.get_current()to return a controlled predefined domain.""" + + site_mock = mock.Mock() + site_mock.domain = "test-domain.com" + + with mock.patch.object( + Site.objects, "get_current", return_value=site_mock + ) as patched: + yield patched + + +@mock.patch.object(NotificationService, "_notify_summary_service", return_value=True) +def test_notify_external_services_transcript_mode(mock_notify_summary): + """Test notification routing for transcript mode recordings.""" + + service = NotificationService() + + recording = factories.RecordingFactory(mode=models.RecordingModeChoices.TRANSCRIPT) + result = service.notify_external_services(recording) + + assert result is True + mock_notify_summary.assert_called_once_with(recording) + + +@mock.patch.object(NotificationService, "_notify_user_by_email", return_value=True) +def test_notify_external_services_screen_recording_mode(mock_notify_email): + """Test notification routing for screen recording mode.""" + + service = NotificationService() + + recording = factories.RecordingFactory( + mode=models.RecordingModeChoices.SCREEN_RECORDING + ) + + result = service.notify_external_services(recording) + + assert result is True + mock_notify_email.assert_called_once_with(recording) + + +def test_notify_external_services_unknown_mode(caplog): + """Test notification for unknown recording mode.""" + recording = factories.RecordingFactory() + + # Bypass validation + recording.mode = "unknown" + + service = NotificationService() + + result = service.notify_external_services(recording) + + assert result is False + assert f"Unknown recording mode unknown for recording {recording.id}" in caplog.text + + +def test_notify_user_by_email_success(mocked_current_site, settings): + """Test successful email notification to recording owners.""" + settings.EMAIL_BRAND_NAME = "ACME" + settings.EMAIL_SUPPORT_EMAIL = "support@acme.com" + settings.EMAIL_LOGO_IMG = "https://acme.com/logo" + settings.SCREEN_RECORDING_BASE_URL = "https://acme.com/recordings" + settings.EMAIL_FROM = "notifications@acme.com" + + recording = factories.RecordingFactory(room__name="Conference Room A") + + owners = [ + factories.UserRecordingAccessFactory( + recording=recording, role=models.RoleChoices.OWNER + ).user, + factories.UserRecordingAccessFactory( + recording=recording, role=models.RoleChoices.OWNER + ).user, + ] + owner_emails = [owner.email for owner in owners] + + # Create non-owner users to verify they don't receive emails + factories.UserRecordingAccessFactory( + recording=recording, role=models.RoleChoices.MEMBER + ) + factories.UserRecordingAccessFactory( + recording=recording, role=models.RoleChoices.ADMIN + ) + + notification_service = NotificationService() + + with mock.patch("core.recording.event.notification.send_mail") as mock_send_mail: + result = notification_service._notify_user_by_email(recording) + + assert result is True + mock_send_mail.assert_called_once() + + subject, body, sender, recipients = mock_send_mail.call_args[0] + + assert subject == "Your recording is ready" + + # Verify email contains expected content + required_content = [ + "ACME", # Brand name + "support@acme.com", # Support email + "https://acme.com/logo", # Logo URL + f"https://acme.com/recordings/{recording.id}", # Recording link + "Conference Room A", # Room name + recording.created_at.strftime("%A %d %B %Y"), # Formatted date + recording.created_at.strftime("%H:%M"), # Formatted time + ] + + for content in required_content: + assert content in body + + assert sender == "notifications@acme.com" + + # Verify all owners received the email (order-independent comparison) + assert sorted(recipients) == sorted(owner_emails) + + +def test_notify_user_by_email_no_owners(mocked_current_site, caplog): + """Test email notification when no owners are found.""" + + # Recording with no access + recording = factories.RecordingFactory() + + result = notification_service._notify_user_by_email(recording) + + assert result is False + assert f"No owner found for recording {recording.id}" in caplog.text + + +def test_notify_user_by_email_smtp_exception(mocked_current_site, caplog): + """Test email notification when an exception occurs.""" + + recording = factories.RecordingFactory(room__name="Conference Room A") + owner = factories.UserRecordingAccessFactory( + recording=recording, role=models.RoleChoices.OWNER + ).user + + notification_service = NotificationService() + + with mock.patch( + "core.recording.event.notification.send_mail", + side_effect=smtplib.SMTPException("SMTP Error"), + ) as mock_send_mail: + result = notification_service._notify_user_by_email(recording) + + assert result is False + mock_send_mail.assert_called_once() + assert f"notification to ['{owner.email}'] was not sent" in caplog.text diff --git a/src/backend/locale/en_US/LC_MESSAGES/django.po b/src/backend/locale/en_US/LC_MESSAGES/django.po index f6741bfa..04c9a96c 100644 --- a/src/backend/locale/en_US/LC_MESSAGES/django.po +++ b/src/backend/locale/en_US/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-04-14 19:00+0000\n" +"POT-Creation-Date: 2025-04-14 19:04+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -309,6 +309,10 @@ msgstr "" msgid "Invalid token" msgstr "" +#: core/recording/event/notification.py:81 +msgid "Your recording is ready" +msgstr "" + #: core/templates/mail/html/invitation.html:160 #: core/templates/mail/text/invitation.txt:3 msgid "La Suite Numérique" @@ -394,6 +398,63 @@ msgstr "" msgid "The La Suite Numérique Team" msgstr "" +#: core/templates/mail/html/screen_recording.html:159 +#: core/templates/mail/text/screen_recording.txt:3 +msgid "Logo email" +msgstr "" + +#: core/templates/mail/html/screen_recording.html:188 +#: core/templates/mail/text/screen_recording.txt:6 +msgid "Your recording is ready!" +msgstr "" + +#: core/templates/mail/html/screen_recording.html:195 +#: core/templates/mail/text/screen_recording.txt:8 +#, python-format +msgid "" +" Your recording of \"%(room_name)s\" on %(recording_date)s at " +"%(recording_time)s is now ready to download. " +msgstr "" + +#: core/templates/mail/html/screen_recording.html:201 +#: core/templates/mail/text/screen_recording.txt:10 +msgid "To keep this recording permanently:" +msgstr "" + +#: core/templates/mail/html/screen_recording.html:203 +#: core/templates/mail/text/screen_recording.txt:12 +msgid "Click the \\" +msgstr "" + +#: core/templates/mail/html/screen_recording.html:204 +#: core/templates/mail/text/screen_recording.txt:13 +msgid "Use the \\" +msgstr "" + +#: core/templates/mail/html/screen_recording.html:205 +#: core/templates/mail/text/screen_recording.txt:14 +msgid "Save the file to your preferred location" +msgstr "" + +#: core/templates/mail/html/screen_recording.html:216 +#: core/templates/mail/text/screen_recording.txt:16 +msgid "Open" +msgstr "" + +#: core/templates/mail/html/screen_recording.html:225 +#: core/templates/mail/text/screen_recording.txt:18 +#, python-format +msgid "" +" If you have any questions or need assistance, please contact our support " +"team at %(support_email)s. " +msgstr "" + +#: core/templates/mail/html/screen_recording.html:240 +#: core/templates/mail/text/screen_recording.txt:22 +#, python-format +msgid " Thank you for using %(brandname)s. " +msgstr "" + #: core/templates/mail/text/invitation.txt:8 msgid "Welcome to Meet" msgstr "" diff --git a/src/backend/locale/fr_FR/LC_MESSAGES/django.po b/src/backend/locale/fr_FR/LC_MESSAGES/django.po index f6741bfa..04c9a96c 100644 --- a/src/backend/locale/fr_FR/LC_MESSAGES/django.po +++ b/src/backend/locale/fr_FR/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-04-14 19:00+0000\n" +"POT-Creation-Date: 2025-04-14 19:04+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -309,6 +309,10 @@ msgstr "" msgid "Invalid token" msgstr "" +#: core/recording/event/notification.py:81 +msgid "Your recording is ready" +msgstr "" + #: core/templates/mail/html/invitation.html:160 #: core/templates/mail/text/invitation.txt:3 msgid "La Suite Numérique" @@ -394,6 +398,63 @@ msgstr "" msgid "The La Suite Numérique Team" msgstr "" +#: core/templates/mail/html/screen_recording.html:159 +#: core/templates/mail/text/screen_recording.txt:3 +msgid "Logo email" +msgstr "" + +#: core/templates/mail/html/screen_recording.html:188 +#: core/templates/mail/text/screen_recording.txt:6 +msgid "Your recording is ready!" +msgstr "" + +#: core/templates/mail/html/screen_recording.html:195 +#: core/templates/mail/text/screen_recording.txt:8 +#, python-format +msgid "" +" Your recording of \"%(room_name)s\" on %(recording_date)s at " +"%(recording_time)s is now ready to download. " +msgstr "" + +#: core/templates/mail/html/screen_recording.html:201 +#: core/templates/mail/text/screen_recording.txt:10 +msgid "To keep this recording permanently:" +msgstr "" + +#: core/templates/mail/html/screen_recording.html:203 +#: core/templates/mail/text/screen_recording.txt:12 +msgid "Click the \\" +msgstr "" + +#: core/templates/mail/html/screen_recording.html:204 +#: core/templates/mail/text/screen_recording.txt:13 +msgid "Use the \\" +msgstr "" + +#: core/templates/mail/html/screen_recording.html:205 +#: core/templates/mail/text/screen_recording.txt:14 +msgid "Save the file to your preferred location" +msgstr "" + +#: core/templates/mail/html/screen_recording.html:216 +#: core/templates/mail/text/screen_recording.txt:16 +msgid "Open" +msgstr "" + +#: core/templates/mail/html/screen_recording.html:225 +#: core/templates/mail/text/screen_recording.txt:18 +#, python-format +msgid "" +" If you have any questions or need assistance, please contact our support " +"team at %(support_email)s. " +msgstr "" + +#: core/templates/mail/html/screen_recording.html:240 +#: core/templates/mail/text/screen_recording.txt:22 +#, python-format +msgid " Thank you for using %(brandname)s. " +msgstr "" + #: core/templates/mail/text/invitation.txt:8 msgid "Welcome to Meet" msgstr "" diff --git a/src/backend/meet/settings.py b/src/backend/meet/settings.py index 57d076c8..dd867a89 100755 --- a/src/backend/meet/settings.py +++ b/src/backend/meet/settings.py @@ -318,6 +318,10 @@ class Base(Configuration): EMAIL_USE_TLS = values.BooleanValue(False) EMAIL_USE_SSL = values.BooleanValue(False) EMAIL_FROM = values.Value("from@example.com") + EMAIL_BRAND_NAME = values.Value(None) + EMAIL_SUPPORT_EMAIL = values.Value(None) + EMAIL_LOGO_IMG = values.Value(None) + EMAIL_DOMAIN = values.Value(None) AUTH_USER_MODEL = "core.User" @@ -481,6 +485,9 @@ class Base(Configuration): SUMMARY_SERVICE_API_TOKEN = values.Value( None, environ_name="SUMMARY_SERVICE_API_TOKEN", environ_prefix=None ) + SCREEN_RECORDING_BASE_URL = values.Value( + None, environ_name="SUMMARY_SERVICE_API_TOKEN", environ_prefix=None + ) # Marketing and communication settings SIGNUP_NEW_USER_TO_MARKETING_EMAIL = values.BooleanValue( diff --git a/src/helm/env.d/dev-keycloak/values.meet.yaml.gotmpl b/src/helm/env.d/dev-keycloak/values.meet.yaml.gotmpl index 72bb8ad4..306e2e20 100644 --- a/src/helm/env.d/dev-keycloak/values.meet.yaml.gotmpl +++ b/src/helm/env.d/dev-keycloak/values.meet.yaml.gotmpl @@ -16,6 +16,10 @@ backend: DJANGO_EMAIL_HOST: "mailcatcher" DJANGO_EMAIL_PORT: 1025 DJANGO_EMAIL_USE_SSL: False + DJANGO_EMAIL_BRAND_NAME: "La Suite Numérique" + DJANGO_EMAIL_SUPPORT_EMAIL: "test@yopmail.com" + DJANGO_EMAIL_LOGO_IMG: https://meet.127.0.0.1.nip.io/assets/logo-suite-numerique.png + DJANGO_EMAIL_DOMAIN: https://meet.127.0.0.1.nip.io OIDC_OP_JWKS_ENDPOINT: https://keycloak.127.0.0.1.nip.io/realms/meet/protocol/openid-connect/certs OIDC_OP_AUTHORIZATION_ENDPOINT: https://keycloak.127.0.0.1.nip.io/realms/meet/protocol/openid-connect/auth OIDC_OP_TOKEN_ENDPOINT: https://keycloak.127.0.0.1.nip.io/realms/meet/protocol/openid-connect/token @@ -61,6 +65,7 @@ backend: RECORDING_STORAGE_EVENT_TOKEN: password SUMMARY_SERVICE_ENDPOINT: http://meet-summary:80/api/v1/tasks/ SUMMARY_SERVICE_API_TOKEN: password + SCREEN_RECORDING_BASE_URL: https://meet.127.0.0.1.nip.io/recordings SSL_CERT_FILE: /usr/local/lib/python3.12/site-packages/certifi/cacert.pem diff --git a/src/helm/env.d/dev/values.meet.yaml.gotmpl b/src/helm/env.d/dev/values.meet.yaml.gotmpl index b674a864..2e4d4ac5 100644 --- a/src/helm/env.d/dev/values.meet.yaml.gotmpl +++ b/src/helm/env.d/dev/values.meet.yaml.gotmpl @@ -32,6 +32,10 @@ backend: DJANGO_EMAIL_HOST: "mailcatcher" DJANGO_EMAIL_PORT: 1025 DJANGO_EMAIL_USE_SSL: False + DJANGO_EMAIL_BRAND_NAME: "La Suite Numérique" + DJANGO_EMAIL_SUPPORT_EMAIL: "test@yopmail.com" + DJANGO_EMAIL_LOGO_IMG: https://meet.127.0.0.1.nip.io/assets/logo-suite-numerique.png + DJANGO_EMAIL_DOMAIN: https://meet.127.0.0.1.nip.io OIDC_OP_JWKS_ENDPOINT: https://fca.integ01.dev-agentconnect.fr/api/v2/jwks OIDC_OP_AUTHORIZATION_ENDPOINT: https://fca.integ01.dev-agentconnect.fr/api/v2/authorize OIDC_OP_TOKEN_ENDPOINT: https://fca.integ01.dev-agentconnect.fr/api/v2/token @@ -83,6 +87,7 @@ backend: RECORDING_STORAGE_EVENT_TOKEN: password SUMMARY_SERVICE_ENDPOINT: http://meet-summary:80/api/v1/tasks/ SUMMARY_SERVICE_API_TOKEN: password + SCREEN_RECORDING_BASE_URL: https://meet.127.0.0.1.nip.io/recordings SIGNUP_NEW_USER_TO_MARKETING_EMAIL: True BREVO_API_KEY: secretKeyRef: diff --git a/src/mail/mjml/screen_recording.mjml b/src/mail/mjml/screen_recording.mjml new file mode 100644 index 00000000..d0a04b2b --- /dev/null +++ b/src/mail/mjml/screen_recording.mjml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + +

{% trans "Your recording is ready!"%}

+
+ + + {% blocktrans %} + Your recording of "{{room_name}}" on {{recording_date}} at {{recording_time}} is now ready to download. + {% endblocktrans %} + + +

{% trans "To keep this recording permanently:" %}

+
    +
  1. {% trans "Click the \"Open\" button below" %}
  2. +
  3. {% trans "Use the \"Download\" button in the interface" %}
  4. +
  5. {% trans "Save the file to your preferred location" %}
  6. +
+
+ + {% trans "Open"%} + + + {% blocktrans %} + If you have any questions or need assistance, please contact our support team at {{support_email}}. + {% endblocktrans %} + + + + +

+ {% blocktrans %} + Thank you for using {{brandname}}. + {% endblocktrans %} +

+
+
+
+
+
+