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:
parent
aad16bc28c
commit
67b3426c85
@ -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' });
|
||||
|
||||
@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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' });
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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
|
||||
});
|
||||
};
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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);
|
||||
};
|
||||
|
||||
@ -46,4 +46,4 @@ preferencesRouter.get(
|
||||
'/appearance',
|
||||
withAuth(apiKeyValidator, tokenAndRoleValidator(UserRole.ADMIN)),
|
||||
appearancePrefCtrl.getAppearancePreferences
|
||||
);
|
||||
);
|
||||
@ -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);
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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>;
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -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}"`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user