diff --git a/backend/src/controllers/global-preferences/room-preferences.controller.ts b/backend/src/controllers/global-preferences/room-preferences.controller.ts index d38a5bc..1e02432 100644 --- a/backend/src/controllers/global-preferences/room-preferences.controller.ts +++ b/backend/src/controllers/global-preferences/room-preferences.controller.ts @@ -8,13 +8,13 @@ export const updateRoomPreferences = async (req: Request, res: Response) => { const logger = container.get(LoggerService); logger.verbose(`Updating room preferences: ${JSON.stringify(req.body)}`); - const { roomName, roomPreferences } = req.body; + const { roomId, roomPreferences } = req.body; try { const preferenceService = container.get(GlobalPreferencesService); preferenceService.validateRoomPreferences(roomPreferences); - const savedPreferences = await preferenceService.updateOpenViduRoomPreferences(roomName, roomPreferences); + const savedPreferences = await preferenceService.updateOpenViduRoomPreferences(roomId, roomPreferences); return res .status(200) @@ -34,9 +34,9 @@ export const getRoomPreferences = async (req: Request, res: Response) => { const logger = container.get(LoggerService); try { - const roomName = req.params.roomName; + const { roomId } = req.params; const preferenceService = container.get(GlobalPreferencesService); - const preferences = await preferenceService.getOpenViduRoomPreferences(roomName); + const preferences = await preferenceService.getOpenViduRoomPreferences(roomId); if (!preferences) { return res.status(404).json({ message: 'Room preferences not found' }); diff --git a/backend/src/controllers/participant.controller.ts b/backend/src/controllers/participant.controller.ts index 9576fa2..f778ef8 100644 --- a/backend/src/controllers/participant.controller.ts +++ b/backend/src/controllers/participant.controller.ts @@ -7,22 +7,24 @@ import { ParticipantService } from '../services/participant.service.js'; import { MEET_PARTICIPANT_TOKEN_EXPIRATION, PARTICIPANT_TOKEN_COOKIE_NAME } from '../environment.js'; import { getCookieOptions } from '../utils/cookie-utils.js'; import { TokenService } from '../services/token.service.js'; +import { RoomService } from '../services/room.service.js'; export const generateParticipantToken = async (req: Request, res: Response) => { const logger = container.get(LoggerService); - const tokenOptions: TokenOptions = req.body; - const { roomName } = tokenOptions; const participantService = container.get(ParticipantService); + const roomService = container.get(RoomService); + const tokenOptions: TokenOptions = req.body; + const { roomId } = tokenOptions; try { - logger.verbose(`Generating participant token for room ${roomName}`); + logger.verbose(`Generating participant token for room ${roomId}`); + await roomService.createLivekitRoom(roomId); const token = await participantService.generateOrRefreshParticipantToken(tokenOptions); res.cookie(PARTICIPANT_TOKEN_COOKIE_NAME, token, getCookieOptions('/', MEET_PARTICIPANT_TOKEN_EXPIRATION)); - logger.verbose(`Participant token generated for room ${roomName}`); return res.status(200).json({ token }); } catch (error) { - logger.error(`Error generating participant token for room: ${roomName}`); + logger.error(`Error generating participant token for room: ${roomId}`); return handleError(res, error); } }; @@ -47,18 +49,18 @@ export const refreshParticipantToken = async (req: Request, res: Response) => { } const tokenOptions: TokenOptions = req.body; - const { roomName } = tokenOptions; + const { roomId } = tokenOptions; const participantService = container.get(ParticipantService); try { - logger.verbose(`Refreshing participant token for room ${roomName}`); + logger.verbose(`Refreshing participant token for room ${roomId}`); const token = await participantService.generateOrRefreshParticipantToken(tokenOptions, true); res.cookie(PARTICIPANT_TOKEN_COOKIE_NAME, token, getCookieOptions('/', MEET_PARTICIPANT_TOKEN_EXPIRATION)); - logger.verbose(`Participant token refreshed for room ${roomName}`); + logger.verbose(`Participant token refreshed for room ${roomId}`); return res.status(200).json({ token }); } catch (error) { - logger.error(`Error refreshing participant token for room: ${roomName}`); + logger.error(`Error refreshing participant token for room: ${roomId}`); return handleError(res, error); } }; @@ -67,13 +69,13 @@ export const deleteParticipant = async (req: Request, res: Response) => { const logger = container.get(LoggerService); const participantService = container.get(ParticipantService); const { participantName } = req.params; - const roomName: string = req.query.roomName as string; + const roomId: string = req.query.roomId as string; try { - await participantService.deleteParticipant(participantName, roomName); + await participantService.deleteParticipant(participantName, roomId); res.status(200).json({ message: 'Participant deleted' }); } catch (error) { - logger.error(`Error deleting participant from room: ${roomName}`); + logger.error(`Error deleting participant from room: ${roomId}`); return handleError(res, error); } }; diff --git a/backend/src/controllers/room.controller.ts b/backend/src/controllers/room.controller.ts index d45333c..d160e8d 100644 --- a/backend/src/controllers/room.controller.ts +++ b/backend/src/controllers/room.controller.ts @@ -14,7 +14,7 @@ export const createRoom = async (req: Request, res: Response) => { logger.verbose(`Creating room with options '${JSON.stringify(options)}'`); const baseUrl = `${req.protocol}://${req.get('host')}`; - const room = await roomService.createRoom(baseUrl, options); + const room = await roomService.createMeetRoom(baseUrl, options); return res.status(200).json(room); } catch (error) { logger.error(`Error creating room with options '${JSON.stringify(options)}'`); @@ -47,14 +47,14 @@ export const getRooms = async (req: Request, res: Response) => { export const getRoom = async (req: Request, res: Response) => { const logger = container.get(LoggerService); - const { roomName } = req.params; + const { roomId } = req.params; const fields = req.query.fields as string[] | undefined; try { - logger.verbose(`Getting room with id '${roomName}'`); + logger.verbose(`Getting room with id '${roomId}'`); const roomService = container.get(RoomService); - const room = await roomService.getOpenViduRoom(roomName); + const room = await roomService.getMeetRoom(roomId); if (fields && fields.length > 0) { const filteredRoom = filterObjectFields(room, fields); @@ -63,7 +63,7 @@ export const getRoom = async (req: Request, res: Response) => { return res.status(200).json(room); } catch (error) { - logger.error(`Error getting room with id '${roomName}'`); + logger.error(`Error getting room with id '${roomId}'`); handleError(res, error); } }; @@ -72,14 +72,14 @@ export const deleteRooms = async (req: Request, res: Response) => { const logger = container.get(LoggerService); const roomService = container.get(RoomService); - const { roomName } = req.params; - const { roomNames } = req.body; + const { roomId } = req.params; + const { roomIds } = req.body; - const roomsToDelete = roomName ? [roomName] : roomNames; + const roomsToDelete = roomId ? [roomId] : roomIds; - // TODO: Validate roomNames with ZOD + // TODO: Validate roomIds with ZOD if (!Array.isArray(roomsToDelete) || roomsToDelete.length === 0) { - return res.status(400).json({ error: 'roomNames must be a non-empty array' }); + return res.status(400).json({ error: 'roomIds must be a non-empty array' }); } try { @@ -98,16 +98,16 @@ export const getParticipantRole = async (req: Request, res: Response) => { const logger = container.get(LoggerService); const roomService = container.get(RoomService); - const { roomName } = req.params; + const { roomId } = req.params; const { secret } = req.query as { secret: string }; try { - logger.verbose(`Getting participant role for room '${roomName}'`); + logger.verbose(`Getting participant role for room '${roomId}'`); - const role = await roomService.getRoomSecretRole(roomName, secret); + const role = await roomService.getRoomSecretRole(roomId, secret); return res.status(200).json(role); } catch (error) { - logger.error(`Error getting participant role for room '${roomName}'`); + logger.error(`Error getting participant role for room '${roomId}'`); handleError(res, error); } }; diff --git a/backend/src/helpers/ov-components-adapter.helper.ts b/backend/src/helpers/ov-components-adapter.helper.ts index ba4b704..2b97164 100644 --- a/backend/src/helpers/ov-components-adapter.helper.ts +++ b/backend/src/helpers/ov-components-adapter.helper.ts @@ -45,7 +45,7 @@ export class OpenViduComponentsAdapterHelper { private static parseRecordingInfoToOpenViduComponents(info: MeetRecordingInfo) { return { id: info.recordingId, - roomName: info.details ?? '', + roomName: info.roomId, roomId: info.roomId, // outputMode: info.outputMode, status: this.mapRecordingStatus(info.status), diff --git a/backend/src/helpers/room.helper.ts b/backend/src/helpers/room.helper.ts index 276a891..1cbda09 100644 --- a/backend/src/helpers/room.helper.ts +++ b/backend/src/helpers/room.helper.ts @@ -1,7 +1,4 @@ import { MeetRoom, MeetRoomOptions } from '@typings-ce'; -import { CreateOptions } from 'livekit-server-sdk'; -import { MEET_NAME_ID } from '../environment.js'; -import { uid } from 'uid/single'; export class MeetRoomHelper { private constructor() { @@ -17,43 +14,9 @@ export class MeetRoomHelper { static toOpenViduOptions(room: MeetRoom): MeetRoomOptions { return { expirationDate: room.expirationDate, - maxParticipants: room.maxParticipants, + // maxParticipants: room.maxParticipants, preferences: room.preferences, - roomNamePrefix: room.roomNamePrefix + roomIdPrefix: room.roomIdPrefix }; } - - static generateLivekitRoomOptions(roomInput: MeetRoom | MeetRoomOptions): CreateOptions { - const isOpenViduRoom = 'creationDate' in roomInput; - const sanitizedPrefix = roomInput.roomNamePrefix - ?.trim() - .replace(/[^a-zA-Z0-9-]/g, '') - .replace(/-+$/, ''); - const sanitizedRoomName = sanitizedPrefix ? `${sanitizedPrefix}-${uid(15)}` : uid(15); - const { - roomName = sanitizedRoomName, - expirationDate, - maxParticipants, - creationDate = Date.now() - } = roomInput as MeetRoom; - - const timeUntilExpiration = this.calculateExpirationTime(expirationDate, creationDate); - - return { - name: roomName, - metadata: JSON.stringify({ - createdBy: MEET_NAME_ID, - roomOptions: isOpenViduRoom - ? MeetRoomHelper.toOpenViduOptions(roomInput as MeetRoom) - : roomInput - }), - emptyTimeout: timeUntilExpiration, - maxParticipants: maxParticipants || undefined, - departureTimeout: 31_536_000 // 1 year - }; - } - - private static calculateExpirationTime(expirationDate: number, creationDate: number): number { - return Math.max(0, Math.floor((expirationDate - creationDate) / 1000)); - } } diff --git a/backend/src/middlewares/participant.middleware.ts b/backend/src/middlewares/participant.middleware.ts index 96f0e43..cab0572 100644 --- a/backend/src/middlewares/participant.middleware.ts +++ b/backend/src/middlewares/participant.middleware.ts @@ -19,8 +19,8 @@ export const configureTokenAuth = async (req: Request, res: Response, next: Next let role: ParticipantRole; try { - const { roomName, secret } = req.body as TokenOptions; - role = await roomService.getRoomSecretRole(roomName, secret); + const { roomId, secret } = req.body as TokenOptions; + role = await roomService.getRoomSecretRole(roomId, secret); } catch (error) { logger.error('Error getting room secret role', error); return res.status(500).json({ message: 'Internal server error' }); @@ -55,14 +55,14 @@ export const configureTokenAuth = async (req: Request, res: Response, next: Next }; export const withModeratorPermissions = async (req: Request, res: Response, next: NextFunction) => { - const roomName = req.query.roomName as string; + const roomId = req.query.roomId as string; const payload = req.session?.tokenClaims; if (!payload) { return res.status(403).json({ message: 'Insufficient permissions to access this resource' }); } - const sameRoom = payload.video?.room === roomName; + const sameRoom = payload.video?.room === roomId; const metadata = JSON.parse(payload.metadata || '{}'); const role = metadata.role as ParticipantRole; diff --git a/backend/src/middlewares/recording.middleware.ts b/backend/src/middlewares/recording.middleware.ts index 2e50cf4..dee9b0e 100644 --- a/backend/src/middlewares/recording.middleware.ts +++ b/backend/src/middlewares/recording.middleware.ts @@ -22,7 +22,7 @@ export const withRecordingEnabled = async (req: Request, res: Response, next: Ne try { const roomService = container.get(RoomService); - room = await roomService.getOpenViduRoom(roomId); + room = await roomService.getMeetRoom(roomId); } catch (error) { logger.error('Error checking recording preferences:' + error); return res.status(403).json({ message: 'Recording is disabled in this room' }); diff --git a/backend/src/middlewares/request-validators/participant-validator.middleware.ts b/backend/src/middlewares/request-validators/participant-validator.middleware.ts index eaae28a..e6a9971 100644 --- a/backend/src/middlewares/request-validators/participant-validator.middleware.ts +++ b/backend/src/middlewares/request-validators/participant-validator.middleware.ts @@ -3,13 +3,13 @@ import { Request, Response, NextFunction } from 'express'; import { z } from 'zod'; const ParticipantTokenRequestSchema: z.ZodType = z.object({ - roomName: z.string().nonempty('Room name is required'), + roomId: z.string().nonempty('Room ID is required'), participantName: z.string().nonempty('Participant name is required'), secret: z.string().nonempty('Secret is required') }); const DeleteParticipantSchema = z.object({ - roomName: z.string().trim().min(1, 'roomName is required') + roomId: z.string().trim().min(1, 'Room ID is required') }); export const validateParticipantTokenRequest = (req: Request, res: Response, next: NextFunction) => { diff --git a/backend/src/middlewares/request-validators/room-validator.middleware.ts b/backend/src/middlewares/request-validators/room-validator.middleware.ts index 5fe310b..0adf10e 100644 --- a/backend/src/middlewares/request-validators/room-validator.middleware.ts +++ b/backend/src/middlewares/request-validators/room-validator.middleware.ts @@ -1,26 +1,26 @@ import { - ChatPreferences, + MeetChatPreferences, MeetRoomOptions, - RecordingPreferences, - RoomPreferences, - VirtualBackgroundPreferences + MeetRecordingPreferences, + MeetRoomPreferences, + MeetVirtualBackgroundPreferences } from '@typings-ce'; import { Request, Response, NextFunction } from 'express'; import { z } from 'zod'; -const RecordingPreferencesSchema: z.ZodType = z.object({ +const RecordingPreferencesSchema: z.ZodType = z.object({ enabled: z.boolean() }); -const ChatPreferencesSchema: z.ZodType = z.object({ +const ChatPreferencesSchema: z.ZodType = z.object({ enabled: z.boolean() }); -const VirtualBackgroundPreferencesSchema: z.ZodType = z.object({ +const VirtualBackgroundPreferencesSchema: z.ZodType = z.object({ enabled: z.boolean() }); -const RoomPreferencesSchema: z.ZodType = z.object({ +const RoomPreferencesSchema: z.ZodType = z.object({ recordingPreferences: RecordingPreferencesSchema, chatPreferences: ChatPreferencesSchema, virtualBackgroundPreferences: VirtualBackgroundPreferencesSchema @@ -31,44 +31,41 @@ const RoomRequestOptionsSchema: z.ZodType = z.object({ .number() .positive('Expiration date must be a positive integer') .min(Date.now(), 'Expiration date must be in the future'), - roomNamePrefix: z + roomIdPrefix: z .string() - .transform((val) => val.replace(/\s+/g, '-')) + .transform( + (val) => + val + .trim() // Remove leading and trailing spaces + .replace(/\s+/g, '') // Remove all whitespace instead of replacing it with hyphens + .replace(/[^a-zA-Z0-9-]/g, '') // Remove any character except letters, numbers, and hyphens + .replace(/-+/g, '-') // Replace multiple consecutive hyphens with a single one + .replace(/-+$/, '') // Remove trailing hyphens + ) .optional() .default(''), preferences: RoomPreferencesSchema.optional().default({ recordingPreferences: { enabled: true }, chatPreferences: { enabled: true }, virtualBackgroundPreferences: { enabled: true } - }), - maxParticipants: z - .number() - .positive('Max participants must be a positive integer') - .nullable() - .optional() - .default(null) + }) + // maxParticipants: z + // .number() + // .positive('Max participants must be a positive integer') + // .nullable() + // .optional() + // .default(null) }); const GetParticipantRoleSchema = z.object({ secret: z.string() }); -export const validateRoomRequest = (req: Request, res: Response, next: NextFunction) => { +export const withValidRoomOptions = (req: Request, res: Response, next: NextFunction) => { const { success, error, data } = RoomRequestOptionsSchema.safeParse(req.body); if (!success) { - const errors = error.errors.map((error) => ({ - field: error.path.join('.'), - message: error.message - })); - - console.log(errors); - - return res.status(422).json({ - error: 'Unprocessable Entity', - message: 'Invalid request body', - details: errors - }); + return rejectRequest(res, error); } req.body = data; @@ -105,3 +102,16 @@ export const validateGetParticipantRoleRequest = (req: Request, res: Response, n req.query = data; next(); }; + +const rejectRequest = (res: Response, error: z.ZodError) => { + const errors = error.errors.map((error) => ({ + field: error.path.join('.'), + message: error.message + })); + + return res.status(422).json({ + error: 'Unprocessable Entity', + message: 'Invalid request body', + details: errors + }); +}; diff --git a/backend/src/middlewares/room.middleware.ts b/backend/src/middlewares/room.middleware.ts index 47253b2..73e5851 100644 --- a/backend/src/middlewares/room.middleware.ts +++ b/backend/src/middlewares/room.middleware.ts @@ -52,7 +52,7 @@ export const configureCreateRoomAuth = async (req: Request, res: Response, next: * - If the user is not a moderator, access is denied. */ export const configureRoomAuthorization = async (req: Request, res: Response, next: NextFunction) => { - const roomName = req.params.roomName as string; + const roomId = req.params.roomId as string; const payload = req.session?.tokenClaims; // If there is no token, the user is admin or it is invoked using the API key @@ -61,7 +61,7 @@ export const configureRoomAuthorization = async (req: Request, res: Response, ne return next(); } - const sameRoom = payload.video?.room === roomName; + const sameRoom = payload.video?.room === roomId; const metadata = JSON.parse(payload.metadata || '{}'); const role = metadata.role as ParticipantRole; diff --git a/backend/src/models/error.model.ts b/backend/src/models/error.model.ts index 1cd377c..a329c45 100644 --- a/backend/src/models/error.model.ts +++ b/backend/src/models/error.model.ts @@ -71,8 +71,8 @@ export const errorRecordingCannotBeStoppedWhileStarting = (recordingId: string): return new OpenViduMeetError('Recording Error', `Recording '${recordingId}' cannot be stopped while starting`, 409); }; -export const errorRecordingAlreadyStarted = (roomName: string): OpenViduMeetError => { - return new OpenViduMeetError('Recording Error', `The room '${roomName}' is already being recorded`, 409); +export const errorRecordingAlreadyStarted = (roomId: string): OpenViduMeetError => { + return new OpenViduMeetError('Recording Error', `The room '${roomId}' is already being recorded`, 409); }; const isMatchingError = (error: OpenViduMeetError, originalError: OpenViduMeetError): boolean => { @@ -100,24 +100,24 @@ export const isErrorRecordingCannotBeStoppedWhileStarting = ( }; // Room errors -export const errorRoomNotFound = (roomName: string): OpenViduMeetError => { - return new OpenViduMeetError('Room Error', `The room '${roomName}' does not exist`, 404); +export const errorRoomNotFound = (roomId: string): OpenViduMeetError => { + return new OpenViduMeetError('Room Error', `The room '${roomId}' does not exist`, 404); }; // Participant errors -export const errorParticipantUnauthorized = (roomName: string): OpenViduMeetError => { +export const errorParticipantUnauthorized = (roomId: string): OpenViduMeetError => { return new OpenViduMeetError( 'Participant Error', - `Unauthorized generating token with received credentials in room '${roomName}'`, + `Unauthorized generating token with received credentials in room '${roomId}'`, 406 ); }; -export const errorParticipantNotFound = (participantName: string, roomName: string): OpenViduMeetError => { - return new OpenViduMeetError('Participant Error', `'${participantName}' not found in room '${roomName}'`, 404); +export const errorParticipantNotFound = (participantName: string, roomId: string): OpenViduMeetError => { + return new OpenViduMeetError('Participant Error', `'${participantName}' not found in room '${roomId}'`, 404); }; -export const errorParticipantAlreadyExists = (participantName: string, roomName: string): OpenViduMeetError => { - return new OpenViduMeetError('Room Error', `'${participantName}' already exists in room in ${roomName}`, 409); +export const errorParticipantAlreadyExists = (participantName: string, roomId: string): OpenViduMeetError => { + return new OpenViduMeetError('Room Error', `'${participantName}' already exists in room in ${roomId}`, 409); }; diff --git a/backend/src/routes/global-preferences.routes.ts b/backend/src/routes/global-preferences.routes.ts index 45873d2..6d2eb7d 100644 --- a/backend/src/routes/global-preferences.routes.ts +++ b/backend/src/routes/global-preferences.routes.ts @@ -46,4 +46,4 @@ preferencesRouter.get( '/appearance', withAuth(apiKeyValidator, tokenAndRoleValidator(UserRole.ADMIN)), appearancePrefCtrl.getAppearancePreferences -); +); \ No newline at end of file diff --git a/backend/src/routes/room.routes.ts b/backend/src/routes/room.routes.ts index a11be5a..df5d5b2 100644 --- a/backend/src/routes/room.routes.ts +++ b/backend/src/routes/room.routes.ts @@ -5,22 +5,26 @@ import { withAuth, tokenAndRoleValidator, apiKeyValidator, - participantTokenValidator -} from '../middlewares/auth.middleware.js'; -import { + participantTokenValidator, validateGetParticipantRoleRequest, validateGetRoomQueryParams, - validateRoomRequest -} from '../middlewares/request-validators/room-validator.middleware.js'; + withValidRoomOptions, + configureCreateRoomAuth, + configureRoomAuthorization +} from '../middlewares/index.js'; + import { UserRole } from '@typings-ce'; -import { configureCreateRoomAuth, configureRoomAuthorization } from '../middlewares/room.middleware.js'; export const roomRouter = Router(); roomRouter.use(bodyParser.urlencoded({ extended: true })); roomRouter.use(bodyParser.json()); // Room Routes -roomRouter.post('/', configureCreateRoomAuth, validateRoomRequest, roomCtrl.createRoom); +roomRouter.post('/', configureCreateRoomAuth, withValidRoomOptions, roomCtrl.createRoom); + + + + roomRouter.get( '/', withAuth(apiKeyValidator, tokenAndRoleValidator(UserRole.ADMIN)), @@ -28,13 +32,13 @@ roomRouter.get( roomCtrl.getRooms ); roomRouter.get( - '/:roomName', + '/:roomId', withAuth(apiKeyValidator, tokenAndRoleValidator(UserRole.ADMIN), participantTokenValidator), configureRoomAuthorization, validateGetRoomQueryParams, roomCtrl.getRoom ); -roomRouter.delete('/:roomName', withAuth(apiKeyValidator, tokenAndRoleValidator(UserRole.ADMIN)), roomCtrl.deleteRooms); +roomRouter.delete('/:roomId', withAuth(apiKeyValidator, tokenAndRoleValidator(UserRole.ADMIN)), roomCtrl.deleteRooms); // Room preferences roomRouter.put('/', withAuth(apiKeyValidator, tokenAndRoleValidator(UserRole.ADMIN)), roomCtrl.updateRoomPreferences); @@ -44,4 +48,4 @@ export const internalRoomRouter = Router(); internalRoomRouter.use(bodyParser.urlencoded({ extended: true })); internalRoomRouter.use(bodyParser.json()); -internalRoomRouter.get('/:roomName/participant-role', validateGetParticipantRoleRequest, roomCtrl.getParticipantRole); +internalRoomRouter.get('/:roomId/participant-role', validateGetParticipantRoleRequest, roomCtrl.getParticipantRole); diff --git a/backend/src/services/livekit.service.ts b/backend/src/services/livekit.service.ts index 1cd2546..7b6665f 100644 --- a/backend/src/services/livekit.service.ts +++ b/backend/src/services/livekit.service.ts @@ -27,7 +27,8 @@ import { errorLivekitIsNotAvailable, errorParticipantNotFound, errorRoomNotFound, - internalError + internalError, + OpenViduMeetError } from '../models/error.model.js'; import { ParticipantPermissions, ParticipantRole, TokenOptions } from '@typings-ce'; import { RecordingHelper } from '../helpers/recording.helper.js'; @@ -52,6 +53,28 @@ export class LiveKitService { } } + /** + * Checks if a room with the specified name exists in LiveKit. + * + * @param roomName - The name of the room to check + * @returns A Promise that resolves to true if the room exists, false otherwise + * @throws Will rethrow service availability or other unexpected errors + */ + async roomExists(roomName: string): Promise { + try { + await this.getRoom(roomName); + return true; + } catch (error) { + if (error instanceof OpenViduMeetError && error.statusCode === 404) { + return false; + } + + // Rethrow other errors as they indicate we couldn't determine if the room exists + this.logger.error(`Error checking if room ${roomName} exists:`, error); + throw error; + } + } + async getRoom(roomName: string): Promise { let rooms: Room[] = []; @@ -94,6 +117,14 @@ export class LiveKitService { } } + /** + * Retrieves information about a specific participant in a LiveKit room. + * + * @param roomName - The name of the room where the participant is located + * @param participantName - The name of the participant to retrieve + * @returns A Promise that resolves to the participant's information + * @throws An internal error if the participant cannot be found or another error occurs + */ async getParticipant(roomName: string, participantName: string): Promise { try { return await this.roomClient.getParticipant(roomName, participantName); @@ -132,8 +163,8 @@ export class LiveKitService { permissions: ParticipantPermissions, role: ParticipantRole ): Promise { - const { roomName, participantName } = options; - this.logger.info(`Generating token for ${participantName} in room ${roomName}`); + const { roomId, participantName } = options; + this.logger.info(`Generating token for ${participantName} in room ${roomId}`); const at = new AccessToken(LIVEKIT_API_KEY, LIVEKIT_API_SECRET, { identity: participantName, diff --git a/backend/src/services/participant.service.ts b/backend/src/services/participant.service.ts index 6de9ebd..53f4b77 100644 --- a/backend/src/services/participant.service.ts +++ b/backend/src/services/participant.service.ts @@ -15,64 +15,66 @@ export class ParticipantService { ) {} async generateOrRefreshParticipantToken(options: TokenOptions, refresh = false): Promise { - const { roomName, participantName, secret } = options; + const { roomId, participantName, secret } = options; // Check if participant with same participantName exists in the room - const participantExists = await this.participantExists(roomName, participantName); + const participantExists = await this.participantExists(roomId, participantName); if (!refresh && participantExists) { - this.logger.verbose(`Participant ${participantName} already exists in room ${roomName}`); - throw errorParticipantAlreadyExists(participantName, roomName); + 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 ${roomName}`); - throw errorParticipantNotFound(participantName, roomName); + this.logger.verbose(`Participant ${participantName} does not exist in room ${roomId}`); + throw errorParticipantNotFound(participantName, roomId); } - const role = await this.roomService.getRoomSecretRole(roomName, secret); - return this.generateParticipantToken(role, options); + const role = await this.roomService.getRoomSecretRole(roomId, secret); + const token = await this.generateParticipantToken(role, options); + this.logger.verbose(`Participant token generated for room ${roomId}`); + return token; } protected async generateParticipantToken(role: ParticipantRole, options: TokenOptions): Promise { - const permissions = this.getParticipantPermissions(role, options.roomName); + const permissions = this.getParticipantPermissions(role, options.roomId); return this.livekitService.generateToken(options, permissions, role); } - async getParticipant(roomName: string, participantName: string): Promise { + async getParticipant(roomId: string, participantName: string): Promise { this.logger.verbose(`Fetching participant ${participantName}`); - return this.livekitService.getParticipant(roomName, participantName); + return this.livekitService.getParticipant(roomId, participantName); } - async participantExists(roomName: string, participantName: string): Promise { - this.logger.verbose(`Checking if participant ${participantName} exists in room ${roomName}`); + async participantExists(roomId: string, participantName: string): Promise { + this.logger.verbose(`Checking if participant ${participantName} exists in room ${roomId}`); try { - const participant = await this.getParticipant(roomName, participantName); + const participant = await this.getParticipant(roomId, participantName); return participant !== null; } catch (error) { return false; } } - async deleteParticipant(participantName: string, roomName: string): Promise { - this.logger.verbose(`Deleting participant ${participantName} from room ${roomName}`); + async deleteParticipant(participantName: string, roomId: string): Promise { + this.logger.verbose(`Deleting participant ${participantName} from room ${roomId}`); - return this.livekitService.deleteParticipant(participantName, roomName); + return this.livekitService.deleteParticipant(participantName, roomId); } - getParticipantPermissions(role: ParticipantRole, roomName: string): ParticipantPermissions { + getParticipantPermissions(role: ParticipantRole, roomId: string): ParticipantPermissions { switch (role) { case ParticipantRole.MODERATOR: - return this.generateModeratorPermissions(roomName); + return this.generateModeratorPermissions(roomId); case ParticipantRole.PUBLISHER: - return this.generatePublisherPermissions(roomName); + return this.generatePublisherPermissions(roomId); default: throw new Error(`Role ${role} not supported`); } } - protected generateModeratorPermissions(roomName: string): ParticipantPermissions { + protected generateModeratorPermissions(roomId: string): ParticipantPermissions { return { livekit: { roomCreate: true, @@ -80,7 +82,7 @@ export class ParticipantService { roomList: true, roomRecord: true, roomAdmin: true, - room: roomName, + room: roomId, ingressAdmin: true, canPublish: true, canSubscribe: true, @@ -99,14 +101,14 @@ export class ParticipantService { }; } - protected generatePublisherPermissions(roomName: string): ParticipantPermissions { + protected generatePublisherPermissions(roomId: string): ParticipantPermissions { return { livekit: { roomJoin: true, roomList: true, roomRecord: false, roomAdmin: false, - room: roomName, + room: roomId, ingressAdmin: false, canPublish: true, canSubscribe: true, diff --git a/backend/src/services/preferences/global-preferences-storage.interface.ts b/backend/src/services/preferences/global-preferences-storage.interface.ts index 2b38c54..cce55d1 100644 --- a/backend/src/services/preferences/global-preferences-storage.interface.ts +++ b/backend/src/services/preferences/global-preferences-storage.interface.ts @@ -36,10 +36,10 @@ export interface PreferencesStorage< /** * Retrieves the {@link MeetRoom}. * - * @param roomName - The name of the room to retrieve. + * @param roomId - The name of the room to retrieve. * @returns A promise that resolves to the OpenVidu Room, or null if not found. **/ - getOpenViduRoom(roomName: string): Promise; + getOpenViduRoom(roomId: string): Promise; /** * Saves the OpenVidu Room. @@ -52,8 +52,8 @@ export interface PreferencesStorage< /** * Deletes the OpenVidu Room for a given room name. * - * @param roomName - The name of the room whose should be deleted. + * @param roomId - The name of the room whose should be deleted. * @returns A promise that resolves when the room have been deleted. **/ - deleteOpenViduRoom(roomName: string): Promise; + deleteOpenViduRoom(roomId: string): Promise; } diff --git a/backend/src/services/preferences/global-preferences.service.ts b/backend/src/services/preferences/global-preferences.service.ts index 54bba55..ef6a35c 100644 --- a/backend/src/services/preferences/global-preferences.service.ts +++ b/backend/src/services/preferences/global-preferences.service.ts @@ -3,7 +3,7 @@ * regardless of the underlying storage mechanism. */ -import { AuthMode, AuthType, GlobalPreferences, MeetRoom, RoomPreferences } from '@typings-ce'; +import { AuthMode, AuthType, GlobalPreferences, MeetRoom, MeetRoomPreferences } from '@typings-ce'; import { LoggerService } from '../logger.service.js'; import { PreferencesStorage } from './global-preferences-storage.interface.js'; import { GlobalPreferencesStorageFactory } from './global-preferences.factory.js'; @@ -64,7 +64,7 @@ export class GlobalPreferencesService< } async saveOpenViduRoom(ovRoom: R): Promise { - this.logger.info(`Saving OpenVidu room ${ovRoom.roomName}`); + this.logger.info(`Saving OpenVidu room ${ovRoom.roomId}`); return this.storage.saveOpenViduRoom(ovRoom) as Promise; } @@ -75,27 +75,27 @@ export class GlobalPreferencesService< /** * Retrieves the preferences associated with a specific room. * - * @param roomName - The unique identifier for the room. + * @param roomId - The unique identifier for the room. * @returns A promise that resolves to the room's preferences. * @throws Error if the room preferences are not found. */ - async getOpenViduRoom(roomName: string): Promise { - const openviduRoom = await this.storage.getOpenViduRoom(roomName); + async getOpenViduRoom(roomId: string): Promise { + const openviduRoom = await this.storage.getOpenViduRoom(roomId); if (!openviduRoom) { - this.logger.error(`Room not found for room ${roomName}`); - throw errorRoomNotFound(roomName); + this.logger.error(`Room not found for room ${roomId}`); + throw errorRoomNotFound(roomId); } return openviduRoom as R; } - async deleteOpenViduRoom(roomName: string): Promise { - return this.storage.deleteOpenViduRoom(roomName); + async deleteOpenViduRoom(roomId: string): Promise { + return this.storage.deleteOpenViduRoom(roomId); } - async getOpenViduRoomPreferences(roomName: string): Promise { - const openviduRoom = await this.getOpenViduRoom(roomName); + async getOpenViduRoomPreferences(roomId: string): Promise { + const openviduRoom = await this.getOpenViduRoom(roomId); if (!openviduRoom.preferences) { throw new Error('Room preferences not found'); @@ -109,11 +109,11 @@ export class GlobalPreferencesService< * @param {RoomPreferences} roomPreferences * @returns {Promise} */ - async updateOpenViduRoomPreferences(roomName: string, roomPreferences: RoomPreferences): Promise { + async updateOpenViduRoomPreferences(roomId: string, roomPreferences: MeetRoomPreferences): Promise { // TODO: Move validation to the controller layer this.validateRoomPreferences(roomPreferences); - const openviduRoom = await this.getOpenViduRoom(roomName); + const openviduRoom = await this.getOpenViduRoom(roomId); openviduRoom.preferences = roomPreferences; return this.saveOpenViduRoom(openviduRoom); } @@ -122,7 +122,7 @@ export class GlobalPreferencesService< * Validates the room preferences. * @param {RoomPreferences} preferences */ - validateRoomPreferences(preferences: RoomPreferences) { + validateRoomPreferences(preferences: MeetRoomPreferences) { const { recordingPreferences, chatPreferences, virtualBackgroundPreferences } = preferences; if (!recordingPreferences || !chatPreferences || !virtualBackgroundPreferences) { diff --git a/backend/src/services/preferences/s3-preferences-storage.ts b/backend/src/services/preferences/s3-preferences-storage.ts index 0d340ba..46ed2eb 100644 --- a/backend/src/services/preferences/s3-preferences-storage.ts +++ b/backend/src/services/preferences/s3-preferences-storage.ts @@ -80,14 +80,14 @@ export class S3PreferenceStorage< } async saveOpenViduRoom(ovRoom: R): Promise { - const { roomName } = ovRoom; - const s3Path = `${MEET_S3_ROOMS_PREFIX}/${roomName}/${roomName}.json`; + const { roomId } = ovRoom; + const s3Path = `${MEET_S3_ROOMS_PREFIX}/${roomId}/${roomId}.json`; const roomStr = JSON.stringify(ovRoom); const results = await Promise.allSettled([ this.s3Service.saveObject(s3Path, ovRoom), // TODO: Use a key prefix for Redis - this.redisService.set(roomName, roomStr, false) + this.redisService.set(roomId, roomStr, false) ]); const s3Result = results[0]; @@ -102,15 +102,15 @@ export class S3PreferenceStorage< try { await this.s3Service.deleteObject(s3Path); } catch (rollbackError) { - this.logger.error(`Error rolling back S3 save for room ${roomName}: ${rollbackError}`); + this.logger.error(`Error rolling back S3 save for room ${roomId}: ${rollbackError}`); } } if (redisResult.status === 'fulfilled') { try { - await this.redisService.delete(roomName); + await this.redisService.delete(roomId); } catch (rollbackError) { - this.logger.error(`Error rolling back Redis set for room ${roomName}: ${rollbackError}`); + this.logger.error(`Error rolling back Redis set for room ${roomId}: ${rollbackError}`); } } @@ -118,7 +118,7 @@ export class S3PreferenceStorage< const rejectedResult: PromiseRejectedResult = s3Result.status === 'rejected' ? s3Result : (redisResult as PromiseRejectedResult); const error = rejectedResult.reason; - this.handleError(error, `Error saving Room preferences for room ${roomName}`); + this.handleError(error, `Error saving Room preferences for room ${roomId}`); throw error; } @@ -138,16 +138,16 @@ export class S3PreferenceStorage< } // Extract room names from file paths - const roomNamesList = roomFiles.map((file) => this.extractRoomName(file.Key)).filter(Boolean) as string[]; + const roomIds = roomFiles.map((file) => this.extractRoomId(file.Key)).filter(Boolean) as string[]; // Fetch room preferences in parallel const rooms = await Promise.all( - roomNamesList.map(async (roomName: string) => { - if (!roomName) return null; + roomIds.map(async (roomId: string) => { + if (!roomId) return null; try { - return await this.getOpenViduRoom(roomName); + return await this.getOpenViduRoom(roomId); } catch (error: any) { - this.logger.warn(`Failed to fetch room "${roomName}": ${error.message}`); + this.logger.warn(`Failed to fetch room "${roomId}": ${error.message}`); return null; } }) @@ -162,13 +162,13 @@ export class S3PreferenceStorage< } /** - * Extracts the room name from the given file path. + * Extracts the room id from the given file path. * Assumes the room name is located one directory before the file name. - * Example: 'path/to/roomName/file.json' -> 'roomName' + * Example: 'path/to/roomId/file.json' -> 'roomId' * @param filePath - The S3 object key representing the file path. * @returns The extracted room name or null if extraction fails. */ - private extractRoomName(filePath?: string): string | null { + private extractRoomId(filePath?: string): string | null { if (!filePath) return null; const parts = filePath.split('/'); @@ -198,14 +198,14 @@ export class S3PreferenceStorage< } } - async deleteOpenViduRoom(roomName: string): Promise { + async deleteOpenViduRoom(roomId: string): Promise { try { await Promise.all([ - this.s3Service.deleteObject(`${MEET_S3_ROOMS_PREFIX}/${roomName}/${roomName}.json`), - this.redisService.delete(roomName) + this.s3Service.deleteObject(`${MEET_S3_ROOMS_PREFIX}/${roomId}/${roomId}.json`), + this.redisService.delete(roomId) ]); } catch (error) { - this.handleError(error, `Error deleting Room preferences for room ${roomName}`); + this.handleError(error, `Error deleting Room preferences for room ${roomId}`); } } diff --git a/backend/src/services/recording.service.ts b/backend/src/services/recording.service.ts index 523f62a..dd1e44a 100644 --- a/backend/src/services/recording.service.ts +++ b/backend/src/services/recording.service.ts @@ -62,7 +62,7 @@ export class RecordingService { let acquiredLock: RedisLock | null = null; try { - const room = await this.roomService.getOpenViduRoom(roomId); + const room = await this.roomService.getMeetRoom(roomId); if (!room) throw errorRoomNotFound(roomId); @@ -364,9 +364,9 @@ export class RecordingService { * and sends it to the OpenVidu Components in the given room. The payload * is adapted to match the expected format for OpenVidu Components. */ - sendRecordingSignalToOpenViduComponents(roomName: string, recordingInfo: MeetRecordingInfo) { + sendRecordingSignalToOpenViduComponents(roomId: string, recordingInfo: MeetRecordingInfo) { const { payload, options } = OpenViduComponentsAdapterHelper.generateRecordingSignal(recordingInfo); - return this.roomService.sendSignal(roomName, payload, options); + return this.roomService.sendSignal(roomId, payload, options); } /** diff --git a/backend/src/services/room.service.ts b/backend/src/services/room.service.ts index e1f343f..e2dca99 100644 --- a/backend/src/services/room.service.ts +++ b/backend/src/services/room.service.ts @@ -10,6 +10,9 @@ import { SystemEventService } from './system-event.service.js'; import { TaskSchedulerService } from './task-scheduler.service.js'; import { errorParticipantUnauthorized } from '../models/error.model.js'; import { OpenViduComponentsAdapterHelper } from '../helpers/index.js'; +import { uid } from 'uid/single'; +import { MEET_NAME_ID } from '../environment.js'; +import ms from 'ms'; /** * Service for managing OpenVidu Meet rooms. @@ -34,24 +37,23 @@ export class RoomService { */ async initialize(): Promise { this.systemEventService.onRedisReady(async () => { - try { - await this.deleteOpenViduExpiredRooms(); - } catch (error) { - this.logger.error('Error deleting OpenVidu expired rooms:', error); - } - - await Promise.all([ - //TODO: Livekit rooms should not be created here. They should be created when a user joins a room. - this.restoreMissingLivekitRooms().catch((error) => - this.logger.error('Error restoring missing rooms:', error) - ), - this.taskSchedulerService.startRoomGarbageCollector(this.deleteExpiredRooms.bind(this)) - ]); + // try { + // await this.deleteOpenViduExpiredRooms(); + // } catch (error) { + // this.logger.error('Error deleting OpenVidu expired rooms:', error); + // } + // await Promise.all([ + // //TODO: Livekit rooms should not be created here. They should be created when a user joins a room. + // this.restoreMissingLivekitRooms().catch((error) => + // this.logger.error('Error restoring missing rooms:', error) + // ), + // this.taskSchedulerService.startRoomGarbageCollector(this.deleteExpiredRooms.bind(this)) + // ]); }); } /** - * Creates an OpenVidu room with the specified options. + * Creates an OpenVidu Meet room with the specified options. * * @param {string} baseUrl - The base URL for the room. * @param {MeetRoomOptions} options - The options for creating the OpenVidu room. @@ -60,16 +62,57 @@ export class RoomService { * @throws {Error} If the room creation fails. * */ - async createRoom(baseUrl: string, roomOptions: MeetRoomOptions): Promise { - const livekitRoom: Room = await this.createLivekitRoom(roomOptions); + async createMeetRoom(baseUrl: string, roomOptions: MeetRoomOptions): Promise { + const { preferences, expirationDate, roomIdPrefix } = roomOptions; + const roomId = roomIdPrefix ? `${roomIdPrefix}-${uid(15)}` : uid(15); - const openviduRoom: MeetRoom = this.generateOpenViduRoom(baseUrl, livekitRoom, roomOptions); + const openviduRoom: MeetRoom = { + roomId, + roomIdPrefix, + creationDate: Date.now(), + // maxParticipants, + expirationDate, + preferences, + moderatorRoomUrl: `${baseUrl}/room/${roomId}?secret=${secureUid(10)}`, + publisherRoomUrl: `${baseUrl}/room/${roomId}?secret=${secureUid(10)}` + }; await this.globalPrefService.saveOpenViduRoom(openviduRoom); return openviduRoom; } + /** + * Creates a LiveKit room for the specified Meet Room. + * + * This method creates a LiveKit room with the specified room name and metadata. + * The metadata includes the room options from the Meet Room. + **/ + async createLivekitRoom(roomId: string): Promise { + const roomExists = await this.livekitService.roomExists(roomId); + + if (roomExists) { + this.logger.verbose(`Room ${roomId} already exists in LiveKit.`); + return this.livekitService.getRoom(roomId); + } + + const meetRoom: MeetRoom = await this.getMeetRoom(roomId); + const livekitRoomOptions: CreateOptions = { + name: roomId, + metadata: JSON.stringify({ + createdBy: MEET_NAME_ID, + roomOptions: MeetRoomHelper.toOpenViduOptions(meetRoom) + }), + emptyTimeout: ms('20s'), + departureTimeout: ms('20s') + // maxParticipants: maxParticipants || undefined, + }; + + const room = await this.livekitService.createRoom(livekitRoomOptions); + this.logger.verbose(`Room ${roomId} created in LiveKit.`); + return room; + } + /** * Retrieves a list of rooms. * @returns A Promise that resolves to an array of {@link MeetRoom} objects. @@ -82,11 +125,11 @@ export class RoomService { /** * Retrieves an OpenVidu room by its name. * - * @param roomName - The name of the room to retrieve. + * @param roomId - The name of the room to retrieve. * @returns A promise that resolves to an {@link MeetRoom} object. */ - async getOpenViduRoom(roomName: string): Promise { - return await this.globalPrefService.getOpenViduRoom(roomName); + async getMeetRoom(roomId: string): Promise { + return await this.globalPrefService.getOpenViduRoom(roomId); } /** @@ -94,19 +137,19 @@ export class RoomService { * * This method deletes rooms from both LiveKit and OpenVidu services. * - * @param roomNames - An array of room names to be deleted. + * @param roomIds - An array of room names to be deleted. * @returns A promise that resolves with an array of successfully deleted room names. */ - async deleteRooms(roomNames: string[]): Promise { + async deleteRooms(roomIds: string[]): Promise { const [openViduResults, livekitResults] = await Promise.all([ - this.deleteOpenViduRooms(roomNames), - Promise.allSettled(roomNames.map((roomName) => this.livekitService.deleteRoom(roomName))) + this.deleteOpenViduRooms(roomIds), + Promise.allSettled(roomIds.map((roomId) => this.livekitService.deleteRoom(roomId))) ]); // Log errors from LiveKit deletions livekitResults.forEach((result, index) => { if (result.status === 'rejected') { - this.logger.error(`Failed to delete LiveKit room "${roomNames[index]}": ${result.reason}`); + this.logger.error(`Failed to delete LiveKit room "${roomIds[index]}": ${result.reason}`); } }); @@ -115,7 +158,7 @@ export class RoomService { livekitResults.forEach((result, index) => { if (result.status === 'fulfilled') { - successfullyDeleted.add(roomNames[index]); + successfullyDeleted.add(roomIds[index]); } }); @@ -125,25 +168,25 @@ export class RoomService { /** * Deletes OpenVidu rooms. * - * @param roomNames - List of room names to delete. + * @param roomIds - List of room names to delete. * @returns A promise that resolves with an array of successfully deleted room names. */ - async deleteOpenViduRooms(roomNames: string[]): Promise { + async deleteOpenViduRooms(roomIds: string[]): Promise { const results = await Promise.allSettled( - roomNames.map((roomName) => this.globalPrefService.deleteOpenViduRoom(roomName)) + roomIds.map((roomId) => this.globalPrefService.deleteOpenViduRoom(roomId)) ); const successfulRooms: string[] = []; results.forEach((result, index) => { if (result.status === 'fulfilled') { - successfulRooms.push(roomNames[index]); + successfulRooms.push(roomIds[index]); } else { - this.logger.error(`Failed to delete OpenVidu room "${roomNames[index]}": ${result.reason}`); + this.logger.error(`Failed to delete OpenVidu room "${roomIds[index]}": ${result.reason}`); } }); - if (successfulRooms.length === roomNames.length) { + if (successfulRooms.length === roomIds.length) { this.logger.verbose('All OpenVidu rooms have been deleted.'); } @@ -151,15 +194,16 @@ export class RoomService { } /** - * Determines the role of a participant in a room based on the provided secret. + * Validates a secret against a room's moderator and publisher secrets and returns the corresponding role. * - * @param room - The OpenVidu room object. - * @param secret - The secret used to identify the participant's role. - * @returns The role of the participant {@link ParticipantRole}. - * @throws Will throw an error if the secret is invalid. + * @param roomId - The unique identifier of the room to check + * @param secret - The secret to validate against the room's moderator and publisher secrets + * @returns A promise that resolves to the participant role (MODERATOR or PUBLISHER) if the secret is valid + * @throws Error if the moderator or publisher secrets cannot be extracted from their URLs + * @throws Error if the provided secret doesn't match any of the room's secrets (unauthorized) */ - async getRoomSecretRole(roomName: string, secret: string): Promise { - const room = await this.getOpenViduRoom(roomName); + async getRoomSecretRole(roomId: string, secret: string): Promise { + const room = await this.getMeetRoom(roomId); const { moderatorRoomUrl, publisherRoomUrl } = room; const extractSecret = (urlString: string, type: string): string => { @@ -180,13 +224,13 @@ export class RoomService { case publisherSecret: return ParticipantRole.PUBLISHER; default: - throw errorParticipantUnauthorized(roomName); + throw errorParticipantUnauthorized(roomId); } } - async sendRoomStatusSignalToOpenViduComponents(roomName: string, participantSid: string) { + async sendRoomStatusSignalToOpenViduComponents(roomId: string, participantSid: string) { // Check if recording is started in the room - const activeEgressArray = await this.livekitService.getActiveEgress(roomName); + const activeEgressArray = await this.livekitService.getActiveEgress(roomId); const isRecordingStarted = activeEgressArray.length > 0; // Skip if recording is not started @@ -200,61 +244,20 @@ export class RoomService { participantSid ); - await this.sendSignal(roomName, payload, options); + await this.sendSignal(roomId, payload, options); } /** * Sends a signal to participants in a specified room. * - * @param roomName - The name of the room where the signal will be sent. + * @param roomId - The name of the room where the signal will be sent. * @param rawData - The raw data to be sent as the signal. * @param options - Options for sending the data, including the topic and destination identities. * @returns A promise that resolves when the signal has been sent. */ - async sendSignal(roomName: string, rawData: any, options: SendDataOptions): Promise { - this.logger.verbose(`Notifying participants in room ${roomName}: "${options.topic}".`); - this.livekitService.sendData(roomName, rawData, options); - } - - /** - * Creates a Livekit room with the specified options. - * - * @param roomOptions - The options for creating the room. - * @returns A promise that resolves to the created room. - */ - protected async createLivekitRoom(roomOptions: MeetRoomOptions): Promise { - const livekitRoomOptions: CreateOptions = MeetRoomHelper.generateLivekitRoomOptions(roomOptions); - - return this.livekitService.createRoom(livekitRoomOptions); - } - - /** - * Converts a LiveKit room to an OpenVidu room. - * - * @param livekitRoom - The LiveKit room object containing metadata, name, and creation time. - * @param roomOptions - Options for the OpenVidu room including preferences and end date. - * @returns The converted OpenVidu room object. - * @throws Will throw an error if metadata is not found in the LiveKit room. - */ - protected generateOpenViduRoom( - baseUrl: string, - livekitRoom: Room, - roomOptions: MeetRoomOptions - ): MeetRoom { - const { name: roomName, creationTime } = livekitRoom; - const { preferences, expirationDate, roomNamePrefix, maxParticipants } = roomOptions; - - const openviduRoom: MeetRoom = { - roomName, - roomNamePrefix, - creationDate: Number(creationTime) * 1000, - maxParticipants, - expirationDate, - moderatorRoomUrl: `${baseUrl}/room/${roomName}?secret=${secureUid(10)}`, - publisherRoomUrl: `${baseUrl}/room/${roomName}?secret=${secureUid(10)}`, - preferences - }; - return openviduRoom; + async sendSignal(roomId: string, rawData: Record, options: SendDataOptions): Promise { + this.logger.verbose(`Notifying participants in room ${roomId}: "${options.topic}".`); + this.livekitService.sendData(roomId, rawData, options); } /** @@ -274,7 +277,7 @@ export class RoomService { } const livekitResults = await Promise.allSettled( - ovExpiredRooms.map((roomName) => this.livekitService.deleteRoom(roomName)) + ovExpiredRooms.map((roomId) => this.livekitService.deleteRoom(roomId)) ); const successfulRooms: string[] = []; @@ -309,7 +312,7 @@ export class RoomService { const rooms = await this.listOpenViduRooms(); const expiredRooms = rooms .filter((room) => room.expirationDate && room.expirationDate < now) - .map((room) => room.roomName); + .map((room) => room.roomId); if (expiredRooms.length === 0) { this.logger.verbose('No OpenVidu expired rooms to delete.'); @@ -353,7 +356,7 @@ export class RoomService { } const missingRooms: MeetRoom[] = ovRooms.filter( - (ovRoom) => !lkRooms.some((room) => room.name === ovRoom.roomName) + (ovRoom) => !lkRooms.some((room) => room.name === ovRoom.roomId) ); if (missingRooms.length === 0) { @@ -364,17 +367,17 @@ export class RoomService { this.logger.info(`Restoring ${missingRooms.length} missing rooms`); const creationResults = await Promise.allSettled( - missingRooms.map((ovRoom) => { - this.logger.debug(`Restoring room: ${ovRoom.roomName}`); - this.createLivekitRoom(ovRoom); + missingRooms.map(({ roomId }: MeetRoom) => { + this.logger.debug(`Restoring room: ${roomId}`); + this.createLivekitRoom(roomId); }) ); creationResults.forEach((result, index) => { if (result.status === 'rejected') { - this.logger.error(`Failed to restore room "${missingRooms[index].roomName}": ${result.reason}`); + this.logger.error(`Failed to restore room "${missingRooms[index].roomId}": ${result.reason}`); } else { - this.logger.info(`Restored room "${missingRooms[index].roomName}"`); + this.logger.info(`Restored room "${missingRooms[index].roomId}"`); } }); }