backend: Refactor room-related terminology from 'roomName' to 'roomId' across routes, middlewares, and services

Updated the Livekit room life cycle
This commit is contained in:
Carlos Santos 2025-04-01 17:27:05 +02:00
parent aad16bc28c
commit 67b3426c85
20 changed files with 308 additions and 293 deletions

View File

@ -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' });

View File

@ -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);
}
};

View File

@ -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);
}
};

View File

@ -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),

View File

@ -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));
}
}

View File

@ -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;

View File

@ -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' });

View File

@ -3,13 +3,13 @@ import { Request, Response, NextFunction } from 'express';
import { z } from 'zod';
const ParticipantTokenRequestSchema: z.ZodType<TokenOptions> = 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) => {

View File

@ -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<RecordingPreferences> = z.object({
const RecordingPreferencesSchema: z.ZodType<MeetRecordingPreferences> = z.object({
enabled: z.boolean()
});
const ChatPreferencesSchema: z.ZodType<ChatPreferences> = z.object({
const ChatPreferencesSchema: z.ZodType<MeetChatPreferences> = z.object({
enabled: z.boolean()
});
const VirtualBackgroundPreferencesSchema: z.ZodType<VirtualBackgroundPreferences> = z.object({
const VirtualBackgroundPreferencesSchema: z.ZodType<MeetVirtualBackgroundPreferences> = z.object({
enabled: z.boolean()
});
const RoomPreferencesSchema: z.ZodType<RoomPreferences> = z.object({
const RoomPreferencesSchema: z.ZodType<MeetRoomPreferences> = z.object({
recordingPreferences: RecordingPreferencesSchema,
chatPreferences: ChatPreferencesSchema,
virtualBackgroundPreferences: VirtualBackgroundPreferencesSchema
@ -31,44 +31,41 @@ const RoomRequestOptionsSchema: z.ZodType<MeetRoomOptions> = 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
});
};

View File

@ -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;

View File

@ -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);
};

View File

@ -46,4 +46,4 @@ preferencesRouter.get(
'/appearance',
withAuth(apiKeyValidator, tokenAndRoleValidator(UserRole.ADMIN)),
appearancePrefCtrl.getAppearancePreferences
);
);

View File

@ -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);

View File

@ -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<boolean> {
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<Room> {
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<ParticipantInfo> {
try {
return await this.roomClient.getParticipant(roomName, participantName);
@ -132,8 +163,8 @@ export class LiveKitService {
permissions: ParticipantPermissions,
role: ParticipantRole
): Promise<string> {
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,

View File

@ -15,64 +15,66 @@ export class ParticipantService {
) {}
async generateOrRefreshParticipantToken(options: TokenOptions, refresh = false): Promise<string> {
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<string> {
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<ParticipantInfo | null> {
async getParticipant(roomId: string, participantName: string): Promise<ParticipantInfo | null> {
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<boolean> {
this.logger.verbose(`Checking if participant ${participantName} exists in room ${roomName}`);
async participantExists(roomId: string, participantName: string): Promise<boolean> {
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<void> {
this.logger.verbose(`Deleting participant ${participantName} from room ${roomName}`);
async deleteParticipant(participantName: string, roomId: string): Promise<void> {
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,

View File

@ -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<R | null>;
getOpenViduRoom(roomId: string): Promise<R | null>;
/**
* 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<void>;
deleteOpenViduRoom(roomId: string): Promise<void>;
}

View File

@ -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<R> {
this.logger.info(`Saving OpenVidu room ${ovRoom.roomName}`);
this.logger.info(`Saving OpenVidu room ${ovRoom.roomId}`);
return this.storage.saveOpenViduRoom(ovRoom) as Promise<R>;
}
@ -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<R> {
const openviduRoom = await this.storage.getOpenViduRoom(roomName);
async getOpenViduRoom(roomId: string): Promise<R> {
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<void> {
return this.storage.deleteOpenViduRoom(roomName);
async deleteOpenViduRoom(roomId: string): Promise<void> {
return this.storage.deleteOpenViduRoom(roomId);
}
async getOpenViduRoomPreferences(roomName: string): Promise<RoomPreferences> {
const openviduRoom = await this.getOpenViduRoom(roomName);
async getOpenViduRoomPreferences(roomId: string): Promise<MeetRoomPreferences> {
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<GlobalPreferences>}
*/
async updateOpenViduRoomPreferences(roomName: string, roomPreferences: RoomPreferences): Promise<R> {
async updateOpenViduRoomPreferences(roomId: string, roomPreferences: MeetRoomPreferences): Promise<R> {
// 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) {

View File

@ -80,14 +80,14 @@ export class S3PreferenceStorage<
}
async saveOpenViduRoom(ovRoom: R): Promise<R> {
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<void> {
async deleteOpenViduRoom(roomId: string): Promise<void> {
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}`);
}
}

View File

@ -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);
}
/**

View File

@ -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<void> {
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<MeetRoom> {
const livekitRoom: Room = await this.createLivekitRoom(roomOptions);
async createMeetRoom(baseUrl: string, roomOptions: MeetRoomOptions): Promise<MeetRoom> {
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<Room> {
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<MeetRoom> {
return await this.globalPrefService.getOpenViduRoom(roomName);
async getMeetRoom(roomId: string): Promise<MeetRoom> {
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<string[]> {
async deleteRooms(roomIds: string[]): Promise<string[]> {
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<string[]> {
async deleteOpenViduRooms(roomIds: string[]): Promise<string[]> {
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<ParticipantRole> {
const room = await this.getOpenViduRoom(roomName);
async getRoomSecretRole(roomId: string, secret: string): Promise<ParticipantRole> {
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<void> {
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<Room> {
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<string, unknown>, options: SendDataOptions): Promise<void> {
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}"`);
}
});
}