backend: enhance room deletion logic with new policies for meetings and recordings

This commit is contained in:
juancarmore 2025-09-02 11:45:20 +02:00
parent 632d36a470
commit 4802f48ba6
7 changed files with 462 additions and 219 deletions

View File

@ -61,10 +61,10 @@ export const registerDependencies = () => {
container.bind(FrontendEventService).toSelf().inSingletonScope();
container.bind(LiveKitService).toSelf().inSingletonScope();
container.bind(RecordingService).toSelf().inSingletonScope();
container.bind(RoomService).toSelf().inSingletonScope();
container.bind(ParticipantNameService).toSelf().inSingletonScope();
container.bind(ParticipantService).toSelf().inSingletonScope();
container.bind(RecordingService).toSelf().inSingletonScope();
container.bind(OpenViduWebhookService).toSelf().inSingletonScope();
container.bind(LivekitWebhookService).toSelf().inSingletonScope();
};

View File

@ -1,4 +1,12 @@
import { MeetRoomFilters, MeetRoomOptions, MeetRoomRoleAndPermissions, ParticipantRole } from '@typings-ce';
import {
MeetRoomDeletionPolicyWithMeeting,
MeetRoomDeletionPolicyWithRecordings,
MeetRoomDeletionSuccessCode,
MeetRoomFilters,
MeetRoomOptions,
MeetRoomRoleAndPermissions,
ParticipantRole
} from '@typings-ce';
import { Request, Response } from 'express';
import { container } from '../config/index.js';
import INTERNAL_CONFIG from '../config/internal-config.js';
@ -63,21 +71,26 @@ export const deleteRoom = async (req: Request, res: Response) => {
const roomService = container.get(RoomService);
const { roomId } = req.params;
const { force } = req.query;
const forceDelete = force === 'true';
const { withMeeting, withRecordings } = req.query as {
withMeeting: MeetRoomDeletionPolicyWithMeeting;
withRecordings: MeetRoomDeletionPolicyWithRecordings;
};
try {
logger.verbose(`Deleting room '${roomId}'`);
const response = await roomService.deleteMeetRoom(roomId, withMeeting, withRecordings);
const { deleted } = await roomService.bulkDeleteRooms([roomId], forceDelete);
// Determine the status code based on the success code
// If the room action is scheduled, return 202. Otherwise, return 200.
const scheduledSuccessCodes = [
MeetRoomDeletionSuccessCode.ROOM_WITH_ACTIVE_MEETING_SCHEDULED_TO_BE_DELETED,
MeetRoomDeletionSuccessCode.ROOM_WITH_ACTIVE_MEETING_SCHEDULED_TO_BE_CLOSED,
MeetRoomDeletionSuccessCode.ROOM_WITH_ACTIVE_MEETING_AND_RECORDINGS_SCHEDULED_TO_BE_DELETED
];
const statusCode = scheduledSuccessCodes.includes(response.successCode) ? 202 : 200;
if (deleted.length > 0) {
// Room was deleted
return res.status(204).send();
}
// Room was marked as deleted
return res.status(202).json({ message: `Room '${roomId}' marked for deletion` });
logger.info(response.message);
return res.status(statusCode).json(response);
} catch (error) {
handleError(res, error, `deleting room '${roomId}'`);
}
@ -86,34 +99,30 @@ export const deleteRoom = async (req: Request, res: Response) => {
export const bulkDeleteRooms = async (req: Request, res: Response) => {
const logger = container.get(LoggerService);
const roomService = container.get(RoomService);
const { roomIds, force } = req.query;
const forceDelete = force === 'true';
logger.verbose(`Deleting rooms: ${roomIds}`);
const { roomIds, withMeeting, withRecordings } = req.query as {
roomIds: string[];
withMeeting: MeetRoomDeletionPolicyWithMeeting;
withRecordings: MeetRoomDeletionPolicyWithRecordings;
};
try {
const roomIdsArray = roomIds as string[];
logger.verbose(`Deleting rooms: ${roomIds}`);
const { successful, failed } = await roomService.bulkDeleteMeetRooms(roomIds, withMeeting, withRecordings);
const { deleted, markedForDeletion } = await roomService.bulkDeleteRooms(roomIdsArray, forceDelete);
logger.info(
`Bulk delete operation - Successfully processed rooms: ${successful.length}, failed to process: ${failed.length}`
);
logger.info(`Deleted rooms: ${deleted.length}, marked for deletion: ${markedForDeletion.length}`);
// All rooms were deleted
if (deleted.length > 0 && markedForDeletion.length === 0) {
return res.sendStatus(204);
if (failed.length === 0) {
// All rooms were successfully processed
return res.status(200).json({ message: 'All rooms successfully processed for deletion', successful });
} else {
// Some rooms failed to process
return res
.status(400)
.json({ message: `${failed.length} room(s) failed to process while deleting`, successful, failed });
}
// All room were marked for deletion
if (deleted.length === 0 && markedForDeletion.length > 0) {
const message =
markedForDeletion.length === 1
? `Room '${markedForDeletion[0]}' marked for deletion`
: `Rooms '${markedForDeletion.join(', ')}' marked for deletion`;
return res.status(202).json({ message });
}
// Mixed result (some rooms deleted, some marked for deletion)
return res.status(200).json({ deleted, markedForDeletion });
} catch (error) {
handleError(res, error, `deleting rooms`);
}

View File

@ -1,7 +1,8 @@
import { MeetRoomDeletionErrorCode } from '@typings-ce';
import { Response } from 'express';
import { z } from 'zod';
import { container } from '../config/index.js';
import { LoggerService } from '../services/index.js';
import { z } from 'zod';
type StatusError = 400 | 401 | 402 | 403 | 404 | 409 | 415 | 416 | 422 | 500 | 503;
export class OpenViduMeetError extends Error {
@ -220,6 +221,10 @@ export const errorInvalidRoomSecret = (roomId: string, secret: string): OpenVidu
return new OpenViduMeetError('Room Error', `Secret '${secret}' is not recognized for room '${roomId}'`, 400);
};
export const errorDeletingRoom = (errorCode: MeetRoomDeletionErrorCode, message: string): OpenViduMeetError => {
return new OpenViduMeetError(errorCode, message, 409);
};
// Participant errors
export const errorParticipantNotFound = (participantIdentity: string, roomId: string): OpenViduMeetError => {

View File

@ -12,9 +12,9 @@ export * from './auth.service.js';
export * from './livekit.service.js';
export * from './frontend-event.service.js';
export * from './recording.service.js';
export * from './room.service.js';
export * from './participant-name.service.js';
export * from './participant.service.js';
export * from './recording.service.js';
export * from './openvidu-webhook.service.js';
export * from './livekit-webhook.service.js';

View File

@ -31,7 +31,6 @@ import {
MeetStorageService,
MutexService,
RedisLock,
RoomService,
TaskSchedulerService
} from './index.js';
@ -39,7 +38,6 @@ import {
export class RecordingService {
constructor(
@inject(LiveKitService) protected livekitService: LiveKitService,
@inject(RoomService) protected roomService: RoomService,
@inject(MutexService) protected mutexService: MutexService,
@inject(TaskSchedulerService) protected taskSchedulerService: TaskSchedulerService,
@inject(DistributedEventService) protected systemEventService: DistributedEventService,
@ -552,6 +550,22 @@ export class RecordingService {
}
}
/**
* Helper method to check if a room has recordings
*
* @param roomId - The ID of the room to check
* @returns A promise that resolves to true if the room has recordings, false otherwise
*/
async hasRoomRecordings(roomId: string): Promise<boolean> {
try {
const response = await this.storageService.getAllRecordings(roomId, 1);
return response.recordings.length > 0;
} catch (error) {
this.logger.warn(`Error checking recordings for room '${roomId}': ${error}`);
return false;
}
}
async getRecordingAsStream(
recordingId: string,
rangeHeader?: string
@ -584,7 +598,7 @@ export class RecordingService {
}
protected async validateRoomForStartRecording(roomId: string): Promise<void> {
const room = await this.roomService.getMeetRoom(roomId);
const room = await this.storageService.getMeetRoom(roomId);
if (!room) throw errorRoomNotFound(roomId);

File diff suppressed because it is too large Load Diff

View File

@ -68,3 +68,23 @@ export type MeetRoomFilters = {
roomName?: string;
fields?: string;
};
export const enum MeetRoomDeletionSuccessCode {
ROOM_DELETED = 'room_deleted',
ROOM_WITH_ACTIVE_MEETING_DELETED = 'room_with_active_meeting_deleted',
ROOM_WITH_ACTIVE_MEETING_SCHEDULED_TO_BE_DELETED = 'room_with_active_meeting_scheduled_to_be_deleted',
ROOM_AND_RECORDINGS_DELETED = 'room_and_recordings_deleted',
ROOM_CLOSED = 'room_closed',
ROOM_WITH_ACTIVE_MEETING_AND_RECORDINGS_DELETED = 'room_with_active_meeting_and_recordings_deleted',
ROOM_WITH_ACTIVE_MEETING_CLOSED = 'room_with_active_meeting_closed',
ROOM_WITH_ACTIVE_MEETING_AND_RECORDINGS_SCHEDULED_TO_BE_DELETED = 'room_with_active_meeting_and_recordings_scheduled_to_be_deleted',
ROOM_WITH_ACTIVE_MEETING_SCHEDULED_TO_BE_CLOSED = 'room_with_active_meeting_scheduled_to_be_closed'
}
export const enum MeetRoomDeletionErrorCode {
ROOM_HAS_ACTIVE_MEETING = 'room_has_active_meeting',
ROOM_HAS_RECORDINGS = 'room_has_recordings',
ROOM_WITH_ACTIVE_MEETING_HAS_RECORDINGS = 'room_with_active_meeting_has_recordings',
ROOM_WITH_ACTIVE_MEETING_HAS_RECORDINGS_CANNOT_SCHEDULE_DELETION = 'room_with_active_meeting_has_recordings_cannot_schedule_deletion',
ROOM_WITH_RECORDINGS_HAS_ACTIVE_MEETING = 'room_with_recordings_has_active_meeting'
}