"""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( # noqa: PLR0917 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 )