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);
|
const logger = container.get(LoggerService);
|
||||||
|
|
||||||
logger.verbose(`Updating room preferences: ${JSON.stringify(req.body)}`);
|
logger.verbose(`Updating room preferences: ${JSON.stringify(req.body)}`);
|
||||||
const { roomName, roomPreferences } = req.body;
|
const { roomId, roomPreferences } = req.body;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const preferenceService = container.get(GlobalPreferencesService);
|
const preferenceService = container.get(GlobalPreferencesService);
|
||||||
preferenceService.validateRoomPreferences(roomPreferences);
|
preferenceService.validateRoomPreferences(roomPreferences);
|
||||||
|
|
||||||
const savedPreferences = await preferenceService.updateOpenViduRoomPreferences(roomName, roomPreferences);
|
const savedPreferences = await preferenceService.updateOpenViduRoomPreferences(roomId, roomPreferences);
|
||||||
|
|
||||||
return res
|
return res
|
||||||
.status(200)
|
.status(200)
|
||||||
@ -34,9 +34,9 @@ export const getRoomPreferences = async (req: Request, res: Response) => {
|
|||||||
const logger = container.get(LoggerService);
|
const logger = container.get(LoggerService);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const roomName = req.params.roomName;
|
const { roomId } = req.params;
|
||||||
const preferenceService = container.get(GlobalPreferencesService);
|
const preferenceService = container.get(GlobalPreferencesService);
|
||||||
const preferences = await preferenceService.getOpenViduRoomPreferences(roomName);
|
const preferences = await preferenceService.getOpenViduRoomPreferences(roomId);
|
||||||
|
|
||||||
if (!preferences) {
|
if (!preferences) {
|
||||||
return res.status(404).json({ message: 'Room preferences not found' });
|
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 { MEET_PARTICIPANT_TOKEN_EXPIRATION, PARTICIPANT_TOKEN_COOKIE_NAME } from '../environment.js';
|
||||||
import { getCookieOptions } from '../utils/cookie-utils.js';
|
import { getCookieOptions } from '../utils/cookie-utils.js';
|
||||||
import { TokenService } from '../services/token.service.js';
|
import { TokenService } from '../services/token.service.js';
|
||||||
|
import { RoomService } from '../services/room.service.js';
|
||||||
|
|
||||||
export const generateParticipantToken = async (req: Request, res: Response) => {
|
export const generateParticipantToken = async (req: Request, res: Response) => {
|
||||||
const logger = container.get(LoggerService);
|
const logger = container.get(LoggerService);
|
||||||
const tokenOptions: TokenOptions = req.body;
|
|
||||||
const { roomName } = tokenOptions;
|
|
||||||
const participantService = container.get(ParticipantService);
|
const participantService = container.get(ParticipantService);
|
||||||
|
const roomService = container.get(RoomService);
|
||||||
|
const tokenOptions: TokenOptions = req.body;
|
||||||
|
const { roomId } = tokenOptions;
|
||||||
|
|
||||||
try {
|
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);
|
const token = await participantService.generateOrRefreshParticipantToken(tokenOptions);
|
||||||
|
|
||||||
res.cookie(PARTICIPANT_TOKEN_COOKIE_NAME, token, getCookieOptions('/', MEET_PARTICIPANT_TOKEN_EXPIRATION));
|
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 });
|
return res.status(200).json({ token });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Error generating participant token for room: ${roomName}`);
|
logger.error(`Error generating participant token for room: ${roomId}`);
|
||||||
return handleError(res, error);
|
return handleError(res, error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -47,18 +49,18 @@ export const refreshParticipantToken = async (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const tokenOptions: TokenOptions = req.body;
|
const tokenOptions: TokenOptions = req.body;
|
||||||
const { roomName } = tokenOptions;
|
const { roomId } = tokenOptions;
|
||||||
const participantService = container.get(ParticipantService);
|
const participantService = container.get(ParticipantService);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
logger.verbose(`Refreshing participant token for room ${roomName}`);
|
logger.verbose(`Refreshing participant token for room ${roomId}`);
|
||||||
const token = await participantService.generateOrRefreshParticipantToken(tokenOptions, true);
|
const token = await participantService.generateOrRefreshParticipantToken(tokenOptions, true);
|
||||||
|
|
||||||
res.cookie(PARTICIPANT_TOKEN_COOKIE_NAME, token, getCookieOptions('/', MEET_PARTICIPANT_TOKEN_EXPIRATION));
|
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 });
|
return res.status(200).json({ token });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Error refreshing participant token for room: ${roomName}`);
|
logger.error(`Error refreshing participant token for room: ${roomId}`);
|
||||||
return handleError(res, error);
|
return handleError(res, error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -67,13 +69,13 @@ export const deleteParticipant = async (req: Request, res: Response) => {
|
|||||||
const logger = container.get(LoggerService);
|
const logger = container.get(LoggerService);
|
||||||
const participantService = container.get(ParticipantService);
|
const participantService = container.get(ParticipantService);
|
||||||
const { participantName } = req.params;
|
const { participantName } = req.params;
|
||||||
const roomName: string = req.query.roomName as string;
|
const roomId: string = req.query.roomId as string;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await participantService.deleteParticipant(participantName, roomName);
|
await participantService.deleteParticipant(participantName, roomId);
|
||||||
res.status(200).json({ message: 'Participant deleted' });
|
res.status(200).json({ message: 'Participant deleted' });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Error deleting participant from room: ${roomName}`);
|
logger.error(`Error deleting participant from room: ${roomId}`);
|
||||||
return handleError(res, error);
|
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)}'`);
|
logger.verbose(`Creating room with options '${JSON.stringify(options)}'`);
|
||||||
const baseUrl = `${req.protocol}://${req.get('host')}`;
|
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);
|
return res.status(200).json(room);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Error creating room with options '${JSON.stringify(options)}'`);
|
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) => {
|
export const getRoom = async (req: Request, res: Response) => {
|
||||||
const logger = container.get(LoggerService);
|
const logger = container.get(LoggerService);
|
||||||
|
|
||||||
const { roomName } = req.params;
|
const { roomId } = req.params;
|
||||||
const fields = req.query.fields as string[] | undefined;
|
const fields = req.query.fields as string[] | undefined;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
logger.verbose(`Getting room with id '${roomName}'`);
|
logger.verbose(`Getting room with id '${roomId}'`);
|
||||||
|
|
||||||
const roomService = container.get(RoomService);
|
const roomService = container.get(RoomService);
|
||||||
const room = await roomService.getOpenViduRoom(roomName);
|
const room = await roomService.getMeetRoom(roomId);
|
||||||
|
|
||||||
if (fields && fields.length > 0) {
|
if (fields && fields.length > 0) {
|
||||||
const filteredRoom = filterObjectFields(room, fields);
|
const filteredRoom = filterObjectFields(room, fields);
|
||||||
@ -63,7 +63,7 @@ export const getRoom = async (req: Request, res: Response) => {
|
|||||||
|
|
||||||
return res.status(200).json(room);
|
return res.status(200).json(room);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Error getting room with id '${roomName}'`);
|
logger.error(`Error getting room with id '${roomId}'`);
|
||||||
handleError(res, error);
|
handleError(res, error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -72,14 +72,14 @@ export const deleteRooms = async (req: Request, res: Response) => {
|
|||||||
const logger = container.get(LoggerService);
|
const logger = container.get(LoggerService);
|
||||||
const roomService = container.get(RoomService);
|
const roomService = container.get(RoomService);
|
||||||
|
|
||||||
const { roomName } = req.params;
|
const { roomId } = req.params;
|
||||||
const { roomNames } = req.body;
|
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) {
|
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 {
|
try {
|
||||||
@ -98,16 +98,16 @@ export const getParticipantRole = async (req: Request, res: Response) => {
|
|||||||
const logger = container.get(LoggerService);
|
const logger = container.get(LoggerService);
|
||||||
const roomService = container.get(RoomService);
|
const roomService = container.get(RoomService);
|
||||||
|
|
||||||
const { roomName } = req.params;
|
const { roomId } = req.params;
|
||||||
const { secret } = req.query as { secret: string };
|
const { secret } = req.query as { secret: string };
|
||||||
|
|
||||||
try {
|
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);
|
return res.status(200).json(role);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Error getting participant role for room '${roomName}'`);
|
logger.error(`Error getting participant role for room '${roomId}'`);
|
||||||
handleError(res, error);
|
handleError(res, error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -45,7 +45,7 @@ export class OpenViduComponentsAdapterHelper {
|
|||||||
private static parseRecordingInfoToOpenViduComponents(info: MeetRecordingInfo) {
|
private static parseRecordingInfoToOpenViduComponents(info: MeetRecordingInfo) {
|
||||||
return {
|
return {
|
||||||
id: info.recordingId,
|
id: info.recordingId,
|
||||||
roomName: info.details ?? '',
|
roomName: info.roomId,
|
||||||
roomId: info.roomId,
|
roomId: info.roomId,
|
||||||
// outputMode: info.outputMode,
|
// outputMode: info.outputMode,
|
||||||
status: this.mapRecordingStatus(info.status),
|
status: this.mapRecordingStatus(info.status),
|
||||||
|
|||||||
@ -1,7 +1,4 @@
|
|||||||
import { MeetRoom, MeetRoomOptions } from '@typings-ce';
|
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 {
|
export class MeetRoomHelper {
|
||||||
private constructor() {
|
private constructor() {
|
||||||
@ -17,43 +14,9 @@ export class MeetRoomHelper {
|
|||||||
static toOpenViduOptions(room: MeetRoom): MeetRoomOptions {
|
static toOpenViduOptions(room: MeetRoom): MeetRoomOptions {
|
||||||
return {
|
return {
|
||||||
expirationDate: room.expirationDate,
|
expirationDate: room.expirationDate,
|
||||||
maxParticipants: room.maxParticipants,
|
// maxParticipants: room.maxParticipants,
|
||||||
preferences: room.preferences,
|
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;
|
let role: ParticipantRole;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { roomName, secret } = req.body as TokenOptions;
|
const { roomId, secret } = req.body as TokenOptions;
|
||||||
role = await roomService.getRoomSecretRole(roomName, secret);
|
role = await roomService.getRoomSecretRole(roomId, secret);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error getting room secret role', error);
|
logger.error('Error getting room secret role', error);
|
||||||
return res.status(500).json({ message: 'Internal server 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) => {
|
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;
|
const payload = req.session?.tokenClaims;
|
||||||
|
|
||||||
if (!payload) {
|
if (!payload) {
|
||||||
return res.status(403).json({ message: 'Insufficient permissions to access this resource' });
|
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 metadata = JSON.parse(payload.metadata || '{}');
|
||||||
const role = metadata.role as ParticipantRole;
|
const role = metadata.role as ParticipantRole;
|
||||||
|
|
||||||
|
|||||||
@ -22,7 +22,7 @@ export const withRecordingEnabled = async (req: Request, res: Response, next: Ne
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const roomService = container.get(RoomService);
|
const roomService = container.get(RoomService);
|
||||||
room = await roomService.getOpenViduRoom(roomId);
|
room = await roomService.getMeetRoom(roomId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error checking recording preferences:' + error);
|
logger.error('Error checking recording preferences:' + error);
|
||||||
return res.status(403).json({ message: 'Recording is disabled in this room' });
|
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';
|
import { z } from 'zod';
|
||||||
|
|
||||||
const ParticipantTokenRequestSchema: z.ZodType<TokenOptions> = z.object({
|
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'),
|
participantName: z.string().nonempty('Participant name is required'),
|
||||||
secret: z.string().nonempty('Secret is required')
|
secret: z.string().nonempty('Secret is required')
|
||||||
});
|
});
|
||||||
|
|
||||||
const DeleteParticipantSchema = z.object({
|
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) => {
|
export const validateParticipantTokenRequest = (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
|||||||
@ -1,26 +1,26 @@
|
|||||||
import {
|
import {
|
||||||
ChatPreferences,
|
MeetChatPreferences,
|
||||||
MeetRoomOptions,
|
MeetRoomOptions,
|
||||||
RecordingPreferences,
|
MeetRecordingPreferences,
|
||||||
RoomPreferences,
|
MeetRoomPreferences,
|
||||||
VirtualBackgroundPreferences
|
MeetVirtualBackgroundPreferences
|
||||||
} from '@typings-ce';
|
} from '@typings-ce';
|
||||||
import { Request, Response, NextFunction } from 'express';
|
import { Request, Response, NextFunction } from 'express';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
const RecordingPreferencesSchema: z.ZodType<RecordingPreferences> = z.object({
|
const RecordingPreferencesSchema: z.ZodType<MeetRecordingPreferences> = z.object({
|
||||||
enabled: z.boolean()
|
enabled: z.boolean()
|
||||||
});
|
});
|
||||||
|
|
||||||
const ChatPreferencesSchema: z.ZodType<ChatPreferences> = z.object({
|
const ChatPreferencesSchema: z.ZodType<MeetChatPreferences> = z.object({
|
||||||
enabled: z.boolean()
|
enabled: z.boolean()
|
||||||
});
|
});
|
||||||
|
|
||||||
const VirtualBackgroundPreferencesSchema: z.ZodType<VirtualBackgroundPreferences> = z.object({
|
const VirtualBackgroundPreferencesSchema: z.ZodType<MeetVirtualBackgroundPreferences> = z.object({
|
||||||
enabled: z.boolean()
|
enabled: z.boolean()
|
||||||
});
|
});
|
||||||
|
|
||||||
const RoomPreferencesSchema: z.ZodType<RoomPreferences> = z.object({
|
const RoomPreferencesSchema: z.ZodType<MeetRoomPreferences> = z.object({
|
||||||
recordingPreferences: RecordingPreferencesSchema,
|
recordingPreferences: RecordingPreferencesSchema,
|
||||||
chatPreferences: ChatPreferencesSchema,
|
chatPreferences: ChatPreferencesSchema,
|
||||||
virtualBackgroundPreferences: VirtualBackgroundPreferencesSchema
|
virtualBackgroundPreferences: VirtualBackgroundPreferencesSchema
|
||||||
@ -31,44 +31,41 @@ const RoomRequestOptionsSchema: z.ZodType<MeetRoomOptions> = z.object({
|
|||||||
.number()
|
.number()
|
||||||
.positive('Expiration date must be a positive integer')
|
.positive('Expiration date must be a positive integer')
|
||||||
.min(Date.now(), 'Expiration date must be in the future'),
|
.min(Date.now(), 'Expiration date must be in the future'),
|
||||||
roomNamePrefix: z
|
roomIdPrefix: z
|
||||||
.string()
|
.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()
|
.optional()
|
||||||
.default(''),
|
.default(''),
|
||||||
preferences: RoomPreferencesSchema.optional().default({
|
preferences: RoomPreferencesSchema.optional().default({
|
||||||
recordingPreferences: { enabled: true },
|
recordingPreferences: { enabled: true },
|
||||||
chatPreferences: { enabled: true },
|
chatPreferences: { enabled: true },
|
||||||
virtualBackgroundPreferences: { enabled: true }
|
virtualBackgroundPreferences: { enabled: true }
|
||||||
}),
|
})
|
||||||
maxParticipants: z
|
// maxParticipants: z
|
||||||
.number()
|
// .number()
|
||||||
.positive('Max participants must be a positive integer')
|
// .positive('Max participants must be a positive integer')
|
||||||
.nullable()
|
// .nullable()
|
||||||
.optional()
|
// .optional()
|
||||||
.default(null)
|
// .default(null)
|
||||||
});
|
});
|
||||||
|
|
||||||
const GetParticipantRoleSchema = z.object({
|
const GetParticipantRoleSchema = z.object({
|
||||||
secret: z.string()
|
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);
|
const { success, error, data } = RoomRequestOptionsSchema.safeParse(req.body);
|
||||||
|
|
||||||
if (!success) {
|
if (!success) {
|
||||||
const errors = error.errors.map((error) => ({
|
return rejectRequest(res, 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
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
req.body = data;
|
req.body = data;
|
||||||
@ -105,3 +102,16 @@ export const validateGetParticipantRoleRequest = (req: Request, res: Response, n
|
|||||||
req.query = data;
|
req.query = data;
|
||||||
next();
|
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.
|
* - If the user is not a moderator, access is denied.
|
||||||
*/
|
*/
|
||||||
export const configureRoomAuthorization = async (req: Request, res: Response, next: NextFunction) => {
|
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;
|
const payload = req.session?.tokenClaims;
|
||||||
|
|
||||||
// If there is no token, the user is admin or it is invoked using the API key
|
// 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();
|
return next();
|
||||||
}
|
}
|
||||||
|
|
||||||
const sameRoom = payload.video?.room === roomName;
|
const sameRoom = payload.video?.room === roomId;
|
||||||
const metadata = JSON.parse(payload.metadata || '{}');
|
const metadata = JSON.parse(payload.metadata || '{}');
|
||||||
const role = metadata.role as ParticipantRole;
|
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);
|
return new OpenViduMeetError('Recording Error', `Recording '${recordingId}' cannot be stopped while starting`, 409);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const errorRecordingAlreadyStarted = (roomName: string): OpenViduMeetError => {
|
export const errorRecordingAlreadyStarted = (roomId: string): OpenViduMeetError => {
|
||||||
return new OpenViduMeetError('Recording Error', `The room '${roomName}' is already being recorded`, 409);
|
return new OpenViduMeetError('Recording Error', `The room '${roomId}' is already being recorded`, 409);
|
||||||
};
|
};
|
||||||
|
|
||||||
const isMatchingError = (error: OpenViduMeetError, originalError: OpenViduMeetError): boolean => {
|
const isMatchingError = (error: OpenViduMeetError, originalError: OpenViduMeetError): boolean => {
|
||||||
@ -100,24 +100,24 @@ export const isErrorRecordingCannotBeStoppedWhileStarting = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Room errors
|
// Room errors
|
||||||
export const errorRoomNotFound = (roomName: string): OpenViduMeetError => {
|
export const errorRoomNotFound = (roomId: string): OpenViduMeetError => {
|
||||||
return new OpenViduMeetError('Room Error', `The room '${roomName}' does not exist`, 404);
|
return new OpenViduMeetError('Room Error', `The room '${roomId}' does not exist`, 404);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Participant errors
|
// Participant errors
|
||||||
|
|
||||||
export const errorParticipantUnauthorized = (roomName: string): OpenViduMeetError => {
|
export const errorParticipantUnauthorized = (roomId: string): OpenViduMeetError => {
|
||||||
return new OpenViduMeetError(
|
return new OpenViduMeetError(
|
||||||
'Participant Error',
|
'Participant Error',
|
||||||
`Unauthorized generating token with received credentials in room '${roomName}'`,
|
`Unauthorized generating token with received credentials in room '${roomId}'`,
|
||||||
406
|
406
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const errorParticipantNotFound = (participantName: string, roomName: string): OpenViduMeetError => {
|
export const errorParticipantNotFound = (participantName: string, roomId: string): OpenViduMeetError => {
|
||||||
return new OpenViduMeetError('Participant Error', `'${participantName}' not found in room '${roomName}'`, 404);
|
return new OpenViduMeetError('Participant Error', `'${participantName}' not found in room '${roomId}'`, 404);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const errorParticipantAlreadyExists = (participantName: string, roomName: string): OpenViduMeetError => {
|
export const errorParticipantAlreadyExists = (participantName: string, roomId: string): OpenViduMeetError => {
|
||||||
return new OpenViduMeetError('Room Error', `'${participantName}' already exists in room in ${roomName}`, 409);
|
return new OpenViduMeetError('Room Error', `'${participantName}' already exists in room in ${roomId}`, 409);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -46,4 +46,4 @@ preferencesRouter.get(
|
|||||||
'/appearance',
|
'/appearance',
|
||||||
withAuth(apiKeyValidator, tokenAndRoleValidator(UserRole.ADMIN)),
|
withAuth(apiKeyValidator, tokenAndRoleValidator(UserRole.ADMIN)),
|
||||||
appearancePrefCtrl.getAppearancePreferences
|
appearancePrefCtrl.getAppearancePreferences
|
||||||
);
|
);
|
||||||
@ -5,22 +5,26 @@ import {
|
|||||||
withAuth,
|
withAuth,
|
||||||
tokenAndRoleValidator,
|
tokenAndRoleValidator,
|
||||||
apiKeyValidator,
|
apiKeyValidator,
|
||||||
participantTokenValidator
|
participantTokenValidator,
|
||||||
} from '../middlewares/auth.middleware.js';
|
|
||||||
import {
|
|
||||||
validateGetParticipantRoleRequest,
|
validateGetParticipantRoleRequest,
|
||||||
validateGetRoomQueryParams,
|
validateGetRoomQueryParams,
|
||||||
validateRoomRequest
|
withValidRoomOptions,
|
||||||
} from '../middlewares/request-validators/room-validator.middleware.js';
|
configureCreateRoomAuth,
|
||||||
|
configureRoomAuthorization
|
||||||
|
} from '../middlewares/index.js';
|
||||||
|
|
||||||
import { UserRole } from '@typings-ce';
|
import { UserRole } from '@typings-ce';
|
||||||
import { configureCreateRoomAuth, configureRoomAuthorization } from '../middlewares/room.middleware.js';
|
|
||||||
|
|
||||||
export const roomRouter = Router();
|
export const roomRouter = Router();
|
||||||
roomRouter.use(bodyParser.urlencoded({ extended: true }));
|
roomRouter.use(bodyParser.urlencoded({ extended: true }));
|
||||||
roomRouter.use(bodyParser.json());
|
roomRouter.use(bodyParser.json());
|
||||||
|
|
||||||
// Room Routes
|
// Room Routes
|
||||||
roomRouter.post('/', configureCreateRoomAuth, validateRoomRequest, roomCtrl.createRoom);
|
roomRouter.post('/', configureCreateRoomAuth, withValidRoomOptions, roomCtrl.createRoom);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
roomRouter.get(
|
roomRouter.get(
|
||||||
'/',
|
'/',
|
||||||
withAuth(apiKeyValidator, tokenAndRoleValidator(UserRole.ADMIN)),
|
withAuth(apiKeyValidator, tokenAndRoleValidator(UserRole.ADMIN)),
|
||||||
@ -28,13 +32,13 @@ roomRouter.get(
|
|||||||
roomCtrl.getRooms
|
roomCtrl.getRooms
|
||||||
);
|
);
|
||||||
roomRouter.get(
|
roomRouter.get(
|
||||||
'/:roomName',
|
'/:roomId',
|
||||||
withAuth(apiKeyValidator, tokenAndRoleValidator(UserRole.ADMIN), participantTokenValidator),
|
withAuth(apiKeyValidator, tokenAndRoleValidator(UserRole.ADMIN), participantTokenValidator),
|
||||||
configureRoomAuthorization,
|
configureRoomAuthorization,
|
||||||
validateGetRoomQueryParams,
|
validateGetRoomQueryParams,
|
||||||
roomCtrl.getRoom
|
roomCtrl.getRoom
|
||||||
);
|
);
|
||||||
roomRouter.delete('/:roomName', withAuth(apiKeyValidator, tokenAndRoleValidator(UserRole.ADMIN)), roomCtrl.deleteRooms);
|
roomRouter.delete('/:roomId', withAuth(apiKeyValidator, tokenAndRoleValidator(UserRole.ADMIN)), roomCtrl.deleteRooms);
|
||||||
|
|
||||||
// Room preferences
|
// Room preferences
|
||||||
roomRouter.put('/', withAuth(apiKeyValidator, tokenAndRoleValidator(UserRole.ADMIN)), roomCtrl.updateRoomPreferences);
|
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.urlencoded({ extended: true }));
|
||||||
internalRoomRouter.use(bodyParser.json());
|
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,
|
errorLivekitIsNotAvailable,
|
||||||
errorParticipantNotFound,
|
errorParticipantNotFound,
|
||||||
errorRoomNotFound,
|
errorRoomNotFound,
|
||||||
internalError
|
internalError,
|
||||||
|
OpenViduMeetError
|
||||||
} from '../models/error.model.js';
|
} from '../models/error.model.js';
|
||||||
import { ParticipantPermissions, ParticipantRole, TokenOptions } from '@typings-ce';
|
import { ParticipantPermissions, ParticipantRole, TokenOptions } from '@typings-ce';
|
||||||
import { RecordingHelper } from '../helpers/recording.helper.js';
|
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> {
|
async getRoom(roomName: string): Promise<Room> {
|
||||||
let rooms: 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> {
|
async getParticipant(roomName: string, participantName: string): Promise<ParticipantInfo> {
|
||||||
try {
|
try {
|
||||||
return await this.roomClient.getParticipant(roomName, participantName);
|
return await this.roomClient.getParticipant(roomName, participantName);
|
||||||
@ -132,8 +163,8 @@ export class LiveKitService {
|
|||||||
permissions: ParticipantPermissions,
|
permissions: ParticipantPermissions,
|
||||||
role: ParticipantRole
|
role: ParticipantRole
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const { roomName, participantName } = options;
|
const { roomId, participantName } = options;
|
||||||
this.logger.info(`Generating token for ${participantName} in room ${roomName}`);
|
this.logger.info(`Generating token for ${participantName} in room ${roomId}`);
|
||||||
|
|
||||||
const at = new AccessToken(LIVEKIT_API_KEY, LIVEKIT_API_SECRET, {
|
const at = new AccessToken(LIVEKIT_API_KEY, LIVEKIT_API_SECRET, {
|
||||||
identity: participantName,
|
identity: participantName,
|
||||||
|
|||||||
@ -15,64 +15,66 @@ export class ParticipantService {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
async generateOrRefreshParticipantToken(options: TokenOptions, refresh = false): Promise<string> {
|
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
|
// 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) {
|
if (!refresh && participantExists) {
|
||||||
this.logger.verbose(`Participant ${participantName} already exists in room ${roomName}`);
|
this.logger.verbose(`Participant ${participantName} already exists in room ${roomId}`);
|
||||||
throw errorParticipantAlreadyExists(participantName, roomName);
|
throw errorParticipantAlreadyExists(participantName, roomId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (refresh && !participantExists) {
|
if (refresh && !participantExists) {
|
||||||
this.logger.verbose(`Participant ${participantName} does not exist in room ${roomName}`);
|
this.logger.verbose(`Participant ${participantName} does not exist in room ${roomId}`);
|
||||||
throw errorParticipantNotFound(participantName, roomName);
|
throw errorParticipantNotFound(participantName, roomId);
|
||||||
}
|
}
|
||||||
|
|
||||||
const role = await this.roomService.getRoomSecretRole(roomName, secret);
|
const role = await this.roomService.getRoomSecretRole(roomId, secret);
|
||||||
return this.generateParticipantToken(role, options);
|
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> {
|
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);
|
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}`);
|
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> {
|
async participantExists(roomId: string, participantName: string): Promise<boolean> {
|
||||||
this.logger.verbose(`Checking if participant ${participantName} exists in room ${roomName}`);
|
this.logger.verbose(`Checking if participant ${participantName} exists in room ${roomId}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const participant = await this.getParticipant(roomName, participantName);
|
const participant = await this.getParticipant(roomId, participantName);
|
||||||
return participant !== null;
|
return participant !== null;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteParticipant(participantName: string, roomName: string): Promise<void> {
|
async deleteParticipant(participantName: string, roomId: string): Promise<void> {
|
||||||
this.logger.verbose(`Deleting participant ${participantName} from room ${roomName}`);
|
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) {
|
switch (role) {
|
||||||
case ParticipantRole.MODERATOR:
|
case ParticipantRole.MODERATOR:
|
||||||
return this.generateModeratorPermissions(roomName);
|
return this.generateModeratorPermissions(roomId);
|
||||||
case ParticipantRole.PUBLISHER:
|
case ParticipantRole.PUBLISHER:
|
||||||
return this.generatePublisherPermissions(roomName);
|
return this.generatePublisherPermissions(roomId);
|
||||||
default:
|
default:
|
||||||
throw new Error(`Role ${role} not supported`);
|
throw new Error(`Role ${role} not supported`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected generateModeratorPermissions(roomName: string): ParticipantPermissions {
|
protected generateModeratorPermissions(roomId: string): ParticipantPermissions {
|
||||||
return {
|
return {
|
||||||
livekit: {
|
livekit: {
|
||||||
roomCreate: true,
|
roomCreate: true,
|
||||||
@ -80,7 +82,7 @@ export class ParticipantService {
|
|||||||
roomList: true,
|
roomList: true,
|
||||||
roomRecord: true,
|
roomRecord: true,
|
||||||
roomAdmin: true,
|
roomAdmin: true,
|
||||||
room: roomName,
|
room: roomId,
|
||||||
ingressAdmin: true,
|
ingressAdmin: true,
|
||||||
canPublish: true,
|
canPublish: true,
|
||||||
canSubscribe: true,
|
canSubscribe: true,
|
||||||
@ -99,14 +101,14 @@ export class ParticipantService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
protected generatePublisherPermissions(roomName: string): ParticipantPermissions {
|
protected generatePublisherPermissions(roomId: string): ParticipantPermissions {
|
||||||
return {
|
return {
|
||||||
livekit: {
|
livekit: {
|
||||||
roomJoin: true,
|
roomJoin: true,
|
||||||
roomList: true,
|
roomList: true,
|
||||||
roomRecord: false,
|
roomRecord: false,
|
||||||
roomAdmin: false,
|
roomAdmin: false,
|
||||||
room: roomName,
|
room: roomId,
|
||||||
ingressAdmin: false,
|
ingressAdmin: false,
|
||||||
canPublish: true,
|
canPublish: true,
|
||||||
canSubscribe: true,
|
canSubscribe: true,
|
||||||
|
|||||||
@ -36,10 +36,10 @@ export interface PreferencesStorage<
|
|||||||
/**
|
/**
|
||||||
* Retrieves the {@link MeetRoom}.
|
* 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.
|
* @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.
|
* Saves the OpenVidu Room.
|
||||||
@ -52,8 +52,8 @@ export interface PreferencesStorage<
|
|||||||
/**
|
/**
|
||||||
* Deletes the OpenVidu Room for a given room name.
|
* 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.
|
* @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.
|
* 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 { LoggerService } from '../logger.service.js';
|
||||||
import { PreferencesStorage } from './global-preferences-storage.interface.js';
|
import { PreferencesStorage } from './global-preferences-storage.interface.js';
|
||||||
import { GlobalPreferencesStorageFactory } from './global-preferences.factory.js';
|
import { GlobalPreferencesStorageFactory } from './global-preferences.factory.js';
|
||||||
@ -64,7 +64,7 @@ export class GlobalPreferencesService<
|
|||||||
}
|
}
|
||||||
|
|
||||||
async saveOpenViduRoom(ovRoom: R): Promise<R> {
|
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>;
|
return this.storage.saveOpenViduRoom(ovRoom) as Promise<R>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -75,27 +75,27 @@ export class GlobalPreferencesService<
|
|||||||
/**
|
/**
|
||||||
* Retrieves the preferences associated with a specific room.
|
* 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.
|
* @returns A promise that resolves to the room's preferences.
|
||||||
* @throws Error if the room preferences are not found.
|
* @throws Error if the room preferences are not found.
|
||||||
*/
|
*/
|
||||||
async getOpenViduRoom(roomName: string): Promise<R> {
|
async getOpenViduRoom(roomId: string): Promise<R> {
|
||||||
const openviduRoom = await this.storage.getOpenViduRoom(roomName);
|
const openviduRoom = await this.storage.getOpenViduRoom(roomId);
|
||||||
|
|
||||||
if (!openviduRoom) {
|
if (!openviduRoom) {
|
||||||
this.logger.error(`Room not found for room ${roomName}`);
|
this.logger.error(`Room not found for room ${roomId}`);
|
||||||
throw errorRoomNotFound(roomName);
|
throw errorRoomNotFound(roomId);
|
||||||
}
|
}
|
||||||
|
|
||||||
return openviduRoom as R;
|
return openviduRoom as R;
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteOpenViduRoom(roomName: string): Promise<void> {
|
async deleteOpenViduRoom(roomId: string): Promise<void> {
|
||||||
return this.storage.deleteOpenViduRoom(roomName);
|
return this.storage.deleteOpenViduRoom(roomId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getOpenViduRoomPreferences(roomName: string): Promise<RoomPreferences> {
|
async getOpenViduRoomPreferences(roomId: string): Promise<MeetRoomPreferences> {
|
||||||
const openviduRoom = await this.getOpenViduRoom(roomName);
|
const openviduRoom = await this.getOpenViduRoom(roomId);
|
||||||
|
|
||||||
if (!openviduRoom.preferences) {
|
if (!openviduRoom.preferences) {
|
||||||
throw new Error('Room preferences not found');
|
throw new Error('Room preferences not found');
|
||||||
@ -109,11 +109,11 @@ export class GlobalPreferencesService<
|
|||||||
* @param {RoomPreferences} roomPreferences
|
* @param {RoomPreferences} roomPreferences
|
||||||
* @returns {Promise<GlobalPreferences>}
|
* @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
|
// TODO: Move validation to the controller layer
|
||||||
this.validateRoomPreferences(roomPreferences);
|
this.validateRoomPreferences(roomPreferences);
|
||||||
|
|
||||||
const openviduRoom = await this.getOpenViduRoom(roomName);
|
const openviduRoom = await this.getOpenViduRoom(roomId);
|
||||||
openviduRoom.preferences = roomPreferences;
|
openviduRoom.preferences = roomPreferences;
|
||||||
return this.saveOpenViduRoom(openviduRoom);
|
return this.saveOpenViduRoom(openviduRoom);
|
||||||
}
|
}
|
||||||
@ -122,7 +122,7 @@ export class GlobalPreferencesService<
|
|||||||
* Validates the room preferences.
|
* Validates the room preferences.
|
||||||
* @param {RoomPreferences} preferences
|
* @param {RoomPreferences} preferences
|
||||||
*/
|
*/
|
||||||
validateRoomPreferences(preferences: RoomPreferences) {
|
validateRoomPreferences(preferences: MeetRoomPreferences) {
|
||||||
const { recordingPreferences, chatPreferences, virtualBackgroundPreferences } = preferences;
|
const { recordingPreferences, chatPreferences, virtualBackgroundPreferences } = preferences;
|
||||||
|
|
||||||
if (!recordingPreferences || !chatPreferences || !virtualBackgroundPreferences) {
|
if (!recordingPreferences || !chatPreferences || !virtualBackgroundPreferences) {
|
||||||
|
|||||||
@ -80,14 +80,14 @@ export class S3PreferenceStorage<
|
|||||||
}
|
}
|
||||||
|
|
||||||
async saveOpenViduRoom(ovRoom: R): Promise<R> {
|
async saveOpenViduRoom(ovRoom: R): Promise<R> {
|
||||||
const { roomName } = ovRoom;
|
const { roomId } = ovRoom;
|
||||||
const s3Path = `${MEET_S3_ROOMS_PREFIX}/${roomName}/${roomName}.json`;
|
const s3Path = `${MEET_S3_ROOMS_PREFIX}/${roomId}/${roomId}.json`;
|
||||||
const roomStr = JSON.stringify(ovRoom);
|
const roomStr = JSON.stringify(ovRoom);
|
||||||
|
|
||||||
const results = await Promise.allSettled([
|
const results = await Promise.allSettled([
|
||||||
this.s3Service.saveObject(s3Path, ovRoom),
|
this.s3Service.saveObject(s3Path, ovRoom),
|
||||||
// TODO: Use a key prefix for Redis
|
// TODO: Use a key prefix for Redis
|
||||||
this.redisService.set(roomName, roomStr, false)
|
this.redisService.set(roomId, roomStr, false)
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const s3Result = results[0];
|
const s3Result = results[0];
|
||||||
@ -102,15 +102,15 @@ export class S3PreferenceStorage<
|
|||||||
try {
|
try {
|
||||||
await this.s3Service.deleteObject(s3Path);
|
await this.s3Service.deleteObject(s3Path);
|
||||||
} catch (rollbackError) {
|
} 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') {
|
if (redisResult.status === 'fulfilled') {
|
||||||
try {
|
try {
|
||||||
await this.redisService.delete(roomName);
|
await this.redisService.delete(roomId);
|
||||||
} catch (rollbackError) {
|
} 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 =
|
const rejectedResult: PromiseRejectedResult =
|
||||||
s3Result.status === 'rejected' ? s3Result : (redisResult as PromiseRejectedResult);
|
s3Result.status === 'rejected' ? s3Result : (redisResult as PromiseRejectedResult);
|
||||||
const error = rejectedResult.reason;
|
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;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -138,16 +138,16 @@ export class S3PreferenceStorage<
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Extract room names from file paths
|
// 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
|
// Fetch room preferences in parallel
|
||||||
const rooms = await Promise.all(
|
const rooms = await Promise.all(
|
||||||
roomNamesList.map(async (roomName: string) => {
|
roomIds.map(async (roomId: string) => {
|
||||||
if (!roomName) return null;
|
if (!roomId) return null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await this.getOpenViduRoom(roomName);
|
return await this.getOpenViduRoom(roomId);
|
||||||
} catch (error: any) {
|
} 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;
|
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.
|
* 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.
|
* @param filePath - The S3 object key representing the file path.
|
||||||
* @returns The extracted room name or null if extraction fails.
|
* @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;
|
if (!filePath) return null;
|
||||||
|
|
||||||
const parts = filePath.split('/');
|
const parts = filePath.split('/');
|
||||||
@ -198,14 +198,14 @@ export class S3PreferenceStorage<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteOpenViduRoom(roomName: string): Promise<void> {
|
async deleteOpenViduRoom(roomId: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
this.s3Service.deleteObject(`${MEET_S3_ROOMS_PREFIX}/${roomName}/${roomName}.json`),
|
this.s3Service.deleteObject(`${MEET_S3_ROOMS_PREFIX}/${roomId}/${roomId}.json`),
|
||||||
this.redisService.delete(roomName)
|
this.redisService.delete(roomId)
|
||||||
]);
|
]);
|
||||||
} catch (error) {
|
} 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;
|
let acquiredLock: RedisLock | null = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const room = await this.roomService.getOpenViduRoom(roomId);
|
const room = await this.roomService.getMeetRoom(roomId);
|
||||||
|
|
||||||
if (!room) throw errorRoomNotFound(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
|
* and sends it to the OpenVidu Components in the given room. The payload
|
||||||
* is adapted to match the expected format for OpenVidu Components.
|
* 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);
|
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 { TaskSchedulerService } from './task-scheduler.service.js';
|
||||||
import { errorParticipantUnauthorized } from '../models/error.model.js';
|
import { errorParticipantUnauthorized } from '../models/error.model.js';
|
||||||
import { OpenViduComponentsAdapterHelper } from '../helpers/index.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.
|
* Service for managing OpenVidu Meet rooms.
|
||||||
@ -34,24 +37,23 @@ export class RoomService {
|
|||||||
*/
|
*/
|
||||||
async initialize(): Promise<void> {
|
async initialize(): Promise<void> {
|
||||||
this.systemEventService.onRedisReady(async () => {
|
this.systemEventService.onRedisReady(async () => {
|
||||||
try {
|
// try {
|
||||||
await this.deleteOpenViduExpiredRooms();
|
// await this.deleteOpenViduExpiredRooms();
|
||||||
} catch (error) {
|
// } catch (error) {
|
||||||
this.logger.error('Error deleting OpenVidu expired rooms:', error);
|
// this.logger.error('Error deleting OpenVidu expired rooms:', error);
|
||||||
}
|
// }
|
||||||
|
// await Promise.all([
|
||||||
await Promise.all([
|
// //TODO: Livekit rooms should not be created here. They should be created when a user joins a room.
|
||||||
//TODO: Livekit rooms should not be created here. They should be created when a user joins a room.
|
// this.restoreMissingLivekitRooms().catch((error) =>
|
||||||
this.restoreMissingLivekitRooms().catch((error) =>
|
// this.logger.error('Error restoring missing rooms:', error)
|
||||||
this.logger.error('Error restoring missing rooms:', error)
|
// ),
|
||||||
),
|
// this.taskSchedulerService.startRoomGarbageCollector(this.deleteExpiredRooms.bind(this))
|
||||||
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 {string} baseUrl - The base URL for the room.
|
||||||
* @param {MeetRoomOptions} options - The options for creating the OpenVidu 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.
|
* @throws {Error} If the room creation fails.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
async createRoom(baseUrl: string, roomOptions: MeetRoomOptions): Promise<MeetRoom> {
|
async createMeetRoom(baseUrl: string, roomOptions: MeetRoomOptions): Promise<MeetRoom> {
|
||||||
const livekitRoom: Room = await this.createLivekitRoom(roomOptions);
|
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);
|
await this.globalPrefService.saveOpenViduRoom(openviduRoom);
|
||||||
|
|
||||||
return 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.
|
* Retrieves a list of rooms.
|
||||||
* @returns A Promise that resolves to an array of {@link MeetRoom} objects.
|
* @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.
|
* 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.
|
* @returns A promise that resolves to an {@link MeetRoom} object.
|
||||||
*/
|
*/
|
||||||
async getOpenViduRoom(roomName: string): Promise<MeetRoom> {
|
async getMeetRoom(roomId: string): Promise<MeetRoom> {
|
||||||
return await this.globalPrefService.getOpenViduRoom(roomName);
|
return await this.globalPrefService.getOpenViduRoom(roomId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -94,19 +137,19 @@ export class RoomService {
|
|||||||
*
|
*
|
||||||
* This method deletes rooms from both LiveKit and OpenVidu services.
|
* 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.
|
* @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([
|
const [openViduResults, livekitResults] = await Promise.all([
|
||||||
this.deleteOpenViduRooms(roomNames),
|
this.deleteOpenViduRooms(roomIds),
|
||||||
Promise.allSettled(roomNames.map((roomName) => this.livekitService.deleteRoom(roomName)))
|
Promise.allSettled(roomIds.map((roomId) => this.livekitService.deleteRoom(roomId)))
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Log errors from LiveKit deletions
|
// Log errors from LiveKit deletions
|
||||||
livekitResults.forEach((result, index) => {
|
livekitResults.forEach((result, index) => {
|
||||||
if (result.status === 'rejected') {
|
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) => {
|
livekitResults.forEach((result, index) => {
|
||||||
if (result.status === 'fulfilled') {
|
if (result.status === 'fulfilled') {
|
||||||
successfullyDeleted.add(roomNames[index]);
|
successfullyDeleted.add(roomIds[index]);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -125,25 +168,25 @@ export class RoomService {
|
|||||||
/**
|
/**
|
||||||
* Deletes OpenVidu rooms.
|
* 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.
|
* @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(
|
const results = await Promise.allSettled(
|
||||||
roomNames.map((roomName) => this.globalPrefService.deleteOpenViduRoom(roomName))
|
roomIds.map((roomId) => this.globalPrefService.deleteOpenViduRoom(roomId))
|
||||||
);
|
);
|
||||||
|
|
||||||
const successfulRooms: string[] = [];
|
const successfulRooms: string[] = [];
|
||||||
|
|
||||||
results.forEach((result, index) => {
|
results.forEach((result, index) => {
|
||||||
if (result.status === 'fulfilled') {
|
if (result.status === 'fulfilled') {
|
||||||
successfulRooms.push(roomNames[index]);
|
successfulRooms.push(roomIds[index]);
|
||||||
} else {
|
} 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.');
|
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 roomId - The unique identifier of the room to check
|
||||||
* @param secret - The secret used to identify the participant's role.
|
* @param secret - The secret to validate against the room's moderator and publisher secrets
|
||||||
* @returns The role of the participant {@link ParticipantRole}.
|
* @returns A promise that resolves to the participant role (MODERATOR or PUBLISHER) if the secret is valid
|
||||||
* @throws Will throw an error if the secret is invalid.
|
* @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> {
|
async getRoomSecretRole(roomId: string, secret: string): Promise<ParticipantRole> {
|
||||||
const room = await this.getOpenViduRoom(roomName);
|
const room = await this.getMeetRoom(roomId);
|
||||||
const { moderatorRoomUrl, publisherRoomUrl } = room;
|
const { moderatorRoomUrl, publisherRoomUrl } = room;
|
||||||
|
|
||||||
const extractSecret = (urlString: string, type: string): string => {
|
const extractSecret = (urlString: string, type: string): string => {
|
||||||
@ -180,13 +224,13 @@ export class RoomService {
|
|||||||
case publisherSecret:
|
case publisherSecret:
|
||||||
return ParticipantRole.PUBLISHER;
|
return ParticipantRole.PUBLISHER;
|
||||||
default:
|
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
|
// 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;
|
const isRecordingStarted = activeEgressArray.length > 0;
|
||||||
|
|
||||||
// Skip if recording is not started
|
// Skip if recording is not started
|
||||||
@ -200,61 +244,20 @@ export class RoomService {
|
|||||||
participantSid
|
participantSid
|
||||||
);
|
);
|
||||||
|
|
||||||
await this.sendSignal(roomName, payload, options);
|
await this.sendSignal(roomId, payload, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends a signal to participants in a specified room.
|
* 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 rawData - The raw data to be sent as the signal.
|
||||||
* @param options - Options for sending the data, including the topic and destination identities.
|
* @param options - Options for sending the data, including the topic and destination identities.
|
||||||
* @returns A promise that resolves when the signal has been sent.
|
* @returns A promise that resolves when the signal has been sent.
|
||||||
*/
|
*/
|
||||||
async sendSignal(roomName: string, rawData: any, options: SendDataOptions): Promise<void> {
|
async sendSignal(roomId: string, rawData: Record<string, unknown>, options: SendDataOptions): Promise<void> {
|
||||||
this.logger.verbose(`Notifying participants in room ${roomName}: "${options.topic}".`);
|
this.logger.verbose(`Notifying participants in room ${roomId}: "${options.topic}".`);
|
||||||
this.livekitService.sendData(roomName, rawData, options);
|
this.livekitService.sendData(roomId, 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -274,7 +277,7 @@ export class RoomService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const livekitResults = await Promise.allSettled(
|
const livekitResults = await Promise.allSettled(
|
||||||
ovExpiredRooms.map((roomName) => this.livekitService.deleteRoom(roomName))
|
ovExpiredRooms.map((roomId) => this.livekitService.deleteRoom(roomId))
|
||||||
);
|
);
|
||||||
|
|
||||||
const successfulRooms: string[] = [];
|
const successfulRooms: string[] = [];
|
||||||
@ -309,7 +312,7 @@ export class RoomService {
|
|||||||
const rooms = await this.listOpenViduRooms();
|
const rooms = await this.listOpenViduRooms();
|
||||||
const expiredRooms = rooms
|
const expiredRooms = rooms
|
||||||
.filter((room) => room.expirationDate && room.expirationDate < now)
|
.filter((room) => room.expirationDate && room.expirationDate < now)
|
||||||
.map((room) => room.roomName);
|
.map((room) => room.roomId);
|
||||||
|
|
||||||
if (expiredRooms.length === 0) {
|
if (expiredRooms.length === 0) {
|
||||||
this.logger.verbose('No OpenVidu expired rooms to delete.');
|
this.logger.verbose('No OpenVidu expired rooms to delete.');
|
||||||
@ -353,7 +356,7 @@ export class RoomService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const missingRooms: MeetRoom[] = ovRooms.filter(
|
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) {
|
if (missingRooms.length === 0) {
|
||||||
@ -364,17 +367,17 @@ export class RoomService {
|
|||||||
this.logger.info(`Restoring ${missingRooms.length} missing rooms`);
|
this.logger.info(`Restoring ${missingRooms.length} missing rooms`);
|
||||||
|
|
||||||
const creationResults = await Promise.allSettled(
|
const creationResults = await Promise.allSettled(
|
||||||
missingRooms.map((ovRoom) => {
|
missingRooms.map(({ roomId }: MeetRoom) => {
|
||||||
this.logger.debug(`Restoring room: ${ovRoom.roomName}`);
|
this.logger.debug(`Restoring room: ${roomId}`);
|
||||||
this.createLivekitRoom(ovRoom);
|
this.createLivekitRoom(roomId);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
creationResults.forEach((result, index) => {
|
creationResults.forEach((result, index) => {
|
||||||
if (result.status === 'rejected') {
|
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 {
|
} 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