From 13c88d201c20d7ed6087521c7365ee06cb98510e Mon Sep 17 00:00:00 2001 From: juancarmore Date: Thu, 14 Aug 2025 13:50:47 +0200 Subject: [PATCH] backend: refactor code to generate participantIdentity based on name and unique ID --- .../participant-validator.middleware.ts | 3 +- backend/src/models/error.model.ts | 4 ++ backend/src/services/livekit.service.ts | 43 ++++++++++------ backend/src/services/participant.service.ts | 51 +++++++++++-------- backend/src/services/token.service.ts | 15 +++++- typings/src/participant.ts | 4 ++ 6 files changed, 82 insertions(+), 38 deletions(-) diff --git a/backend/src/middlewares/request-validators/participant-validator.middleware.ts b/backend/src/middlewares/request-validators/participant-validator.middleware.ts index 4274d10..4711a84 100644 --- a/backend/src/middlewares/request-validators/participant-validator.middleware.ts +++ b/backend/src/middlewares/request-validators/participant-validator.middleware.ts @@ -7,7 +7,8 @@ import { nonEmptySanitizedRoomId } from './room-validator.middleware.js'; const ParticipantTokenRequestSchema: z.ZodType = z.object({ roomId: nonEmptySanitizedRoomId('roomId'), secret: z.string().nonempty('Secret is required'), - participantName: z.string().optional() + participantName: z.string().optional(), + participantIdentity: z.string().optional() }); const UpdateParticipantRequestSchema = z.object({ diff --git a/backend/src/models/error.model.ts b/backend/src/models/error.model.ts index d1f3fe4..8905b13 100644 --- a/backend/src/models/error.model.ts +++ b/backend/src/models/error.model.ts @@ -242,6 +242,10 @@ export const errorInvalidParticipantRole = (): OpenViduMeetError => { return new OpenViduMeetError('Participant', 'No valid participant role provided', 400); }; +export const errorParticipantIdentityNotProvided = (): OpenViduMeetError => { + return new OpenViduMeetError('Participant', 'No participant identity provided', 400); +}; + // Handlers export const handleError = (res: Response, error: OpenViduMeetError | unknown, operationDescription: string) => { diff --git a/backend/src/services/livekit.service.ts b/backend/src/services/livekit.service.ts index 5d33427..583aeca 100644 --- a/backend/src/services/livekit.service.ts +++ b/backend/src/services/livekit.service.ts @@ -182,6 +182,34 @@ export class LiveKitService { } } + async participantExists( + roomName: string, + participantNameOrIdentity: string, + participantField: 'name' | 'identity' = 'identity' + ): Promise { + try { + const participants: ParticipantInfo[] = await this.listRoomParticipants(roomName); + return participants.some((participant) => { + let fieldValue = participant[participantField]; + + // If the field is empty or undefined, use identity as a fallback + if (!fieldValue && participantField === 'name') { + fieldValue = participant.identity; + } + + return fieldValue === participantNameOrIdentity; + }); + } catch (error: any) { + this.logger.error(error); + + if (error?.cause?.code === 'ECONNREFUSED') { + throw errorLivekitNotAvailable(); + } + + return false; + } + } + /** * Retrieves information about a specific participant in a LiveKit room. * @@ -383,19 +411,4 @@ export class LiveKitService { // TODO: Remove deprecated warning by using ParticipantInfo_Kind: participant.kind === ParticipantInfo_Kind.EGRESS; return participant.identity.startsWith('EG_') && participant.permission?.recorder === true; } - - private async participantExists(roomName: string, participantIdentity: string): Promise { - try { - const participants: ParticipantInfo[] = await this.listRoomParticipants(roomName); - return participants.some((participant) => participant.identity === participantIdentity); - } catch (error: any) { - this.logger.error(error); - - if (error?.cause?.code === 'ECONNREFUSED') { - throw errorLivekitNotAvailable(); - } - - return false; - } - } } diff --git a/backend/src/services/participant.service.ts b/backend/src/services/participant.service.ts index 6a6df01..5b79c5f 100644 --- a/backend/src/services/participant.service.ts +++ b/backend/src/services/participant.service.ts @@ -9,7 +9,11 @@ import { inject, injectable } from 'inversify'; 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, + errorParticipantIdentityNotProvided, + errorParticipantNotFound +} from '../models/error.model.js'; import { FrontendEventService, LiveKitService, LoggerService, RoomService, TokenService } from './index.js'; @injectable() @@ -27,20 +31,29 @@ export class ParticipantService { currentRoles: { role: ParticipantRole; permissions: OpenViduMeetPermissions }[], refresh = false ): Promise { - const { roomId, participantName, secret } = participantOptions; + const { roomId, secret, participantName, participantIdentity } = participantOptions; if (participantName) { - // Check if participant with same participantName exists in the room - const participantExists = await this.participantExists(roomId, participantName); + if (!refresh) { + // Check if participant with same participantName exists in the room + const participantExists = await this.participantExists(roomId, participantName, 'name'); - if (!refresh && participantExists) { - this.logger.verbose(`Participant '${participantName}' already exists in room '${roomId}'`); - throw errorParticipantAlreadyExists(participantName, roomId); - } + if (participantExists) { + this.logger.verbose(`Participant '${participantName}' already exists in room '${roomId}'`); + throw errorParticipantAlreadyExists(participantName, roomId); + } + } else { + if (!participantIdentity) { + throw errorParticipantIdentityNotProvided(); + } - if (refresh && !participantExists) { - this.logger.verbose(`Participant '${participantName}' does not exist in room '${roomId}'`); - throw errorParticipantNotFound(participantName, roomId); + // Check if participant with same participantIdentity exists in the room + const participantExists = await this.participantExists(roomId, participantIdentity, 'identity'); + + if (!participantExists) { + this.logger.verbose(`Participant '${participantName}' does not exist in room '${roomId}'`); + throw errorParticipantNotFound(participantName, roomId); + } } } @@ -70,15 +83,13 @@ export class ParticipantService { return this.livekitService.getParticipant(roomId, participantIdentity); } - async participantExists(roomId: string, participantIdentity: string): Promise { - this.logger.verbose(`Checking if participant '${participantIdentity}' exists in room '${roomId}'`); - - try { - await this.getParticipant(roomId, participantIdentity); - return true; - } catch (error) { - return false; - } + async participantExists( + roomId: string, + participantNameOrIdentity: string, + participantField: 'name' | 'identity' = 'identity' + ): Promise { + this.logger.verbose(`Checking if participant '${participantNameOrIdentity}' exists in room '${roomId}'`); + return this.livekitService.participantExists(roomId, participantNameOrIdentity, participantField); } async deleteParticipant(roomId: string, participantIdentity: string): Promise { diff --git a/backend/src/services/token.service.ts b/backend/src/services/token.service.ts index 9305cec..4be127c 100644 --- a/backend/src/services/token.service.ts +++ b/backend/src/services/token.service.ts @@ -13,6 +13,7 @@ import { AccessToken, AccessTokenOptions, ClaimGrants, TokenVerifier, VideoGrant import INTERNAL_CONFIG from '../config/internal-config.js'; import { LIVEKIT_API_KEY, LIVEKIT_API_SECRET, LIVEKIT_URL } from '../environment.js'; import { LoggerService } from './index.js'; +import { uid } from 'uid'; @injectable() export class TokenService { @@ -47,7 +48,17 @@ export class TokenService { selectedRole: ParticipantRole ): Promise { const { roomId, participantName } = participantOptions; - this.logger.info(`Generating token for room '${roomId}'`); + this.logger.info( + `Generating token for room '${roomId}'` + (participantName ? ` and participant '${participantName}'` : '') + ); + + let { participantIdentity } = participantOptions; + + if (participantName && !participantIdentity) { + // Generate participant identity based on name and unique ID + const identityPrefix = participantName.replace(/\s+/g, ''); // Remove all spaces + participantIdentity = `${identityPrefix}-${uid(5)}`; + } const metadata: MeetTokenMetadata = { livekitUrl: LIVEKIT_URL, @@ -55,7 +66,7 @@ export class TokenService { selectedRole }; const tokenOptions: AccessTokenOptions = { - identity: participantName, + identity: participantIdentity, name: participantName, ttl: INTERNAL_CONFIG.PARTICIPANT_TOKEN_EXPIRATION, metadata: JSON.stringify(metadata) diff --git a/typings/src/participant.ts b/typings/src/participant.ts index d84bc99..d22857e 100644 --- a/typings/src/participant.ts +++ b/typings/src/participant.ts @@ -17,6 +17,10 @@ export interface ParticipantOptions { * The name of the participant. */ participantName?: string; + /** + * The identity of the participant. + */ + participantIdentity?: string; } /**