mirror of
https://github.com/suitenumerique/meet.git
synced 2026-08-17 14:07:49 +00:00
385da86759
Ensure the participant requesting a mute action is still present in the room before processing the request. This mitigates scenarios where a previously issued token could be reused after the meeting has ended. Current token lifetime is intentionally long-lived and will be refactored in the future to better align with LiveKit session constraints. In the meantime, add this extra validation step to reduce the attack surface.
199 lines
6.1 KiB
Python
199 lines
6.1 KiB
Python
"""Participants management service for LiveKit rooms."""
|
|
|
|
# pylint: disable=too-many-arguments,no-name-in-module,too-many-positional-arguments
|
|
# ruff: noqa:PLR0913
|
|
|
|
import json
|
|
import uuid
|
|
from logging import getLogger
|
|
from typing import Dict, Optional
|
|
|
|
from asgiref.sync import async_to_sync
|
|
from livekit.api import (
|
|
MuteRoomTrackRequest,
|
|
RoomParticipantIdentity,
|
|
TwirpError,
|
|
UpdateParticipantRequest,
|
|
)
|
|
from livekit.protocol.models import ParticipantInfo
|
|
|
|
from core import utils
|
|
|
|
from .lobby import LobbyService
|
|
|
|
logger = getLogger(__name__)
|
|
|
|
|
|
class ParticipantsManagementException(Exception):
|
|
"""Exception raised when a participant management operations fail."""
|
|
|
|
|
|
class ParticipantNotFoundException(ParticipantsManagementException):
|
|
"""Raised when the target participant does not exist in the room."""
|
|
|
|
|
|
class ParticipantsManagement:
|
|
"""Service for managing participants."""
|
|
|
|
@async_to_sync
|
|
async def mute(self, room_name: str, identity: str, track_sid: str):
|
|
"""Mute a specific audio or video track for a participant in a room."""
|
|
|
|
lkapi = utils.create_livekit_client()
|
|
|
|
try:
|
|
await lkapi.room.mute_published_track(
|
|
MuteRoomTrackRequest(
|
|
room=room_name,
|
|
identity=identity,
|
|
track_sid=track_sid,
|
|
muted=True,
|
|
)
|
|
)
|
|
|
|
except TwirpError as e:
|
|
if e.code == "not_found":
|
|
logger.warning(
|
|
"Participant %s not found in room %s, skipping muting",
|
|
identity,
|
|
room_name,
|
|
)
|
|
raise ParticipantNotFoundException("Participant does not exist") from e
|
|
|
|
logger.exception(
|
|
"Unexpected error muting participant %s for room %s",
|
|
identity,
|
|
room_name,
|
|
)
|
|
raise ParticipantsManagementException("Could not mute participant") from e
|
|
|
|
finally:
|
|
await lkapi.aclose()
|
|
|
|
@async_to_sync
|
|
async def remove(self, room_name: str, identity: str):
|
|
"""Remove a participant from a room and clear their lobby cache."""
|
|
|
|
try:
|
|
LobbyService().clear_participant_cache(
|
|
room_id=uuid.UUID(room_name), participant_id=identity
|
|
)
|
|
except (ValueError, TypeError) as exc:
|
|
logger.warning(
|
|
"participants_management.remove: room_name '%s' is not a UUID; "
|
|
"skipping lobby cache clear",
|
|
room_name,
|
|
exc_info=exc,
|
|
)
|
|
|
|
lkapi = utils.create_livekit_client()
|
|
|
|
try:
|
|
await lkapi.room.remove_participant(
|
|
RoomParticipantIdentity(room=room_name, identity=identity)
|
|
)
|
|
except TwirpError as e:
|
|
if e.code == "not_found":
|
|
logger.warning(
|
|
"Participant %s not found in room %s, skipping removing",
|
|
identity,
|
|
room_name,
|
|
)
|
|
raise ParticipantNotFoundException("Participant does not exist") from e
|
|
|
|
logger.exception(
|
|
"Unexpected error removing participant %s for room %s",
|
|
identity,
|
|
room_name,
|
|
)
|
|
raise ParticipantsManagementException("Could not remove participant") from e
|
|
|
|
finally:
|
|
await lkapi.aclose()
|
|
|
|
@async_to_sync
|
|
async def update(
|
|
self,
|
|
room_name: str,
|
|
identity: str,
|
|
metadata: Optional[Dict] = None,
|
|
attributes: Optional[Dict] = None,
|
|
permission: Optional[Dict] = None,
|
|
name: Optional[str] = None,
|
|
):
|
|
"""Update participant properties such as metadata, attributes, permissions, or name."""
|
|
|
|
lkapi = utils.create_livekit_client()
|
|
|
|
try:
|
|
await lkapi.room.update_participant(
|
|
UpdateParticipantRequest(
|
|
room=room_name,
|
|
identity=identity,
|
|
metadata=json.dumps(metadata),
|
|
permission=permission,
|
|
attributes=attributes,
|
|
name=name,
|
|
)
|
|
)
|
|
|
|
except TwirpError as e:
|
|
if e.code == "not_found":
|
|
logger.warning(
|
|
"Participant %s not found in room %s, skipping update",
|
|
identity,
|
|
room_name,
|
|
)
|
|
raise ParticipantNotFoundException("Participant does not exist") from e
|
|
|
|
logger.exception(
|
|
"Unexpected error updating participant %s for room %s",
|
|
identity,
|
|
room_name,
|
|
)
|
|
raise ParticipantsManagementException("Could not update participant") from e
|
|
|
|
finally:
|
|
await lkapi.aclose()
|
|
|
|
@async_to_sync
|
|
async def check_if_in_meeting(self, room_name: str, identity: str) -> bool:
|
|
"""Check whether `identity` is currently a participant in `room_name`.
|
|
|
|
Raises ParticipantsManagementException for unexpected LiveKit errors
|
|
so callers can fail closed rather than silently allowing the action.
|
|
"""
|
|
|
|
if not room_name or not identity:
|
|
return False
|
|
|
|
lkapi = utils.create_livekit_client()
|
|
|
|
try:
|
|
participant = await lkapi.room.get_participant(
|
|
RoomParticipantIdentity(
|
|
room=room_name,
|
|
identity=identity,
|
|
)
|
|
)
|
|
except TwirpError as e:
|
|
if e.code == "not_found":
|
|
raise ParticipantNotFoundException("Participant does not exist") from e
|
|
|
|
logger.exception(
|
|
"Unexpected error checking participant %s in room %s",
|
|
identity,
|
|
room_name,
|
|
)
|
|
raise ParticipantsManagementException(
|
|
"Could not verify participant presence"
|
|
) from e
|
|
|
|
finally:
|
|
await lkapi.aclose()
|
|
|
|
return (
|
|
participant is not None
|
|
and participant.state != ParticipantInfo.State.DISCONNECTED
|
|
)
|