mirror of
https://github.com/suitenumerique/meet.git
synced 2026-07-26 11:58:53 +00:00
♻️(fullstack) simplify source serialization
Simplify source serialization and validation logic while improving type safety around room configuration handling. Introduce a dedicated TypeScript type matching the backend Pydantic model more precisely. Also harmonize track source casing between frontend and backend to remove redundant conversion logic and resolve #1282.
This commit is contained in:
committed by
aleb_the_flash
parent
5a7a0da923
commit
5bac1668fe
@@ -13,7 +13,7 @@ from django.core.exceptions import SuspiciousOperation
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from django_pydantic_field.rest_framework import SchemaField
|
||||
from pydantic import BaseModel, Field
|
||||
from pydantic import BaseModel, Field, field_serializer
|
||||
from pydantic import ValidationError as PydanticValidationError
|
||||
from rest_framework import serializers
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
@@ -317,9 +317,7 @@ class MuteParticipantSerializer(BaseParticipantsManagementSerializer):
|
||||
)
|
||||
|
||||
|
||||
RoomConfigurationTrackSource = Literal[
|
||||
"camera", "microphone", "screen_share", "screen_share_audio"
|
||||
]
|
||||
TrackSource = Literal["camera", "microphone", "screen_share", "screen_share_audio"]
|
||||
|
||||
|
||||
class RoomConfiguration(BaseModel):
|
||||
@@ -328,15 +326,12 @@ class RoomConfiguration(BaseModel):
|
||||
Unknown fields are rejected.
|
||||
"""
|
||||
|
||||
can_publish_sources: list[RoomConfigurationTrackSource] | None = None
|
||||
can_publish_sources: list[TrackSource] | None = None
|
||||
everyone_can_mute: bool | None = None
|
||||
|
||||
model_config = {"extra": "forbid"}
|
||||
|
||||
|
||||
TrackSource = Literal["SCREEN_SHARE", "SCREEN_SHARE_AUDIO", "CAMERA", "MICROPHONE"]
|
||||
|
||||
|
||||
class ParticipantPermission(BaseModel):
|
||||
"""Mirror the LiveKit ParticipantPermission protobuf.
|
||||
|
||||
@@ -356,6 +351,10 @@ class ParticipantPermission(BaseModel):
|
||||
|
||||
model_config = {"extra": "forbid"}
|
||||
|
||||
@field_serializer("can_publish_sources")
|
||||
def _serialize_sources(self, sources: list[str]) -> list[str]:
|
||||
return [s.upper() for s in sources]
|
||||
|
||||
|
||||
class UpdateParticipantSerializer(BaseParticipantsManagementSerializer):
|
||||
"""Validate participant update data."""
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
Test rooms API endpoints in the Meet core app: participants management.
|
||||
"""
|
||||
|
||||
# pylint: disable=redefined-outer-name,unused-argument,protected-access
|
||||
# pylint: disable=redefined-outer-name,unused-argument,protected-access,no-name-in-module
|
||||
|
||||
import random
|
||||
from unittest import mock
|
||||
@@ -13,7 +13,7 @@ from django.core.exceptions import SuspiciousOperation
|
||||
from django.urls import reverse
|
||||
|
||||
import pytest
|
||||
from livekit.api import TwirpError
|
||||
from livekit.api import TwirpError, UpdateParticipantRequest
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
@@ -385,8 +385,8 @@ def test_update_participant_success(mock_livekit_client):
|
||||
"can_publish": True,
|
||||
"can_publish_data": True,
|
||||
"can_publish_sources": [
|
||||
"CAMERA",
|
||||
"MICROPHONE",
|
||||
"camera",
|
||||
"microphone",
|
||||
],
|
||||
"can_update_metadata": True,
|
||||
"can_subscribe_metrics": True,
|
||||
@@ -413,8 +413,8 @@ def test_update_participant_success(mock_livekit_client):
|
||||
{"can_publish_data": True},
|
||||
{
|
||||
"can_publish_sources": [
|
||||
"CAMERA",
|
||||
"MICROPHONE",
|
||||
"camera",
|
||||
"microphone",
|
||||
]
|
||||
},
|
||||
{"can_update_metadata": True},
|
||||
@@ -445,9 +445,41 @@ def test_update_participant_permission_fields_are_optional(
|
||||
assert response.data == {"status": "success"}
|
||||
|
||||
mock_livekit_client.room.update_participant.assert_called_once()
|
||||
|
||||
(request_arg,), _ = mock_livekit_client.room.update_participant.call_args
|
||||
assert isinstance(request_arg, UpdateParticipantRequest)
|
||||
|
||||
mock_livekit_client.aclose.assert_called_once()
|
||||
|
||||
|
||||
def test_update_participant_permission_fields_invalid_case(mock_livekit_client):
|
||||
"""Should raise bad request when can_publish_sources is uppercase."""
|
||||
client = APIClient()
|
||||
room = RoomFactory()
|
||||
user = UserFactory()
|
||||
UserResourceAccessFactory(
|
||||
resource=room, user=user, role=random.choice(["administrator", "owner"])
|
||||
)
|
||||
client.force_authenticate(user=user)
|
||||
|
||||
payload = {
|
||||
"participant_identity": str(uuid4()),
|
||||
"permission": {
|
||||
"can_publish_sources": [
|
||||
"CAMERA",
|
||||
"microphone",
|
||||
]
|
||||
},
|
||||
}
|
||||
|
||||
url = reverse("rooms-update-participant", kwargs={"pk": room.id})
|
||||
response = client.post(url, payload, format="json")
|
||||
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
mock_livekit_client.room.update_participant.assert_not_called()
|
||||
mock_livekit_client.aclose.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"value,permission_key",
|
||||
[
|
||||
|
||||
@@ -2,6 +2,8 @@ import { fetchApi } from './fetchApi'
|
||||
import { keys } from './queryKeys'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { RecordingMode } from '@/features/recording'
|
||||
import { Track } from 'livekit-client'
|
||||
import Source = Track.Source
|
||||
|
||||
export interface ApiConfig {
|
||||
analytics?: {
|
||||
@@ -50,7 +52,7 @@ export interface ApiConfig {
|
||||
url: string
|
||||
force_wss_protocol: boolean
|
||||
enable_firefox_proxy_workaround: boolean
|
||||
default_sources: string[]
|
||||
default_sources: Source[]
|
||||
}
|
||||
transcription_destination?: string
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import { Track } from 'livekit-client'
|
||||
import Source = Track.Source
|
||||
|
||||
export type ApiLiveKit = {
|
||||
url: string
|
||||
room: string
|
||||
@@ -10,6 +13,11 @@ export enum ApiAccessLevel {
|
||||
RESTRICTED = 'restricted',
|
||||
}
|
||||
|
||||
export type RoomConfiguration = {
|
||||
can_publish_sources?: Source[] | null
|
||||
everyone_can_mute?: boolean | null
|
||||
}
|
||||
|
||||
export type ApiRoom = {
|
||||
id: string
|
||||
name: string
|
||||
@@ -18,7 +26,5 @@ export type ApiRoom = {
|
||||
is_administrable: boolean
|
||||
access_level: ApiAccessLevel
|
||||
livekit?: ApiLiveKit
|
||||
configuration?: {
|
||||
[key: string]: string | number | boolean | string[]
|
||||
}
|
||||
configuration?: RoomConfiguration
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ export const useParticipantPermissions = () => {
|
||||
|
||||
const updateParticipantPermissions = async (
|
||||
participant: Participant,
|
||||
sources: Array<Source>
|
||||
sources: Source[]
|
||||
) => {
|
||||
if (!data?.id) {
|
||||
throw new Error('Room id is not available')
|
||||
@@ -20,7 +20,7 @@ export const useParticipantPermissions = () => {
|
||||
can_update_metadata: participant.permissions?.canUpdateMetadata,
|
||||
can_subscribe_metrics: participant.permissions?.canSubscribeMetrics,
|
||||
can_publish: sources.length > 0,
|
||||
can_publish_sources: sources.map((source) => source.toUpperCase()),
|
||||
can_publish_sources: sources,
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
@@ -39,10 +39,6 @@ export const usePublishSourcesManager = () => {
|
||||
|
||||
const { notifyParticipants } = useNotifyParticipants()
|
||||
|
||||
const defaultSources = configData?.livekit?.default_sources?.map((source) => {
|
||||
return source as Source
|
||||
})
|
||||
|
||||
// The name can be misleading—use the slug instead to ensure the correct React Query key is updated.
|
||||
const roomId = data?.slug
|
||||
|
||||
@@ -54,16 +50,16 @@ export const usePublishSourcesManager = () => {
|
||||
)
|
||||
|
||||
const currentSources = useMemo(() => {
|
||||
const defaultSources = configData?.livekit?.default_sources ?? []
|
||||
|
||||
if (
|
||||
configuration?.can_publish_sources == undefined ||
|
||||
!Array.isArray(configuration?.can_publish_sources)
|
||||
) {
|
||||
return defaultSources
|
||||
}
|
||||
return configuration.can_publish_sources.map((source) => {
|
||||
return source as Source
|
||||
})
|
||||
}, [defaultSources, configuration?.can_publish_sources])
|
||||
return configuration.can_publish_sources
|
||||
}, [configData, configuration?.can_publish_sources])
|
||||
|
||||
const updateSource = useCallback(
|
||||
async (sources: Source[], enabled: boolean) => {
|
||||
@@ -78,7 +74,7 @@ export const usePublishSourcesManager = () => {
|
||||
|
||||
const newConfiguration = {
|
||||
...configuration,
|
||||
can_publish_sources: newSources as string[],
|
||||
can_publish_sources: newSources,
|
||||
}
|
||||
|
||||
const room = await patchRoom({
|
||||
|
||||
Reference in New Issue
Block a user