refactor: replace participantName with participantIdentity for consistency

This commit is contained in:
juancarmore 2025-08-13 17:20:16 +02:00
parent 1071c4c97e
commit 972f6f4f90
5 changed files with 113 additions and 86 deletions

View File

@ -31,7 +31,7 @@ export const generateParticipantToken = async (req: Request, res: Response) => {
try { try {
const claims = tokenService.getClaimsIgnoringExpiration(previousToken); const claims = tokenService.getClaimsIgnoringExpiration(previousToken);
const metadata = JSON.parse(claims.metadata || '{}'); const metadata = participantService.parseMetadata(claims.metadata || '{}');
currentRoles = metadata.roles; currentRoles = metadata.roles;
} catch (error) { } catch (error) {
logger.verbose('Error extracting roles from previous token:', error); logger.verbose('Error extracting roles from previous token:', error);
@ -80,11 +80,12 @@ export const refreshParticipantToken = async (req: Request, res: Response) => {
} }
// Extract roles from the previous token // Extract roles from the previous token
const participantService = container.get(ParticipantService);
let currentRoles: { role: ParticipantRole; permissions: OpenViduMeetPermissions }[] = []; let currentRoles: { role: ParticipantRole; permissions: OpenViduMeetPermissions }[] = [];
try { try {
const claims = tokenService.getClaimsIgnoringExpiration(previousToken); const claims = tokenService.getClaimsIgnoringExpiration(previousToken);
const metadata = JSON.parse(claims.metadata || '{}'); const metadata = participantService.parseMetadata(claims.metadata || '{}');
currentRoles = metadata.roles; currentRoles = metadata.roles;
} catch (err) { } catch (err) {
logger.verbose('Error extracting roles from previous token:', err); logger.verbose('Error extracting roles from previous token:', err);
@ -94,7 +95,6 @@ export const refreshParticipantToken = async (req: Request, res: Response) => {
const participantOptions: ParticipantOptions = req.body; const participantOptions: ParticipantOptions = req.body;
const { roomId } = participantOptions; const { roomId } = participantOptions;
const participantService = container.get(ParticipantService);
try { try {
logger.verbose(`Refreshing participant token for room '${roomId}'`); logger.verbose(`Refreshing participant token for room '${roomId}'`);
@ -111,11 +111,26 @@ export const refreshParticipantToken = async (req: Request, res: Response) => {
} }
}; };
export const updateParticipant = async (req: Request, res: Response) => {
const logger = container.get(LoggerService);
const participantService = container.get(ParticipantService);
const { roomId, participantIdentity } = req.params;
const { role } = req.body;
try {
logger.verbose(`Changing role of participant '${participantIdentity}' in room '${roomId}' to '${role}'`);
await participantService.updateParticipantRole(roomId, participantIdentity, role);
res.status(200).json({ message: `Participant '${participantIdentity}' role updated to '${role}'` });
} catch (error) {
handleError(res, error, `changing role for participant '${participantIdentity}' in room '${roomId}'`);
}
};
export const deleteParticipant = async (req: Request, res: Response) => { export const deleteParticipant = async (req: Request, res: Response) => {
const logger = container.get(LoggerService); const logger = container.get(LoggerService);
const roomService = container.get(RoomService); const roomService = container.get(RoomService);
const participantService = container.get(ParticipantService); const participantService = container.get(ParticipantService);
const { roomId, participantName } = req.params; const { roomId, participantIdentity } = req.params;
// Check if the room exists // Check if the room exists
try { try {
@ -125,25 +140,10 @@ export const deleteParticipant = async (req: Request, res: Response) => {
} }
try { try {
logger.verbose(`Deleting participant '${participantName}' from room '${roomId}'`); logger.verbose(`Deleting participant '${participantIdentity}' from room '${roomId}'`);
await participantService.deleteParticipant(roomId, participantName); await participantService.deleteParticipant(roomId, participantIdentity);
res.status(200).json({ message: 'Participant deleted' }); res.status(200).json({ message: 'Participant deleted' });
} catch (error) { } catch (error) {
handleError(res, error, `deleting participant '${participantName}' from room '${roomId}'`); handleError(res, error, `deleting participant '${participantIdentity}' from room '${roomId}'`);
}
};
export const updateParticipant = async (req: Request, res: Response) => {
const logger = container.get(LoggerService);
const participantService = container.get(ParticipantService);
const { roomId, participantName } = req.params;
const { role } = req.body;
try {
logger.verbose(`Changing role of participant '${participantName}' in room '${roomId}' to '${role}'`);
await participantService.updateParticipantRole(roomId, participantName, role);
res.status(200).json({ message: `Participant '${participantName}' role updated to ${role}` });
} catch (error) {
handleError(res, error, `changing role for participant '${participantName}' in room '${roomId}'`);
} }
}; };

View File

@ -2,7 +2,13 @@ import bodyParser from 'body-parser';
import { Router } from 'express'; import { Router } from 'express';
import * as meetingCtrl from '../controllers/meeting.controller.js'; import * as meetingCtrl from '../controllers/meeting.controller.js';
import * as participantCtrl from '../controllers/participant.controller.js'; import * as participantCtrl from '../controllers/participant.controller.js';
import { participantTokenValidator, withAuth, withModeratorPermissions, withValidParticipantRole, withValidRoomId } from '../middlewares/index.js'; import {
participantTokenValidator,
validateUpdateParticipantRequest,
withAuth,
withModeratorPermissions,
withValidRoomId
} from '../middlewares/index.js';
export const internalMeetingRouter = Router(); export const internalMeetingRouter = Router();
internalMeetingRouter.use(bodyParser.urlencoded({ extended: true })); internalMeetingRouter.use(bodyParser.urlencoded({ extended: true }));
@ -12,23 +18,22 @@ internalMeetingRouter.use(bodyParser.json());
internalMeetingRouter.delete( internalMeetingRouter.delete(
'/:roomId', '/:roomId',
withAuth(participantTokenValidator), withAuth(participantTokenValidator),
withModeratorPermissions,
withValidRoomId, withValidRoomId,
withModeratorPermissions,
meetingCtrl.endMeeting meetingCtrl.endMeeting
); );
internalMeetingRouter.delete(
'/:roomId/participants/:participantName',
withAuth(participantTokenValidator),
withModeratorPermissions,
withValidRoomId,
participantCtrl.deleteParticipant
);
internalMeetingRouter.patch( internalMeetingRouter.patch(
'/:roomId/participants/:participantName', '/:roomId/participants/:participantIdentity',
withAuth(participantTokenValidator), withAuth(participantTokenValidator),
withModeratorPermissions,
withValidRoomId, withValidRoomId,
withValidParticipantRole, withModeratorPermissions,
validateUpdateParticipantRequest,
participantCtrl.updateParticipant participantCtrl.updateParticipant
); );
internalMeetingRouter.delete(
'/:roomId/participants/:participantIdentity',
withAuth(participantTokenValidator),
withValidRoomId,
withModeratorPermissions,
participantCtrl.deleteParticipant
);

View File

@ -23,8 +23,8 @@ import {
internalError, internalError,
OpenViduMeetError OpenViduMeetError
} from '../models/error.model.js'; } from '../models/error.model.js';
import { LoggerService } from './index.js';
import { chunkArray } from '../utils/array.utils.js'; import { chunkArray } from '../utils/array.utils.js';
import { LoggerService } from './index.js';
@injectable() @injectable()
export class LiveKitService { export class LiveKitService {
@ -77,7 +77,7 @@ export class LiveKitService {
*/ */
async roomHasParticipants(roomName: string): Promise<boolean> { async roomHasParticipants(roomName: string): Promise<boolean> {
try { try {
const participants = await this.roomClient.listParticipants(roomName); const participants = await this.listRoomParticipants(roomName);
return participants.length > 0; return participants.length > 0;
} catch (error) { } catch (error) {
return false; return false;
@ -167,20 +167,35 @@ export class LiveKitService {
} }
} }
/**
* Lists all participants in a LiveKit room.
*
* @param roomName - The name of the room to list participants from
* @returns A promise that resolves to an array of participant information
*/
async listRoomParticipants(roomName: string): Promise<ParticipantInfo[]> {
try {
return await this.roomClient.listParticipants(roomName);
} catch (error) {
this.logger.error(`Error listing participants for room '${roomName}': ${error}`);
throw internalError(`listing participants for room '${roomName}'`);
}
}
/** /**
* Retrieves information about a specific participant in a LiveKit room. * Retrieves information about a specific participant in a LiveKit room.
* *
* @param roomName - The name of the room where the participant is located * @param roomName - The name of the room where the participant is located
* @param participantName - The name of the participant to retrieve * @param participantIdentity - The identity of the participant to retrieve
* @returns A Promise that resolves to the participant's information * @returns A Promise that resolves to the participant's information
* @throws An internal error if the participant cannot be found or another error occurs * @throws An internal error if the participant cannot be found or another error occurs
*/ */
async getParticipant(roomName: string, participantName: string): Promise<ParticipantInfo> { async getParticipant(roomName: string, participantIdentity: string): Promise<ParticipantInfo> {
try { try {
return await this.roomClient.getParticipant(roomName, participantName); return await this.roomClient.getParticipant(roomName, participantIdentity);
} catch (error) { } catch (error) {
this.logger.warn(`Participant ${participantName} not found in room ${roomName}: ${error}`); this.logger.warn(`Participant ${participantIdentity} not found in room ${roomName}: ${error}`);
throw errorParticipantNotFound(participantName, roomName); throw errorParticipantNotFound(participantIdentity, roomName);
} }
} }
@ -188,31 +203,31 @@ export class LiveKitService {
* Updates the metadata of a participant in a LiveKit room. * Updates the metadata of a participant in a LiveKit room.
* *
* @param roomName - The name of the room where the participant is located * @param roomName - The name of the room where the participant is located
* @param participantName - The name of the participant whose metadata will be updated * @param participantIdentity - The identity of the participant whose metadata will be updated
* @param metadata - The new metadata to set for the participant * @param metadata - The new metadata to set for the participant
* @returns A Promise that resolves when the metadata has been successfully updated * @returns A Promise that resolves when the metadata has been successfully updated
* @throws An internal error if there is an issue updating the metadata * @throws An internal error if there is an issue updating the metadata
*/ */
async updateParticipantMetadata(roomName: string, participantName: string, metadata: string): Promise<void> { async updateParticipantMetadata(roomName: string, participantIdentity: string, metadata: string): Promise<void> {
try { try {
await this.roomClient.updateParticipant(roomName, participantName, metadata); await this.roomClient.updateParticipant(roomName, participantIdentity, metadata);
this.logger.verbose(`Updated metadata for participant ${participantName} in room ${roomName}`); this.logger.verbose(`Updated metadata for participant '${participantIdentity}' in room '${roomName}'`);
} catch (error) { } catch (error) {
this.logger.error( this.logger.error(
`Error updating metadata for participant ${participantName} in room ${roomName}: ${error}` `Error updating metadata for participant '${participantIdentity}' in room '${roomName}': ${error}`
); );
throw internalError(`updating metadata for participant '${participantName}' in room '${roomName}'`); throw internalError(`updating metadata for participant '${participantIdentity}' in room '${roomName}'`);
} }
} }
async deleteParticipant(participantName: string, roomName: string): Promise<void> { async deleteParticipant(roomName: string, participantIdentity: string): Promise<void> {
const participantExists = await this.participantExists(roomName, participantName); const participantExists = await this.participantExists(roomName, participantIdentity);
if (!participantExists) { if (!participantExists) {
throw errorParticipantNotFound(participantName, roomName); throw errorParticipantNotFound(participantIdentity, roomName);
} }
await this.roomClient.removeParticipant(roomName, participantName); await this.roomClient.removeParticipant(roomName, participantIdentity);
} }
async sendData(roomName: string, rawData: Record<string, any>, options: SendDataOptions): Promise<void> { async sendData(roomName: string, rawData: Record<string, any>, options: SendDataOptions): Promise<void> {
@ -369,10 +384,10 @@ export class LiveKitService {
return participant.identity.startsWith('EG_') && participant.permission?.recorder === true; return participant.identity.startsWith('EG_') && participant.permission?.recorder === true;
} }
private async participantExists(roomName: string, participantName: string): Promise<boolean> { private async participantExists(roomName: string, participantIdentity: string): Promise<boolean> {
try { try {
const participants: ParticipantInfo[] = await this.roomClient.listParticipants(roomName); const participants: ParticipantInfo[] = await this.listRoomParticipants(roomName);
return participants.some((participant) => participant.identity === participantName); return participants.some((participant) => participant.identity === participantIdentity);
} catch (error: any) { } catch (error: any) {
this.logger.error(error); this.logger.error(error);

View File

@ -7,9 +7,10 @@ import {
} from '@typings-ce'; } from '@typings-ce';
import { inject, injectable } from 'inversify'; import { inject, injectable } from 'inversify';
import { ParticipantInfo } from 'livekit-server-sdk'; import { ParticipantInfo } from 'livekit-server-sdk';
import { MeetRoomHelper } from '../helpers/room.helper.js';
import { validateMeetTokenMetadata } from '../middlewares/index.js';
import { errorParticipantAlreadyExists, errorParticipantNotFound } from '../models/error.model.js'; import { errorParticipantAlreadyExists, errorParticipantNotFound } from '../models/error.model.js';
import { FrontendEventService, LiveKitService, LoggerService, RoomService, TokenService } from './index.js'; import { FrontendEventService, LiveKitService, LoggerService, RoomService, TokenService } from './index.js';
import { MeetRoomHelper } from '../helpers/room.helper.js';
@injectable() @injectable()
export class ParticipantService { export class ParticipantService {
@ -64,26 +65,26 @@ export class ParticipantService {
return this.tokenService.generateParticipantToken(participantOptions, permissions.livekit, currentRoles, role); return this.tokenService.generateParticipantToken(participantOptions, permissions.livekit, currentRoles, role);
} }
async getParticipant(roomId: string, participantName: string): Promise<ParticipantInfo | null> { async getParticipant(roomId: string, participantIdentity: string): Promise<ParticipantInfo> {
this.logger.verbose(`Fetching participant '${participantName}'`); this.logger.verbose(`Fetching participant '${participantIdentity}'`);
return this.livekitService.getParticipant(roomId, participantName); return this.livekitService.getParticipant(roomId, participantIdentity);
} }
async participantExists(roomId: string, participantName: string): Promise<boolean> { async participantExists(roomId: string, participantIdentity: string): Promise<boolean> {
this.logger.verbose(`Checking if participant '${participantName}' exists in room '${roomId}'`); this.logger.verbose(`Checking if participant '${participantIdentity}' exists in room '${roomId}'`);
try { try {
const participant = await this.getParticipant(roomId, participantName); await this.getParticipant(roomId, participantIdentity);
return participant !== null; return true;
} catch (error) { } catch (error) {
return false; return false;
} }
} }
async deleteParticipant(roomId: string, participantName: string): Promise<void> { async deleteParticipant(roomId: string, participantIdentity: string): Promise<void> {
this.logger.verbose(`Deleting participant '${participantName}' from room '${roomId}'`); this.logger.verbose(`Deleting participant '${participantIdentity}' from room '${roomId}'`);
return this.livekitService.deleteParticipant(participantName, roomId); return this.livekitService.deleteParticipant(roomId, participantIdentity);
} }
getParticipantPermissions(roomId: string, role: ParticipantRole, addJoinPermission = true): ParticipantPermissions { getParticipantPermissions(roomId: string, role: ParticipantRole, addJoinPermission = true): ParticipantPermissions {
@ -97,36 +98,42 @@ export class ParticipantService {
} }
} }
async updateParticipantRole(roomId: string, participantName: string, newRole: ParticipantRole): Promise<void> { async updateParticipantRole(roomId: string, participantIdentity: string, newRole: ParticipantRole): Promise<void> {
try { try {
const meetRoom = await this.roomService.getMeetRoom(roomId); const meetRoom = await this.roomService.getMeetRoom(roomId);
const participant = await this.getParticipant(roomId, participantName); const participant = await this.getParticipant(roomId, participantIdentity);
const metadata: MeetTokenMetadata = this.parseMetadata(participant.metadata);
const metadata: MeetTokenMetadata = this.parseMetadata(participant!.metadata); // Update selected role and roles array
metadata.selectedRole = newRole;
const currentRoles = metadata.roles;
if (!metadata || typeof metadata !== 'object') { if (!currentRoles.some((r) => r.role === newRole)) {
throw new Error(`Invalid metadata for participant ${participantName}`); const { openvidu } = this.getParticipantPermissions(roomId, newRole);
currentRoles.push({ role: newRole, permissions: openvidu });
} }
// TODO: Should we update the roles array as well? await this.livekitService.updateParticipantMetadata(roomId, participantIdentity, JSON.stringify(metadata));
metadata.selectedRole = newRole;
await this.livekitService.updateParticipantMetadata(roomId, participantName, JSON.stringify(metadata));
const { speakerSecret, moderatorSecret } = MeetRoomHelper.extractSecretsFromRoom(meetRoom); const { speakerSecret, moderatorSecret } = MeetRoomHelper.extractSecretsFromRoom(meetRoom);
const secret = newRole === ParticipantRole.MODERATOR ? moderatorSecret : speakerSecret; const secret = newRole === ParticipantRole.MODERATOR ? moderatorSecret : speakerSecret;
await this.frontendEventService.sendParticipantRoleUpdatedSignal(roomId, participantName, newRole, secret); await this.frontendEventService.sendParticipantRoleUpdatedSignal(
roomId,
participantIdentity,
newRole,
secret
);
} catch (error) { } catch (error) {
this.logger.error('Error changing participant role:', error); this.logger.error('Error updating participant role:', error);
throw error; throw error;
} }
} }
protected parseMetadata(metadata: string): MeetTokenMetadata { parseMetadata(metadata: string): MeetTokenMetadata {
try { try {
return JSON.parse(metadata); const parsedMetadata = JSON.parse(metadata);
return validateMeetTokenMetadata(parsedMetadata);
} catch (error) { } catch (error) {
this.logger.error('Failed to parse participant metadata:', error); this.logger.error('Failed to parse participant metadata:', error);
throw new Error('Invalid participant metadata format'); throw new Error('Invalid participant metadata format');

View File

@ -11,7 +11,7 @@ import {
} from '../../../helpers/request-helpers.js'; } from '../../../helpers/request-helpers.js';
import { RoomData, setupSingleRoom } from '../../../helpers/test-scenarios.js'; import { RoomData, setupSingleRoom } from '../../../helpers/test-scenarios.js';
const participantName = 'TEST_PARTICIPANT'; const participantIdentity = 'TEST_PARTICIPANT';
describe('Meetings API Tests', () => { describe('Meetings API Tests', () => {
let livekitService: LiveKitService; let livekitService: LiveKitService;
@ -34,17 +34,17 @@ describe('Meetings API Tests', () => {
it('should remove participant from LiveKit room', async () => { it('should remove participant from LiveKit room', async () => {
// Check if participant exists before deletion // Check if participant exists before deletion
const participant = await livekitService.getParticipant(roomData.room.roomId, participantName); const participant = await livekitService.getParticipant(roomData.room.roomId, participantIdentity);
expect(participant).toBeDefined(); expect(participant).toBeDefined();
expect(participant.identity).toBe(participantName); expect(participant.identity).toBe(participantIdentity);
// Delete the participant // Delete the participant
const response = await deleteParticipant(roomData.room.roomId, participantName, roomData.moderatorCookie); const response = await deleteParticipant(roomData.room.roomId, participantIdentity, roomData.moderatorCookie);
expect(response.status).toBe(200); expect(response.status).toBe(200);
// Check if the participant has been removed from LiveKit // Check if the participant has been removed from LiveKit
try { try {
await livekitService.getParticipant(roomData.room.roomId, participantName); await livekitService.getParticipant(roomData.room.roomId, participantIdentity);
} catch (error) { } catch (error) {
expect((error as OpenViduMeetError).statusCode).toBe(404); expect((error as OpenViduMeetError).statusCode).toBe(404);
} }
@ -65,7 +65,7 @@ describe('Meetings API Tests', () => {
let response = await deleteRoom(roomData.room.roomId, { force: true }); let response = await deleteRoom(roomData.room.roomId, { force: true });
expect(response.status).toBe(204); expect(response.status).toBe(204);
response = await deleteParticipant(roomData.room.roomId, participantName, roomData.moderatorCookie); response = await deleteParticipant(roomData.room.roomId, participantIdentity, roomData.moderatorCookie);
expect(response.status).toBe(404); expect(response.status).toBe(404);
expect(response.body.error).toBe('Room Error'); expect(response.body.error).toBe('Room Error');
}); });