import { MeetTokenMetadata, OpenViduMeetPermissions, ParticipantOptions, ParticipantPermissions, ParticipantRole } from '@typings-ce'; import { inject, injectable } from 'inversify'; import { ParticipantInfo } from 'livekit-server-sdk'; import { errorParticipantAlreadyExists, errorParticipantNotFound } from '../models/error.model.js'; import { FrontendEventService, LiveKitService, LoggerService, RoomService, TokenService } from './index.js'; import { MeetRoomHelper } from '../helpers/room.helper.js'; @injectable() export class ParticipantService { constructor( @inject(LoggerService) protected logger: LoggerService, @inject(RoomService) protected roomService: RoomService, @inject(LiveKitService) protected livekitService: LiveKitService, @inject(FrontendEventService) protected frontendEventService: FrontendEventService, @inject(TokenService) protected tokenService: TokenService ) {} async generateOrRefreshParticipantToken( participantOptions: ParticipantOptions, currentRoles: { role: ParticipantRole; permissions: OpenViduMeetPermissions }[], refresh = false ): Promise { const { roomId, participantName, secret } = participantOptions; if (participantName) { // Check if participant with same participantName exists in the room const participantExists = await this.participantExists(roomId, participantName); if (!refresh && participantExists) { this.logger.verbose(`Participant '${participantName}' already exists in room '${roomId}'`); throw errorParticipantAlreadyExists(participantName, roomId); } if (refresh && !participantExists) { this.logger.verbose(`Participant '${participantName}' does not exist in room '${roomId}'`); throw errorParticipantNotFound(participantName, roomId); } } const role = await this.roomService.getRoomRoleBySecret(roomId, secret); const token = await this.generateParticipantToken(participantOptions, role, currentRoles); this.logger.verbose(`Participant token generated for room '${roomId}'`); return token; } protected async generateParticipantToken( participantOptions: ParticipantOptions, role: ParticipantRole, currentRoles: { role: ParticipantRole; permissions: OpenViduMeetPermissions }[] ): Promise { const { roomId, participantName } = participantOptions; const permissions = this.getParticipantPermissions(roomId, role, !!participantName); if (!currentRoles.some((r) => r.role === role)) { currentRoles.push({ role, permissions: permissions.openvidu }); } return this.tokenService.generateParticipantToken(participantOptions, permissions.livekit, currentRoles, role); } async getParticipant(roomId: string, participantName: string): Promise { this.logger.verbose(`Fetching participant '${participantName}'`); return this.livekitService.getParticipant(roomId, participantName); } async participantExists(roomId: string, participantName: string): Promise { this.logger.verbose(`Checking if participant '${participantName}' exists in room '${roomId}'`); try { const participant = await this.getParticipant(roomId, participantName); return participant !== null; } catch (error) { return false; } } async deleteParticipant(roomId: string, participantName: string): Promise { this.logger.verbose(`Deleting participant '${participantName}' from room '${roomId}'`); return this.livekitService.deleteParticipant(participantName, roomId); } getParticipantPermissions(roomId: string, role: ParticipantRole, addJoinPermission = true): ParticipantPermissions { switch (role) { case ParticipantRole.MODERATOR: return this.generateModeratorPermissions(roomId, addJoinPermission); case ParticipantRole.SPEAKER: return this.generateSpeakerPermissions(roomId, addJoinPermission); default: throw new Error(`Role ${role} not supported`); } } async updateParticipantRole(roomId: string, participantName: string, newRole: ParticipantRole): Promise { try { const meetRoom = await this.roomService.getMeetRoom(roomId); const participant = await this.getParticipant(roomId, participantName); const metadata: MeetTokenMetadata = this.parseMetadata(participant!.metadata); if (!metadata || typeof metadata !== 'object') { throw new Error(`Invalid metadata for participant ${participantName}`); } // TODO: Should we update the roles array as well? metadata.selectedRole = newRole; await this.livekitService.updateParticipantMetadata(roomId, participantName, JSON.stringify(metadata)); const { speakerSecret, moderatorSecret } = MeetRoomHelper.extractSecretsFromRoom(meetRoom); const secret = newRole === ParticipantRole.MODERATOR ? moderatorSecret : speakerSecret; await this.frontendEventService.sendParticipantRoleUpdatedSignal(roomId, participantName, newRole, secret); } catch (error) { this.logger.error('Error changing participant role:', error); throw error; } } protected parseMetadata(metadata: string): MeetTokenMetadata { try { return JSON.parse(metadata); } catch (error) { this.logger.error('Failed to parse participant metadata:', error); throw new Error('Invalid participant metadata format'); } } protected generateModeratorPermissions(roomId: string, addJoinPermission = true): ParticipantPermissions { return { livekit: { roomJoin: addJoinPermission, room: roomId, canPublish: true, canSubscribe: true, canPublishData: true, canUpdateOwnMetadata: true }, openvidu: { canRecord: true, canChat: true, canChangeVirtualBackground: true } }; } protected generateSpeakerPermissions(roomId: string, addJoinPermission = true): ParticipantPermissions { return { livekit: { roomJoin: addJoinPermission, room: roomId, canPublish: true, canSubscribe: true, canPublishData: true, canUpdateOwnMetadata: true }, openvidu: { canRecord: false, canChat: true, canChangeVirtualBackground: true } }; } }