mirror of
https://github.com/suitenumerique/meet.git
synced 2026-09-09 17:05:56 +00:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fca2c94270 | |||
| e0ab7f191f | |||
| 455b315dbb | |||
| 3089b03062 | |||
| 60febb3b57 | |||
| bf76ab1ddf | |||
| 7565ede0a7 | |||
| 1a15e9f44e | |||
| 7838d8acfe | |||
| 3bb388b937 | |||
| e1cc8105db | |||
| 7844dfcc12 | |||
| ef71003721 | |||
| f74d23c57e | |||
| acedb21045 | |||
| 67e7d382e3 | |||
| 164ac8d948 |
@@ -8,6 +8,19 @@ and this project adheres to
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
|
||||
- 🐛(backend) acknowledge unknown LiveKit webhook events instead of 422
|
||||
- 🔒️(backend) enforce display name setting on rename API
|
||||
|
||||
### Changed
|
||||
|
||||
- 📈(frontend) include LiveKit SIDs in the connection analytics event
|
||||
- 🔇(backend) silence expected 401 warnings on /me
|
||||
- 🔇(backend) silence noisy request summary info logs
|
||||
|
||||
## [1.31.0] - 2026-09-08
|
||||
|
||||
### Added
|
||||
|
||||
- ✨(frontend) add 1080p sending resolution option #1660
|
||||
@@ -15,6 +28,7 @@ and this project adheres to
|
||||
- ✨(backend) update a room's attributes from the external API
|
||||
- 🔊(backend) log request duration in Gunicorn workers
|
||||
- 📈(frontend) track missing lobby participant on accept/reject
|
||||
- ✨(backend) sort waiting participants by their arrival time
|
||||
|
||||
### Changed
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
[project]
|
||||
name = "agents"
|
||||
version = "1.29.0"
|
||||
version = "1.31.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"livekit-agents==1.6.7",
|
||||
|
||||
Generated
+1
-1
@@ -9,7 +9,7 @@ resolution-markers = [
|
||||
|
||||
[[package]]
|
||||
name = "agents"
|
||||
version = "1.29.0"
|
||||
version = "1.31.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "httpx" },
|
||||
|
||||
@@ -909,6 +909,15 @@ class RoomViewSet(
|
||||
"""Rename the current participant in the room."""
|
||||
room = self.get_object()
|
||||
|
||||
if (
|
||||
not settings.AUTHENTICATED_PARTICIPANTS_CAN_EDIT_DISPLAY_NAME
|
||||
and request.user.is_authenticated
|
||||
):
|
||||
return drf_response.Response(
|
||||
{"error": "Authenticated participants cannot edit their display name"},
|
||||
status=drf_status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
|
||||
serializer = serializers.RenameParticipantSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
"""Logging filters for the core application."""
|
||||
|
||||
import logging
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
class SilenceExpected401(logging.Filter):
|
||||
"""Drop the expected 401 from anonymous hits on the /me endpoint.
|
||||
|
||||
The frontend probes `/users/me/` to check authentication; a 401 for
|
||||
anonymous users is normal, not a warning worth logging.
|
||||
"""
|
||||
|
||||
def filter(self, record):
|
||||
"""Return False for a 401 on a silenced path, True otherwise."""
|
||||
if getattr(record, "status_code", None) != 401:
|
||||
return True
|
||||
|
||||
request = getattr(record, "request", None)
|
||||
path = getattr(request, "path", None)
|
||||
if not path:
|
||||
return True
|
||||
|
||||
return path not in settings.LOGGING_SILENCED_401_PATHS
|
||||
@@ -52,12 +52,6 @@ class InvalidPayloadError(LiveKitWebhookError):
|
||||
status_code = 400
|
||||
|
||||
|
||||
class UnsupportedEventTypeError(LiveKitWebhookError):
|
||||
"""Unsupported event type."""
|
||||
|
||||
status_code = 422
|
||||
|
||||
|
||||
class ActionFailedError(LiveKitWebhookError):
|
||||
"""Webhook action fails to process or complete."""
|
||||
|
||||
@@ -74,6 +68,7 @@ class LiveKitWebhookEventType(Enum):
|
||||
# Participant events
|
||||
PARTICIPANT_JOINED = "participant_joined"
|
||||
PARTICIPANT_LEFT = "participant_left"
|
||||
PARTICIPANT_CONNECTION_ABORTED = "participant_connection_aborted"
|
||||
|
||||
# Track events
|
||||
TRACK_PUBLISHED = "track_published"
|
||||
@@ -153,10 +148,13 @@ class LiveKitEventsService:
|
||||
|
||||
try:
|
||||
webhook_type = LiveKitWebhookEventType(data.event)
|
||||
except ValueError as e:
|
||||
raise UnsupportedEventTypeError(
|
||||
f"Unknown webhook type: {data.event}"
|
||||
) from e
|
||||
except ValueError:
|
||||
logger.warning(
|
||||
"Ignoring unknown LiveKit webhook event type '%s' for room '%s'",
|
||||
data.event,
|
||||
room_name,
|
||||
)
|
||||
return
|
||||
|
||||
# Handle according to received webhook type
|
||||
handler = self._webhook_handlers.get(webhook_type.value)
|
||||
|
||||
@@ -9,6 +9,7 @@ from uuid import UUID
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
from django.utils import timezone
|
||||
|
||||
from core import models, utils
|
||||
|
||||
@@ -46,6 +47,7 @@ class LobbyParticipant:
|
||||
username: str
|
||||
color: str
|
||||
id: str
|
||||
entered_at: str
|
||||
|
||||
def to_dict(self) -> Dict[str, str]:
|
||||
"""Serialize the participant object to a dict representation."""
|
||||
@@ -54,6 +56,7 @@ class LobbyParticipant:
|
||||
"username": self.username,
|
||||
"id": self.id,
|
||||
"color": self.color,
|
||||
"entered_at": self.entered_at,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
@@ -68,6 +71,7 @@ class LobbyParticipant:
|
||||
username=data["username"],
|
||||
id=data["id"],
|
||||
color=data["color"],
|
||||
entered_at=data["entered_at"],
|
||||
)
|
||||
except (KeyError, ValueError) as e:
|
||||
logger.exception("Error creating Participant from dict:")
|
||||
@@ -203,6 +207,7 @@ class LobbyService:
|
||||
username=username,
|
||||
id=participant_id,
|
||||
color=utils.generate_color(participant_id),
|
||||
entered_at=timezone.now().isoformat(),
|
||||
)
|
||||
else:
|
||||
participant.status = LobbyParticipantStatus.ACCEPTED
|
||||
@@ -264,6 +269,7 @@ class LobbyService:
|
||||
username=username,
|
||||
id=participant_id,
|
||||
color=color,
|
||||
entered_at=timezone.now().isoformat(),
|
||||
)
|
||||
|
||||
try:
|
||||
@@ -338,6 +344,8 @@ class LobbyService:
|
||||
|
||||
self._index_remove(room_id, *dead_ids)
|
||||
|
||||
waiting_participants.sort(key=lambda p: p["entered_at"], reverse=True)
|
||||
|
||||
return tuple(waiting_participants)
|
||||
|
||||
def handle_participant_entry(
|
||||
|
||||
@@ -9,6 +9,7 @@ from unittest import mock
|
||||
from django.core.cache import cache
|
||||
|
||||
import pytest
|
||||
from freezegun import freeze_time
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from ... import utils
|
||||
@@ -24,6 +25,7 @@ pytestmark = pytest.mark.django_db
|
||||
# Tests for request_entry endpoint
|
||||
|
||||
|
||||
@freeze_time("2025-01-01 10:00:00")
|
||||
def test_request_entry_anonymous(settings):
|
||||
"""Anonymous users should be allowed to request entry to a room."""
|
||||
room = RoomFactory(access_level=RoomAccessLevel.RESTRICTED)
|
||||
@@ -59,6 +61,7 @@ def test_request_entry_anonymous(settings):
|
||||
"username": "test_user",
|
||||
"status": "waiting",
|
||||
"color": "mocked-color",
|
||||
"entered_at": "2025-01-01T10:00:00+00:00",
|
||||
"livekit": None,
|
||||
}
|
||||
|
||||
@@ -71,6 +74,7 @@ def test_request_entry_anonymous(settings):
|
||||
assert participant_data.get("username") == "test_user"
|
||||
|
||||
|
||||
@freeze_time("2025-01-01 10:00:00")
|
||||
def test_request_entry_authenticated_user(settings):
|
||||
"""Authenticated users should be allowed to request entry."""
|
||||
room = RoomFactory(access_level=RoomAccessLevel.RESTRICTED)
|
||||
@@ -108,6 +112,7 @@ def test_request_entry_authenticated_user(settings):
|
||||
"username": "test_user",
|
||||
"status": "waiting",
|
||||
"color": "mocked-color",
|
||||
"entered_at": "2025-01-01T10:00:00+00:00",
|
||||
"livekit": None,
|
||||
}
|
||||
|
||||
@@ -120,6 +125,7 @@ def test_request_entry_authenticated_user(settings):
|
||||
assert participant_data.get("username") == "test_user"
|
||||
|
||||
|
||||
@freeze_time("2025-01-01 10:00:00")
|
||||
def test_request_entry_with_existing_participants(settings):
|
||||
"""Anonymous users should be allowed to request entry to a room with existing participants."""
|
||||
# Create a restricted access room
|
||||
@@ -138,6 +144,7 @@ def test_request_entry_with_existing_participants(settings):
|
||||
"username": "user1",
|
||||
"status": "waiting",
|
||||
"color": "#123456",
|
||||
"entered_at": "2025-01-01T10:00:00+00:00",
|
||||
},
|
||||
)
|
||||
cache.set(
|
||||
@@ -147,6 +154,7 @@ def test_request_entry_with_existing_participants(settings):
|
||||
"username": "user2",
|
||||
"status": "accepted",
|
||||
"color": "#654321",
|
||||
"entered_at": "2025-01-01T10:00:00+00:00",
|
||||
},
|
||||
)
|
||||
|
||||
@@ -178,6 +186,7 @@ def test_request_entry_with_existing_participants(settings):
|
||||
assert response.json() == {
|
||||
"id": participant_id,
|
||||
"username": "test_user",
|
||||
"entered_at": "2025-01-01T10:00:00+00:00",
|
||||
"status": "waiting",
|
||||
"color": "mocked-color",
|
||||
"livekit": None,
|
||||
@@ -192,6 +201,7 @@ def test_request_entry_with_existing_participants(settings):
|
||||
assert participant_data.get("username") == "test_user"
|
||||
|
||||
|
||||
@freeze_time("2025-01-01 10:00:00")
|
||||
def test_request_entry_public_room(settings):
|
||||
"""Entry requests to public rooms should return ACCEPTED status with LiveKit config."""
|
||||
room = RoomFactory(access_level=RoomAccessLevel.PUBLIC)
|
||||
@@ -230,6 +240,7 @@ def test_request_entry_public_room(settings):
|
||||
assert response.json() == {
|
||||
"id": "123",
|
||||
"username": "test_user",
|
||||
"entered_at": "2025-01-01T10:00:00+00:00",
|
||||
"status": "accepted",
|
||||
"color": "mocked-color",
|
||||
"livekit": {"token": "test-token"},
|
||||
@@ -240,6 +251,7 @@ def test_request_entry_public_room(settings):
|
||||
assert not lobby_keys
|
||||
|
||||
|
||||
@freeze_time("2025-01-01 10:00:00")
|
||||
def test_request_entry_authenticated_user_public_room(settings):
|
||||
"""While authenticated, entry request to public rooms should get accepted."""
|
||||
room = RoomFactory(access_level=RoomAccessLevel.PUBLIC)
|
||||
@@ -282,6 +294,7 @@ def test_request_entry_authenticated_user_public_room(settings):
|
||||
assert response.json() == {
|
||||
"id": "2f7f162f-e7d1-421b-90e7-02bfbfbf8def",
|
||||
"username": "test_user",
|
||||
"entered_at": "2025-01-01T10:00:00+00:00",
|
||||
"status": "accepted",
|
||||
"color": "mocked-color",
|
||||
"livekit": {"token": "test-token"},
|
||||
@@ -292,6 +305,7 @@ def test_request_entry_authenticated_user_public_room(settings):
|
||||
assert not lobby_keys
|
||||
|
||||
|
||||
@freeze_time("2025-01-01 10:00:00")
|
||||
def test_request_entry_waiting_participant_public_room(settings):
|
||||
"""While waiting, entry request to public rooms should get accepted."""
|
||||
room = RoomFactory(access_level=RoomAccessLevel.PUBLIC)
|
||||
@@ -308,6 +322,7 @@ def test_request_entry_waiting_participant_public_room(settings):
|
||||
"username": "user1",
|
||||
"status": "waiting",
|
||||
"color": "#123456",
|
||||
"entered_at": "2025-01-01T10:00:00+00:00",
|
||||
},
|
||||
)
|
||||
|
||||
@@ -338,6 +353,7 @@ def test_request_entry_waiting_participant_public_room(settings):
|
||||
"username": "user1",
|
||||
"status": "accepted",
|
||||
"color": "#123456",
|
||||
"entered_at": "2025-01-01T10:00:00+00:00",
|
||||
"livekit": {"token": "test-token"},
|
||||
}
|
||||
|
||||
@@ -443,6 +459,7 @@ def test_allow_participant_to_enter_success(settings, allow_entry, updated_statu
|
||||
"status": "waiting",
|
||||
"username": "foo",
|
||||
"color": "123",
|
||||
"entered_at": "2025-01-01T10:00:00+00:00",
|
||||
},
|
||||
)
|
||||
|
||||
@@ -578,6 +595,7 @@ def test_list_waiting_participants_success(settings):
|
||||
"username": "user1",
|
||||
"status": "waiting",
|
||||
"color": "#123456",
|
||||
"entered_at": "2025-01-01T10:00:00+00:00",
|
||||
},
|
||||
)
|
||||
cache.set(
|
||||
@@ -587,6 +605,7 @@ def test_list_waiting_participants_success(settings):
|
||||
"username": "user2",
|
||||
"status": "waiting",
|
||||
"color": "#654321",
|
||||
"entered_at": "2025-01-01T10:05:00+00:00",
|
||||
},
|
||||
)
|
||||
lobby_service = LobbyService()
|
||||
@@ -597,21 +616,24 @@ def test_list_waiting_participants_success(settings):
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
participants = response.json().get("participants")
|
||||
assert sorted(participants, key=lambda p: p["id"]) == [
|
||||
{
|
||||
"id": "2f7f162f-e7d1-421b-90e7-02bfbfbf8def",
|
||||
"username": "user1",
|
||||
"status": "waiting",
|
||||
"color": "#123456",
|
||||
},
|
||||
{
|
||||
"id": "f4ca3ab8a6c04ad88097b8da33f60f10",
|
||||
"username": "user2",
|
||||
"status": "waiting",
|
||||
"color": "#654321",
|
||||
},
|
||||
]
|
||||
assert response.json() == {
|
||||
"participants": [
|
||||
{
|
||||
"id": "f4ca3ab8a6c04ad88097b8da33f60f10",
|
||||
"username": "user2",
|
||||
"status": "waiting",
|
||||
"color": "#654321",
|
||||
"entered_at": "2025-01-01T10:05:00+00:00",
|
||||
},
|
||||
{
|
||||
"id": "2f7f162f-e7d1-421b-90e7-02bfbfbf8def",
|
||||
"username": "user1",
|
||||
"status": "waiting",
|
||||
"color": "#123456",
|
||||
"entered_at": "2025-01-01T10:00:00+00:00",
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
def test_list_waiting_participants_empty(settings):
|
||||
|
||||
@@ -372,6 +372,67 @@ def test_rename_participant_unexpected_twirp_error(mock_livekit_client, room, to
|
||||
mock_livekit_client.aclose.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name", ["John Doe", "Admin", "Room Owner"])
|
||||
def test_rename_participant_forbidden_when_display_name_edit_disabled(
|
||||
mock_livekit_client, settings, room, token, name
|
||||
):
|
||||
"""
|
||||
Test rename is rejected for authenticated users when the self-hoster
|
||||
disables AUTHENTICATED_PARTICIPANTS_CAN_EDIT_DISPLAY_NAME.
|
||||
"""
|
||||
settings.AUTHENTICATED_PARTICIPANTS_CAN_EDIT_DISPLAY_NAME = False
|
||||
|
||||
client = APIClient()
|
||||
url = reverse("rooms-rename", kwargs={"pk": room.id})
|
||||
response = client.post(
|
||||
url, {"name": name}, format="json", HTTP_AUTHORIZATION=f"Bearer {token}"
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
assert response.data == {
|
||||
"error": "Authenticated participants cannot edit their display name"
|
||||
}
|
||||
mock_livekit_client.room.update_participant.assert_not_called()
|
||||
|
||||
|
||||
def test_rename_participant_allowed_when_display_name_edit_enabled(
|
||||
mock_livekit_client, settings, room, token
|
||||
):
|
||||
"""Test rename still works for authenticated users when the setting is enabled."""
|
||||
settings.AUTHENTICATED_PARTICIPANTS_CAN_EDIT_DISPLAY_NAME = True
|
||||
|
||||
client = APIClient()
|
||||
url = reverse("rooms-rename", kwargs={"pk": room.id})
|
||||
response = client.post(
|
||||
url, {"name": "John Doe"}, format="json", HTTP_AUTHORIZATION=f"Bearer {token}"
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
mock_livekit_client.room.update_participant.assert_called_once()
|
||||
|
||||
|
||||
def test_rename_participant_anonymous_allowed_when_display_name_edit_disabled(
|
||||
mock_livekit_client, settings, room, anonymous_token
|
||||
):
|
||||
"""
|
||||
Test the setting only restricts authenticated users: anonymous participants
|
||||
have no account name to fall back on and can still rename themselves.
|
||||
"""
|
||||
settings.AUTHENTICATED_PARTICIPANTS_CAN_EDIT_DISPLAY_NAME = False
|
||||
|
||||
client = APIClient()
|
||||
url = reverse("rooms-rename", kwargs={"pk": room.id})
|
||||
response = client.post(
|
||||
url,
|
||||
{"name": "Guest User"},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"Bearer {anonymous_token}",
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
mock_livekit_client.room.update_participant.assert_called_once()
|
||||
|
||||
|
||||
def test_rename_participant_success_anonymous(
|
||||
mock_livekit_client, room, anonymous_token
|
||||
):
|
||||
|
||||
@@ -94,7 +94,7 @@ def test_invalid_payload(client, auth_token, mock_livekit_config):
|
||||
|
||||
|
||||
def test_unknown_event_type(client, mock_livekit_config):
|
||||
"""Should return 422 for unknown event type."""
|
||||
"""Should acknowledge (200) an unknown event type rather than reject it."""
|
||||
event_data = json.dumps({"event": "unknown_event_type"})
|
||||
|
||||
# Generate auth token for this specific payload
|
||||
@@ -112,10 +112,8 @@ def test_unknown_event_type(client, mock_livekit_config):
|
||||
HTTP_AUTHORIZATION=auth_token,
|
||||
)
|
||||
|
||||
assert response.status_code == 422
|
||||
assert response.json() == {
|
||||
"status": "error",
|
||||
}
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"status": "success"}
|
||||
|
||||
|
||||
@mock.patch.object(LiveKitEventsService, "_handle_room_finished")
|
||||
|
||||
@@ -16,7 +16,6 @@ from core.services.livekit_events import (
|
||||
AuthenticationError,
|
||||
InvalidPayloadError,
|
||||
LiveKitEventsService,
|
||||
UnsupportedEventTypeError,
|
||||
api,
|
||||
)
|
||||
from core.services.lobby import LobbyService
|
||||
@@ -665,22 +664,27 @@ def test_receive_missing_auth(service):
|
||||
|
||||
|
||||
@mock.patch.object(api.WebhookReceiver, "receive")
|
||||
def test_receive_unsupported_event(mock_receive, service):
|
||||
"""Should raise LiveKitWebhookError for unsupported events."""
|
||||
def test_receive_unknown_event_is_acknowledged(mock_receive, service, caplog):
|
||||
"""Unknown event types are logged and ignored, not rejected.
|
||||
|
||||
LiveKit adds event types over time and does not retry 4xx responses, so
|
||||
raising here would silently drop the event.
|
||||
"""
|
||||
mock_request = mock.MagicMock()
|
||||
mock_request.headers = {"Authorization": "test_token"}
|
||||
mock_request.body = b"{}"
|
||||
|
||||
# Mock returned data with unsupported event type
|
||||
mock_data = mock.MagicMock()
|
||||
mock_data.room.name = str(uuid.uuid4())
|
||||
mock_data.event = "unsupported_event"
|
||||
mock_data.event = "some_future_event"
|
||||
mock_receive.return_value = mock_data
|
||||
|
||||
with pytest.raises(
|
||||
UnsupportedEventTypeError, match="Unknown webhook type: unsupported_event"
|
||||
):
|
||||
service.receive(mock_request)
|
||||
with caplog.at_level("WARNING", logger="core.services.livekit_events"):
|
||||
service.receive(mock_request) # must not raise
|
||||
|
||||
assert "Ignoring unknown LiveKit webhook event type 'some_future_event'" in (
|
||||
caplog.text
|
||||
)
|
||||
|
||||
|
||||
@mock.patch.object(api.WebhookReceiver, "receive")
|
||||
|
||||
@@ -14,6 +14,7 @@ from django.core.cache import cache
|
||||
from django.http import HttpResponse
|
||||
|
||||
import pytest
|
||||
from freezegun import freeze_time
|
||||
|
||||
from core.factories import RoomFactory, UserFactory, UserResourceAccessFactory
|
||||
from core.models import RoleChoices, RoomAccessLevel
|
||||
@@ -55,6 +56,7 @@ def participant_dict():
|
||||
"username": "test-username",
|
||||
"id": "test-participant-id",
|
||||
"color": "#123456",
|
||||
"entered_at": "2025-01-01T10:00:00+00:00",
|
||||
}
|
||||
|
||||
|
||||
@@ -66,6 +68,7 @@ def participant_data():
|
||||
username="test-username",
|
||||
id="test-participant-id",
|
||||
color="#123456",
|
||||
entered_at="2025-01-01T10:00:00+00:00",
|
||||
)
|
||||
|
||||
|
||||
@@ -77,6 +80,7 @@ def test_lobby_participant_to_dict(participant_data):
|
||||
assert result["username"] == "test-username"
|
||||
assert result["id"] == "test-participant-id"
|
||||
assert result["color"] == "#123456"
|
||||
assert result["entered_at"] == "2025-01-01T10:00:00+00:00"
|
||||
|
||||
|
||||
def test_lobby_participant_from_dict_success(participant_dict):
|
||||
@@ -87,6 +91,20 @@ def test_lobby_participant_from_dict_success(participant_dict):
|
||||
assert participant.username == "test-username"
|
||||
assert participant.id == "test-participant-id"
|
||||
assert participant.color == "#123456"
|
||||
assert participant.entered_at == "2025-01-01T10:00:00+00:00"
|
||||
|
||||
|
||||
def test_lobby_participant_from_dict_missing_entered_at():
|
||||
"""`entered_at` is mandatory; data without it is rejected."""
|
||||
data = {
|
||||
"status": "waiting",
|
||||
"username": "test-username",
|
||||
"id": "test-participant-id",
|
||||
"color": "#123456",
|
||||
}
|
||||
|
||||
with pytest.raises(LobbyParticipantParsingError, match="Invalid participant data"):
|
||||
LobbyParticipant.from_dict(data)
|
||||
|
||||
|
||||
def test_lobby_participant_from_dict_default_status():
|
||||
@@ -95,6 +113,7 @@ def test_lobby_participant_from_dict_default_status():
|
||||
"username": "test-username",
|
||||
"id": "test-participant-id",
|
||||
"color": "#123456",
|
||||
"entered_at": "2025-01-01T10:00:00+00:00",
|
||||
}
|
||||
|
||||
participant = LobbyParticipant.from_dict(data_without_status)
|
||||
@@ -120,6 +139,7 @@ def test_lobby_participant_from_dict_invalid_status():
|
||||
"username": "test-username",
|
||||
"id": "test-participant-id",
|
||||
"color": "#123456",
|
||||
"entered_at": "2025-01-01T10:00:00+00:00",
|
||||
}
|
||||
|
||||
with pytest.raises(LobbyParticipantParsingError, match="Invalid participant data"):
|
||||
@@ -264,6 +284,7 @@ def test_request_entry_public_room(
|
||||
username=username,
|
||||
id=participant_id,
|
||||
color="#123456",
|
||||
entered_at="2025-01-01T10:00:00+00:00",
|
||||
)
|
||||
|
||||
lobby_service._get_or_create_participant_id = mock.Mock(return_value=participant_id)
|
||||
@@ -302,6 +323,7 @@ def test_request_entry_trusted_room(
|
||||
username=username,
|
||||
id=participant_id,
|
||||
color="#123456",
|
||||
entered_at="2025-01-01T10:00:00+00:00",
|
||||
)
|
||||
|
||||
lobby_service._get_or_create_participant_id = mock.Mock(return_value=participant_id)
|
||||
@@ -344,6 +366,7 @@ def test_request_entry_new_participant(
|
||||
username=username,
|
||||
id=participant_id,
|
||||
color="#123456",
|
||||
entered_at="2025-01-01T10:00:00+00:00",
|
||||
)
|
||||
mock_enter.return_value = participant_data
|
||||
|
||||
@@ -371,6 +394,7 @@ def test_request_entry_waiting_participant(
|
||||
username=username,
|
||||
id=participant_id,
|
||||
color="#123456",
|
||||
entered_at="2025-01-01T10:00:00+00:00",
|
||||
)
|
||||
lobby_service._get_or_create_participant_id = mock.Mock(return_value=participant_id)
|
||||
lobby_service._get_participant = mock.Mock(return_value=mocked_participant)
|
||||
@@ -399,6 +423,7 @@ def test_request_entry_accepted_participant(
|
||||
username=username,
|
||||
id=participant_id,
|
||||
color="#123456",
|
||||
entered_at="2025-01-01T10:00:00+00:00",
|
||||
)
|
||||
lobby_service._get_or_create_participant_id = mock.Mock(return_value=participant_id)
|
||||
lobby_service._get_participant = mock.Mock(return_value=mocked_participant)
|
||||
@@ -439,6 +464,7 @@ def test_request_entry_participant_with_role(
|
||||
username=username,
|
||||
id=participant_id,
|
||||
color="#123456",
|
||||
entered_at="2025-01-01T10:00:00+00:00",
|
||||
)
|
||||
lobby_service._get_or_create_participant_id = mock.Mock(return_value=participant_id)
|
||||
lobby_service._get_participant = mock.Mock(return_value=mocked_participant)
|
||||
@@ -479,6 +505,7 @@ def test_refresh_waiting_status(mock_cache, lobby_service, participant_id):
|
||||
@mock.patch("core.utils.generate_color")
|
||||
@mock.patch("core.utils.notify_participants")
|
||||
@mock.patch("core.services.lobby.LobbyService._index_add")
|
||||
@freeze_time("2025-01-01 10:00:00")
|
||||
def test_enter_success(
|
||||
mock_index_add,
|
||||
mock_notify,
|
||||
@@ -500,6 +527,7 @@ def test_enter_success(
|
||||
assert participant.username == username
|
||||
assert participant.id == participant_id
|
||||
assert participant.color == "#123456"
|
||||
assert participant.entered_at == "2025-01-01T10:00:00+00:00"
|
||||
|
||||
lobby_service._get_cache_key.assert_called_once_with(room.id, participant_id)
|
||||
|
||||
@@ -629,6 +657,7 @@ def test_list_waiting_participants_multiple(mock_cache, lobby_service):
|
||||
"username": "user1",
|
||||
"id": "participant1",
|
||||
"color": "#123456",
|
||||
"entered_at": "2025-01-01T10:00:00+00:00",
|
||||
}
|
||||
|
||||
participant2 = {
|
||||
@@ -636,6 +665,7 @@ def test_list_waiting_participants_multiple(mock_cache, lobby_service):
|
||||
"username": "user2",
|
||||
"id": "participant2",
|
||||
"color": "#654321",
|
||||
"entered_at": "2025-01-01T10:05:00+00:00",
|
||||
}
|
||||
|
||||
lobby_service._index_members = mock.Mock(
|
||||
@@ -651,9 +681,10 @@ def test_list_waiting_participants_multiple(mock_cache, lobby_service):
|
||||
|
||||
assert len(result) == 2
|
||||
|
||||
# Verify both participants are in the result
|
||||
assert any(p["id"] == "participant1" and p["username"] == "user1" for p in result)
|
||||
assert any(p["id"] == "participant2" and p["username"] == "user2" for p in result)
|
||||
# Most recent entry comes first
|
||||
assert [p["id"] for p in result] == ["participant2", "participant1"]
|
||||
assert result[0]["username"] == "user2"
|
||||
assert result[1]["username"] == "user1"
|
||||
|
||||
# Verify all participants have waiting status
|
||||
assert all(p["status"] == "waiting" for p in result)
|
||||
@@ -689,6 +720,7 @@ def test_list_waiting_participants_partially_corrupted(mock_cache, lobby_service
|
||||
"username": "user2",
|
||||
"id": "participant2",
|
||||
"color": "#654321",
|
||||
"entered_at": "2025-01-01T10:00:00+00:00",
|
||||
}
|
||||
|
||||
corrupted_participant = {"invalid": "data"}
|
||||
@@ -729,12 +761,14 @@ def test_list_waiting_participants_non_waiting(mock_cache, lobby_service):
|
||||
"username": "user1",
|
||||
"id": "participant1",
|
||||
"color": "#123456",
|
||||
"entered_at": "2025-01-01T10:00:00+00:00",
|
||||
}
|
||||
participant2 = {
|
||||
"status": "accepted",
|
||||
"username": "user2",
|
||||
"id": "participant2",
|
||||
"color": "#654321",
|
||||
"entered_at": "2025-01-01T10:00:00+00:00",
|
||||
}
|
||||
|
||||
lobby_service._index_members = mock.Mock(
|
||||
@@ -832,6 +866,7 @@ def test_update_participant_status_success(mock_cache, lobby_service, participan
|
||||
"username": "test-username",
|
||||
"id": participant_id,
|
||||
"color": "#123456",
|
||||
"entered_at": "2025-01-01T10:00:00+00:00",
|
||||
}
|
||||
|
||||
mock_cache.get.return_value = participant_dict
|
||||
@@ -850,6 +885,7 @@ def test_update_participant_status_success(mock_cache, lobby_service, participan
|
||||
"username": "test-username",
|
||||
"id": participant_id,
|
||||
"color": "#123456",
|
||||
"entered_at": "2025-01-01T10:00:00+00:00",
|
||||
}
|
||||
mock_cache.set.assert_called_once_with(
|
||||
"mocked_cache_key", expected_data, timeout=60
|
||||
@@ -875,6 +911,7 @@ def test_clear_room_cache(settings, lobby_service):
|
||||
username="participant1",
|
||||
id="participant1",
|
||||
color="#123456",
|
||||
entered_at="2025-01-01T10:00:00+00:00",
|
||||
),
|
||||
timeout=settings.LOBBY_WAITING_TIMEOUT,
|
||||
)
|
||||
@@ -885,6 +922,7 @@ def test_clear_room_cache(settings, lobby_service):
|
||||
username="participant2",
|
||||
id="participant2",
|
||||
color="#123456",
|
||||
entered_at="2025-01-01T10:00:00+00:00",
|
||||
),
|
||||
timeout=settings.LOBBY_ACCEPTED_TIMEOUT,
|
||||
)
|
||||
@@ -895,6 +933,7 @@ def test_clear_room_cache(settings, lobby_service):
|
||||
username="participant3",
|
||||
id="participant3",
|
||||
color="#123456",
|
||||
entered_at="2025-01-01T10:00:00+00:00",
|
||||
),
|
||||
timeout=settings.LOBBY_DENIED_TIMEOUT,
|
||||
)
|
||||
@@ -930,6 +969,7 @@ def test_clear_participant_cache(lobby_service):
|
||||
"username": "test-username",
|
||||
"id": participant_id,
|
||||
"color": "#123456",
|
||||
"entered_at": "2025-01-01T10:00:00+00:00",
|
||||
}
|
||||
cache.set(cache_key, participant_data, timeout=settings.LOBBY_WAITING_TIMEOUT)
|
||||
lobby_service._index_add(room_id, participant_id)
|
||||
@@ -1000,6 +1040,7 @@ def test_list_waiting_participants_prunes_stale_index_ids(settings, lobby_servic
|
||||
"username": "user1",
|
||||
"status": "waiting",
|
||||
"color": "#123456",
|
||||
"entered_at": "2025-01-01T10:00:00+00:00",
|
||||
},
|
||||
timeout=100,
|
||||
)
|
||||
|
||||
@@ -1110,6 +1110,12 @@ class Base(Configuration):
|
||||
environ_prefix=None,
|
||||
)
|
||||
|
||||
LOGGING_SILENCED_401_PATHS = values.ListValue(
|
||||
default=["/api/v1.0/users/me/"],
|
||||
environ_name="LOGGING_SILENCED_401_PATHS",
|
||||
environ_prefix=None,
|
||||
)
|
||||
|
||||
# Logging
|
||||
# We want to make it easy to log to console but by default we log production
|
||||
# to Sentry and don't want to log to console.
|
||||
@@ -1122,10 +1128,16 @@ class Base(Configuration):
|
||||
"style": "{",
|
||||
},
|
||||
},
|
||||
"filters": {
|
||||
"silence_expected_401": {
|
||||
"()": "core.logging_filters.SilenceExpected401",
|
||||
},
|
||||
},
|
||||
"handlers": {
|
||||
"console": {
|
||||
"class": "logging.StreamHandler",
|
||||
"formatter": "simple",
|
||||
"filters": ["silence_expected_401"],
|
||||
},
|
||||
},
|
||||
# Override root logger to send it to console
|
||||
@@ -1136,6 +1148,13 @@ class Base(Configuration):
|
||||
),
|
||||
},
|
||||
"loggers": {
|
||||
"request.summary": {
|
||||
"level": values.Value(
|
||||
"WARNING",
|
||||
environ_name="LOGGING_LEVEL_REQUEST_SUMMARY",
|
||||
environ_prefix="",
|
||||
)
|
||||
},
|
||||
"core": {
|
||||
"handlers": ["console"],
|
||||
"level": values.Value(
|
||||
|
||||
@@ -7,7 +7,7 @@ build-backend = "uv_build"
|
||||
|
||||
[project]
|
||||
name = "meet"
|
||||
version = "1.30.0"
|
||||
version = "1.31.0"
|
||||
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
|
||||
Generated
+1
-1
@@ -1187,7 +1187,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "meet"
|
||||
version = "1.30.0"
|
||||
version = "1.31.0"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "aiohttp" },
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "meet",
|
||||
"version": "1.30.0",
|
||||
"version": "1.31.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "meet",
|
||||
"version": "1.30.0",
|
||||
"version": "1.31.0",
|
||||
"dependencies": {
|
||||
"@fontsource-variable/atkinson-hyperlegible-next": "5.3.0",
|
||||
"@fontsource-variable/lexend": "5.3.0",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "meet",
|
||||
"private": true,
|
||||
"version": "1.30.0",
|
||||
"version": "1.31.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "panda codegen && vite",
|
||||
|
||||
@@ -8,6 +8,7 @@ export type WaitingParticipant = {
|
||||
status: string
|
||||
username: string
|
||||
color: string
|
||||
entered_at: string
|
||||
}
|
||||
|
||||
export type WaitingParticipantsResponse = {
|
||||
|
||||
@@ -9,6 +9,14 @@ import {
|
||||
} from '../../participants/api/listWaitingParticipants'
|
||||
import { reportError } from '@/features/analytics/telemetry'
|
||||
|
||||
const toTimestamp = (participant: WaitingParticipant): number =>
|
||||
Date.parse(participant.entered_at)
|
||||
|
||||
export const sortWaitingParticipants = (
|
||||
participants: WaitingParticipant[]
|
||||
): WaitingParticipant[] =>
|
||||
[...participants].sort((a, b) => toTimestamp(a) - toTimestamp(b))
|
||||
|
||||
export const useWaitingParticipants = () => {
|
||||
const roomData = useRoomData()
|
||||
const roomId = roomData?.id || '' // FIXME - bad practice
|
||||
@@ -22,7 +30,10 @@ export const useWaitingParticipants = () => {
|
||||
})
|
||||
|
||||
const waitingParticipants = useMemo(
|
||||
() => (canManageLobby ? waitingData?.participants || [] : []),
|
||||
() =>
|
||||
canManageLobby
|
||||
? sortWaitingParticipants(waitingData?.participants || [])
|
||||
: [],
|
||||
[waitingData, canManageLobby]
|
||||
)
|
||||
|
||||
|
||||
@@ -79,7 +79,7 @@ export const LobbyProvider = () => {
|
||||
// 3. Rights regained.
|
||||
const prevCanManageLobby = usePrevious(canManageLobby)
|
||||
useEffect(() => {
|
||||
if (!prevCanManageLobby && canManageLobby && isConnected) {
|
||||
if (prevCanManageLobby != canManageLobby && isConnected) {
|
||||
fetchIfManager()
|
||||
}
|
||||
}, [
|
||||
|
||||
@@ -16,7 +16,7 @@ const Card = styled('div', {
|
||||
borderRadius: '0.25rem',
|
||||
boxShadow: '',
|
||||
width: '100%',
|
||||
maxWidth: '380px',
|
||||
maxWidth: '410px',
|
||||
minHeight: '196px',
|
||||
},
|
||||
})
|
||||
@@ -229,7 +229,7 @@ const ConfirmationMessage = ({ onNext }: { onNext: () => void }) => {
|
||||
return (
|
||||
<Card
|
||||
style={{
|
||||
maxWidth: '380px',
|
||||
maxWidth: '410px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
|
||||
@@ -71,20 +71,30 @@ export const ConnectionObserver = () => {
|
||||
useEffect(() => {
|
||||
if (!isAnalyticsEnabled) return
|
||||
|
||||
const handleConnection = () => {
|
||||
const handleConnection = async () => {
|
||||
// Preserve original connection timestamp across reconnections to measure
|
||||
// total session duration from first connect to final disconnect.
|
||||
if (connectionStartTimeRef.current != null) return
|
||||
connectionStartTimeRef.current = Date.now()
|
||||
void captureMediaEvent('connection-event', {})
|
||||
const participantSid = room.localParticipant.sid
|
||||
const roomSid = await room.getSid().catch(() => undefined)
|
||||
void captureMediaEvent('connection-event', {
|
||||
livekit_room_sid: roomSid,
|
||||
livekit_participant_sid: participantSid,
|
||||
})
|
||||
}
|
||||
|
||||
const handleReconnect = () => {
|
||||
captureEvent('reconnect-event')
|
||||
}
|
||||
|
||||
const handleReconnected = () => {
|
||||
captureEvent('reconnected-event')
|
||||
const handleReconnected = async () => {
|
||||
const participantSid = room.localParticipant.sid
|
||||
const roomSid = await room.getSid().catch(() => undefined)
|
||||
captureEvent('reconnected-event', {
|
||||
livekit_room_sid: roomSid,
|
||||
livekit_participant_sid: participantSid,
|
||||
})
|
||||
}
|
||||
|
||||
const handleSignalingConnect = () => {
|
||||
|
||||
@@ -24,7 +24,8 @@ const Heading = styled('h1', {
|
||||
})
|
||||
|
||||
const buttonClass = css({
|
||||
width: { base: '100%', xsm: 'auto' },
|
||||
width: '100%',
|
||||
flex: 1,
|
||||
})
|
||||
|
||||
enum DisconnectReasonKey {
|
||||
@@ -64,7 +65,7 @@ const FeedbackRoute = () => {
|
||||
<Heading>{t(`feedback.heading.${reasonKey || 'normal'}`)}</Heading>
|
||||
<Stack
|
||||
direction={{ base: 'column', xsm: 'row' }}
|
||||
width={{ base: '100%', xsm: 'auto' }}
|
||||
width="100%"
|
||||
maxWidth="410px"
|
||||
>
|
||||
{showBackButton && (
|
||||
|
||||
@@ -480,7 +480,7 @@
|
||||
"destination": "Ein neues Dokument wird erstellt auf",
|
||||
"destinationUnknown": "Ein neues Dokument wird erstellt",
|
||||
"language": "Meeting-Sprache:",
|
||||
"recording": "Auch eine Aufzeichnung starten"
|
||||
"recording": "Auch eine Videoaufzeichnung starten"
|
||||
},
|
||||
"button": {
|
||||
"start": "Meeting-Transkription starten",
|
||||
|
||||
@@ -480,7 +480,7 @@
|
||||
"destination": "A new document will be created on",
|
||||
"destinationUnknown": "A new document will be created",
|
||||
"language": "Meeting language:",
|
||||
"recording": "Also start a recording"
|
||||
"recording": "Also start a video recording"
|
||||
},
|
||||
"button": {
|
||||
"start": "Start transcribing the meeting",
|
||||
|
||||
@@ -479,7 +479,7 @@
|
||||
"destination": "Se creará un nuevo documento en",
|
||||
"destinationUnknown": "Se creará un nuevo documento",
|
||||
"language": "Idioma de la reunión:",
|
||||
"recording": "Iniciar también una grabación"
|
||||
"recording": "Iniciar también una grabación de vídeo"
|
||||
},
|
||||
"button": {
|
||||
"start": "Empezar a transcribir la reunión",
|
||||
|
||||
@@ -480,7 +480,7 @@
|
||||
"destination": "Un nouveau document sera créé sur",
|
||||
"destinationUnknown": "Un nouveau document sera créé",
|
||||
"language": "Langue de la réunion :",
|
||||
"recording": "Démarrer aussi un enregistrement"
|
||||
"recording": "Démarrer aussi un enregistrement vidéo"
|
||||
},
|
||||
"button": {
|
||||
"start": "Commencer à transcrire la réunion",
|
||||
|
||||
@@ -480,7 +480,7 @@
|
||||
"destination": "Er wordt een nieuw document aangemaakt op",
|
||||
"destinationUnknown": "Een nieuw document wordt aangemaakt",
|
||||
"language": "Vergadertalen:",
|
||||
"recording": "Start ook een opname"
|
||||
"recording": "Start ook een video-opname"
|
||||
},
|
||||
"button": {
|
||||
"start": "Begin met het transcriberen van de vergadering",
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "mail_mjml",
|
||||
"version": "1.30.0",
|
||||
"version": "1.31.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "mail_mjml",
|
||||
"version": "1.30.0",
|
||||
"version": "1.31.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@html-to/text-cli": "0.6.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mail_mjml",
|
||||
"version": "1.30.0",
|
||||
"version": "1.31.0",
|
||||
"description": "An util to generate html and text django's templates from mjml templates",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "sdk",
|
||||
"version": "1.30.0",
|
||||
"version": "1.31.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "sdk",
|
||||
"version": "1.30.0",
|
||||
"version": "1.31.0",
|
||||
"license": "ISC",
|
||||
"workspaces": [
|
||||
"./library",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "sdk",
|
||||
"version": "1.30.0",
|
||||
"version": "1.31.0",
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
[project]
|
||||
name = "summary"
|
||||
version = "1.30.0"
|
||||
version = "1.31.0"
|
||||
requires-python = ">=3.13"
|
||||
dependencies = [
|
||||
"fastapi[standard]>=0.105.0",
|
||||
|
||||
Generated
+1
-1
@@ -1507,7 +1507,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "summary"
|
||||
version = "1.30.0"
|
||||
version = "1.31.0"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "celery" },
|
||||
|
||||
Reference in New Issue
Block a user