mirror of
https://github.com/suitenumerique/meet.git
synced 2026-09-04 14:45:40 +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.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from django_pydantic_field.rest_framework import SchemaField
|
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 pydantic import ValidationError as PydanticValidationError
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from rest_framework.exceptions import PermissionDenied
|
from rest_framework.exceptions import PermissionDenied
|
||||||
@@ -317,9 +317,7 @@ class MuteParticipantSerializer(BaseParticipantsManagementSerializer):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
RoomConfigurationTrackSource = Literal[
|
TrackSource = Literal["camera", "microphone", "screen_share", "screen_share_audio"]
|
||||||
"camera", "microphone", "screen_share", "screen_share_audio"
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class RoomConfiguration(BaseModel):
|
class RoomConfiguration(BaseModel):
|
||||||
@@ -328,15 +326,12 @@ class RoomConfiguration(BaseModel):
|
|||||||
Unknown fields are rejected.
|
Unknown fields are rejected.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
can_publish_sources: list[RoomConfigurationTrackSource] | None = None
|
can_publish_sources: list[TrackSource] | None = None
|
||||||
everyone_can_mute: bool | None = None
|
everyone_can_mute: bool | None = None
|
||||||
|
|
||||||
model_config = {"extra": "forbid"}
|
model_config = {"extra": "forbid"}
|
||||||
|
|
||||||
|
|
||||||
TrackSource = Literal["SCREEN_SHARE", "SCREEN_SHARE_AUDIO", "CAMERA", "MICROPHONE"]
|
|
||||||
|
|
||||||
|
|
||||||
class ParticipantPermission(BaseModel):
|
class ParticipantPermission(BaseModel):
|
||||||
"""Mirror the LiveKit ParticipantPermission protobuf.
|
"""Mirror the LiveKit ParticipantPermission protobuf.
|
||||||
|
|
||||||
@@ -356,6 +351,10 @@ class ParticipantPermission(BaseModel):
|
|||||||
|
|
||||||
model_config = {"extra": "forbid"}
|
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):
|
class UpdateParticipantSerializer(BaseParticipantsManagementSerializer):
|
||||||
"""Validate participant update data."""
|
"""Validate participant update data."""
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
Test rooms API endpoints in the Meet core app: participants management.
|
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
|
import random
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
@@ -13,7 +13,7 @@ from django.core.exceptions import SuspiciousOperation
|
|||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from livekit.api import TwirpError
|
from livekit.api import TwirpError, UpdateParticipantRequest
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.test import APIClient
|
from rest_framework.test import APIClient
|
||||||
|
|
||||||
@@ -385,8 +385,8 @@ def test_update_participant_success(mock_livekit_client):
|
|||||||
"can_publish": True,
|
"can_publish": True,
|
||||||
"can_publish_data": True,
|
"can_publish_data": True,
|
||||||
"can_publish_sources": [
|
"can_publish_sources": [
|
||||||
"CAMERA",
|
"camera",
|
||||||
"MICROPHONE",
|
"microphone",
|
||||||
],
|
],
|
||||||
"can_update_metadata": True,
|
"can_update_metadata": True,
|
||||||
"can_subscribe_metrics": True,
|
"can_subscribe_metrics": True,
|
||||||
@@ -413,8 +413,8 @@ def test_update_participant_success(mock_livekit_client):
|
|||||||
{"can_publish_data": True},
|
{"can_publish_data": True},
|
||||||
{
|
{
|
||||||
"can_publish_sources": [
|
"can_publish_sources": [
|
||||||
"CAMERA",
|
"camera",
|
||||||
"MICROPHONE",
|
"microphone",
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{"can_update_metadata": True},
|
{"can_update_metadata": True},
|
||||||
@@ -445,9 +445,41 @@ def test_update_participant_permission_fields_are_optional(
|
|||||||
assert response.data == {"status": "success"}
|
assert response.data == {"status": "success"}
|
||||||
|
|
||||||
mock_livekit_client.room.update_participant.assert_called_once()
|
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()
|
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(
|
@pytest.mark.parametrize(
|
||||||
"value,permission_key",
|
"value,permission_key",
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import { fetchApi } from './fetchApi'
|
|||||||
import { keys } from './queryKeys'
|
import { keys } from './queryKeys'
|
||||||
import { useQuery } from '@tanstack/react-query'
|
import { useQuery } from '@tanstack/react-query'
|
||||||
import { RecordingMode } from '@/features/recording'
|
import { RecordingMode } from '@/features/recording'
|
||||||
|
import { Track } from 'livekit-client'
|
||||||
|
import Source = Track.Source
|
||||||
|
|
||||||
export interface ApiConfig {
|
export interface ApiConfig {
|
||||||
analytics?: {
|
analytics?: {
|
||||||
@@ -50,7 +52,7 @@ export interface ApiConfig {
|
|||||||
url: string
|
url: string
|
||||||
force_wss_protocol: boolean
|
force_wss_protocol: boolean
|
||||||
enable_firefox_proxy_workaround: boolean
|
enable_firefox_proxy_workaround: boolean
|
||||||
default_sources: string[]
|
default_sources: Source[]
|
||||||
}
|
}
|
||||||
transcription_destination?: string
|
transcription_destination?: string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
import { Track } from 'livekit-client'
|
||||||
|
import Source = Track.Source
|
||||||
|
|
||||||
export type ApiLiveKit = {
|
export type ApiLiveKit = {
|
||||||
url: string
|
url: string
|
||||||
room: string
|
room: string
|
||||||
@@ -10,6 +13,11 @@ export enum ApiAccessLevel {
|
|||||||
RESTRICTED = 'restricted',
|
RESTRICTED = 'restricted',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type RoomConfiguration = {
|
||||||
|
can_publish_sources?: Source[] | null
|
||||||
|
everyone_can_mute?: boolean | null
|
||||||
|
}
|
||||||
|
|
||||||
export type ApiRoom = {
|
export type ApiRoom = {
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
@@ -18,7 +26,5 @@ export type ApiRoom = {
|
|||||||
is_administrable: boolean
|
is_administrable: boolean
|
||||||
access_level: ApiAccessLevel
|
access_level: ApiAccessLevel
|
||||||
livekit?: ApiLiveKit
|
livekit?: ApiLiveKit
|
||||||
configuration?: {
|
configuration?: RoomConfiguration
|
||||||
[key: string]: string | number | boolean | string[]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export const useParticipantPermissions = () => {
|
|||||||
|
|
||||||
const updateParticipantPermissions = async (
|
const updateParticipantPermissions = async (
|
||||||
participant: Participant,
|
participant: Participant,
|
||||||
sources: Array<Source>
|
sources: Source[]
|
||||||
) => {
|
) => {
|
||||||
if (!data?.id) {
|
if (!data?.id) {
|
||||||
throw new Error('Room id is not available')
|
throw new Error('Room id is not available')
|
||||||
@@ -20,7 +20,7 @@ export const useParticipantPermissions = () => {
|
|||||||
can_update_metadata: participant.permissions?.canUpdateMetadata,
|
can_update_metadata: participant.permissions?.canUpdateMetadata,
|
||||||
can_subscribe_metrics: participant.permissions?.canSubscribeMetrics,
|
can_subscribe_metrics: participant.permissions?.canSubscribeMetrics,
|
||||||
can_publish: sources.length > 0,
|
can_publish: sources.length > 0,
|
||||||
can_publish_sources: sources.map((source) => source.toUpperCase()),
|
can_publish_sources: sources,
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -39,10 +39,6 @@ export const usePublishSourcesManager = () => {
|
|||||||
|
|
||||||
const { notifyParticipants } = useNotifyParticipants()
|
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.
|
// The name can be misleading—use the slug instead to ensure the correct React Query key is updated.
|
||||||
const roomId = data?.slug
|
const roomId = data?.slug
|
||||||
|
|
||||||
@@ -54,16 +50,16 @@ export const usePublishSourcesManager = () => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const currentSources = useMemo(() => {
|
const currentSources = useMemo(() => {
|
||||||
|
const defaultSources = configData?.livekit?.default_sources ?? []
|
||||||
|
|
||||||
if (
|
if (
|
||||||
configuration?.can_publish_sources == undefined ||
|
configuration?.can_publish_sources == undefined ||
|
||||||
!Array.isArray(configuration?.can_publish_sources)
|
!Array.isArray(configuration?.can_publish_sources)
|
||||||
) {
|
) {
|
||||||
return defaultSources
|
return defaultSources
|
||||||
}
|
}
|
||||||
return configuration.can_publish_sources.map((source) => {
|
return configuration.can_publish_sources
|
||||||
return source as Source
|
}, [configData, configuration?.can_publish_sources])
|
||||||
})
|
|
||||||
}, [defaultSources, configuration?.can_publish_sources])
|
|
||||||
|
|
||||||
const updateSource = useCallback(
|
const updateSource = useCallback(
|
||||||
async (sources: Source[], enabled: boolean) => {
|
async (sources: Source[], enabled: boolean) => {
|
||||||
@@ -78,7 +74,7 @@ export const usePublishSourcesManager = () => {
|
|||||||
|
|
||||||
const newConfiguration = {
|
const newConfiguration = {
|
||||||
...configuration,
|
...configuration,
|
||||||
can_publish_sources: newSources as string[],
|
can_publish_sources: newSources,
|
||||||
}
|
}
|
||||||
|
|
||||||
const room = await patchRoom({
|
const room = await patchRoom({
|
||||||
|
|||||||
Reference in New Issue
Block a user