From 0125fc0934a5fe63bc2c91cc3f47be07741bdbdc Mon Sep 17 00:00:00 2001 From: juancarmore Date: Thu, 28 Aug 2025 18:32:59 +0200 Subject: [PATCH] backend: implement room status handling and actions when meeting ends --- backend/src/helpers/room.helper.ts | 1 + backend/src/models/error.model.ts | 20 ++--- .../src/services/livekit-webhook.service.ts | 88 +++++++++++++------ backend/src/services/participant.service.ts | 14 ++- backend/src/services/room.service.ts | 2 +- backend/src/services/token.service.ts | 1 - typings/src/room.ts | 5 +- 7 files changed, 85 insertions(+), 46 deletions(-) diff --git a/backend/src/helpers/room.helper.ts b/backend/src/helpers/room.helper.ts index be21fa5..6ec0dd1 100644 --- a/backend/src/helpers/room.helper.ts +++ b/backend/src/helpers/room.helper.ts @@ -16,6 +16,7 @@ export class MeetRoomHelper { return { roomName: room.roomName, autoDeletionDate: room.autoDeletionDate, + autoDeletionPolicy: room.autoDeletionPolicy, preferences: room.preferences // maxParticipants: room.maxParticipants }; diff --git a/backend/src/models/error.model.ts b/backend/src/models/error.model.ts index 5fc0f4d..6360022 100644 --- a/backend/src/models/error.model.ts +++ b/backend/src/models/error.model.ts @@ -204,6 +204,10 @@ export const errorRoomNotFound = (roomId: string): OpenViduMeetError => { return new OpenViduMeetError('Room Error', `Room '${roomId}' does not exist`, 404); }; +export const errorRoomClosed = (roomId: string): OpenViduMeetError => { + return new OpenViduMeetError('Room Error', `Room '${roomId}' is closed and cannot be joined`, 409); +}; + export const errorRoomMetadataNotFound = (roomId: string): OpenViduMeetError => { return new OpenViduMeetError( 'Room Error', @@ -226,28 +230,20 @@ export const errorParticipantNotFound = (participantIdentity: string, roomId: st ); }; -export const errorParticipantAlreadyExists = (participantIdentity: string, roomId: string): OpenViduMeetError => { - return new OpenViduMeetError( - 'Participant Error', - `Participant '${participantIdentity}' already exists in room '${roomId}'`, - 409 - ); -}; - export const errorParticipantTokenNotPresent = (): OpenViduMeetError => { - return new OpenViduMeetError('Participant', 'No participant token provided', 400); + return new OpenViduMeetError('Participant Error', 'No participant token provided', 400); }; export const errorInvalidParticipantToken = (): OpenViduMeetError => { - return new OpenViduMeetError('Participant', 'Invalid participant token', 400); + return new OpenViduMeetError('Participant Error', 'Invalid participant token', 400); }; export const errorInvalidParticipantRole = (): OpenViduMeetError => { - return new OpenViduMeetError('Participant', 'No valid participant role provided', 400); + return new OpenViduMeetError('Participant Error', 'No valid participant role provided', 400); }; export const errorParticipantIdentityNotProvided = (): OpenViduMeetError => { - return new OpenViduMeetError('Participant', 'No participant identity provided', 400); + return new OpenViduMeetError('Participant Error', 'No participant identity provided', 400); }; // Handlers diff --git a/backend/src/services/livekit-webhook.service.ts b/backend/src/services/livekit-webhook.service.ts index 918c8a3..5dd229e 100644 --- a/backend/src/services/livekit-webhook.service.ts +++ b/backend/src/services/livekit-webhook.service.ts @@ -1,10 +1,13 @@ -import { MeetRecordingInfo, MeetRecordingStatus } from '@typings-ce'; +import { MeetingEndAction, MeetRecordingInfo, MeetRecordingStatus, MeetRoomStatus } from '@typings-ce'; import { inject, injectable } from 'inversify'; import { EgressInfo, ParticipantInfo, Room, WebhookEvent, WebhookReceiver } from 'livekit-server-sdk'; +import ms from 'ms'; import { LIVEKIT_API_KEY, LIVEKIT_API_SECRET } from '../environment.js'; import { MeetLock, MeetRoomHelper, RecordingHelper } from '../helpers/index.js'; import { DistributedEventType } from '../models/distributed-event.model.js'; +import { FrontendEventService } from './frontend-event.service.js'; import { + DistributedEventService, LiveKitService, LoggerService, MeetStorageService, @@ -12,11 +15,8 @@ import { OpenViduWebhookService, ParticipantService, RecordingService, - RoomService, - DistributedEventService + RoomService } from './index.js'; -import { FrontendEventService } from './frontend-event.service.js'; -import ms from 'ms'; @injectable() export class LivekitWebhookService { @@ -183,57 +183,89 @@ export class LivekitWebhookService { * Handles a room started event from LiveKit. * * This method retrieves the corresponding meet room from the room service using the LiveKit room name. - * If the meet room is found, it sends a webhook notification indicating that the meeting has started. - * If the meet room is not found, it logs a warning message. + * If the meet room is found, it updates the room status to ACTIVE_MEETING, + * and sends a webhook notification indicating that the meeting has started. + * + * @param {Room} room - The room object that has started. */ - async handleRoomStarted(room: Room) { + async handleRoomStarted({ name: roomId }: Room) { try { - const meetRoom = await this.roomService.getMeetRoom(room.name); + const meetRoom = await this.roomService.getMeetRoom(roomId); if (!meetRoom) { - this.logger.warn(`Room ${room.name} not found in OpenVidu Meet.`); + this.logger.warn(`Room '${roomId}' not found in OpenVidu Meet.`); return; } + this.logger.info(`Processing room_started event for room: ${roomId}`); + + // Update Meet room status to ACTIVE_MEETING + meetRoom.status = MeetRoomStatus.ACTIVE_MEETING; + await this.storageService.saveMeetRoom(meetRoom); + + // Send webhook notification this.openViduWebhookService.sendMeetingStartedWebhook(meetRoom); } catch (error) { - this.logger.error('Error sending meeting started webhook:', error); + this.logger.error('Error handling room started event:', error); } } /** - * Handles the event when a room is finished. + * Handles a room finished event from LiveKit. * - * This method sends a webhook notification indicating that the room has finished. - * If an error occurs while sending the webhook, it logs the error. + * This method retrieves the corresponding meet room from the room service using the LiveKit room name. + * If the meet room is found, it processes the room based on its meeting end action: + * + * - If the action is DELETE, it deletes the room and all associated recordings. + * - If the action is CLOSE, it closes the room without deleting it. + * - If the action is NONE, it simply updates the room status to OPEN. + * + * Then, it sends a webhook notification indicating that the meeting has ended, + * and cleans up any resources associated with the room. * * @param {Room} room - The room object that has finished. - * @returns {Promise} A promise that resolves when the webhook has been sent. */ - async handleRoomFinished({ name: roomName }: Room): Promise { + async handleRoomFinished({ name: roomId }: Room): Promise { try { - const meetRoom = await this.roomService.getMeetRoom(roomName); + const meetRoom = await this.roomService.getMeetRoom(roomId); if (!meetRoom) { - this.logger.warn(`Room ${roomName} not found in OpenVidu Meet.`); + this.logger.warn(`Room '${roomId}' not found in OpenVidu Meet.`); return; } - this.logger.info(`Processing room_finished event for room: ${roomName}`); - - this.openViduWebhookService.sendMeetingEndedWebhook(meetRoom); - + this.logger.info(`Processing room_finished event for room: ${roomId}`); const tasks = []; - if (meetRoom.markedForDeletion) { - // If the room is marked for deletion, we need to delete it - this.logger.info(`Deleting room ${roomName} after meeting finished because it was marked for deletion`); - tasks.push(this.roomService.bulkDeleteRooms([roomName], true)); + switch (meetRoom.meetingEndAction) { + case MeetingEndAction.DELETE: + // TODO: Delete also all recordings associated with the room + this.logger.info( + `Deleting room '${roomId}' after meeting finished because it was scheduled to be deleted` + ); + tasks.push(this.roomService.bulkDeleteRooms([roomId], true)); + break; + case MeetingEndAction.CLOSE: + this.logger.info( + `Closing room '${roomId}' after meeting finished because it was scheduled to be closed` + ); + meetRoom.status = MeetRoomStatus.CLOSED; + meetRoom.meetingEndAction = MeetingEndAction.NONE; + tasks.push(this.storageService.saveMeetRoom(meetRoom)); + break; + default: + // Update Meet room status to OPEN + meetRoom.status = MeetRoomStatus.OPEN; + meetRoom.meetingEndAction = MeetingEndAction.NONE; + tasks.push(this.storageService.saveMeetRoom(meetRoom)); } + // Send webhook notification + this.openViduWebhookService.sendMeetingEndedWebhook(meetRoom); + tasks.push( - this.participantService.cleanupParticipantNames(roomName), - this.recordingService.releaseRecordingLockIfNoEgress(roomName) + this.participantService.cleanupParticipantNames(roomId), + this.recordingService.releaseRecordingLockIfNoEgress(roomId) ); await Promise.all(tasks); } catch (error) { diff --git a/backend/src/services/participant.service.ts b/backend/src/services/participant.service.ts index dc93360..c060595 100644 --- a/backend/src/services/participant.service.ts +++ b/backend/src/services/participant.service.ts @@ -1,4 +1,5 @@ import { + MeetRoomStatus, MeetTokenMetadata, OpenViduMeetPermissions, ParticipantOptions, @@ -9,7 +10,11 @@ import { inject, injectable } from 'inversify'; import { ParticipantInfo } from 'livekit-server-sdk'; import { MeetRoomHelper } from '../helpers/room.helper.js'; import { validateMeetTokenMetadata } from '../middlewares/index.js'; -import { errorParticipantIdentityNotProvided, errorParticipantNotFound } from '../models/error.model.js'; +import { + errorParticipantIdentityNotProvided, + errorParticipantNotFound, + errorRoomClosed +} from '../models/error.model.js'; import { FrontendEventService, LiveKitService, @@ -40,6 +45,13 @@ export class ParticipantService { let finalParticipantOptions: ParticipantOptions = participantOptions; if (participantName) { + // Check that room is open + const room = await this.roomService.getMeetRoom(roomId); + + if (room.status === MeetRoomStatus.CLOSED) { + throw errorRoomClosed(roomId); + } + if (refresh) { if (!participantIdentity) { throw errorParticipantIdentityNotProvided(); diff --git a/backend/src/services/room.service.ts b/backend/src/services/room.service.ts index 3280aaa..9798393 100644 --- a/backend/src/services/room.service.ts +++ b/backend/src/services/room.service.ts @@ -454,7 +454,7 @@ export class RoomService { if (result.status === 'fulfilled' && result.value) { const room = result.value; - room.markedForDeletion = true; + room.meetingEndAction = MeetingEndAction.DELETE; roomsToUpdate.push({ roomId, room }); successfulRoomIds.push(roomId); } else { diff --git a/backend/src/services/token.service.ts b/backend/src/services/token.service.ts index 8579e25..c8f6e1e 100644 --- a/backend/src/services/token.service.ts +++ b/backend/src/services/token.service.ts @@ -13,7 +13,6 @@ import { AccessToken, AccessTokenOptions, ClaimGrants, TokenVerifier, VideoGrant import INTERNAL_CONFIG from '../config/internal-config.js'; import { LIVEKIT_API_KEY, LIVEKIT_API_SECRET, LIVEKIT_URL } from '../environment.js'; import { LoggerService } from './index.js'; -import { uid } from 'uid'; @injectable() export class TokenService { diff --git a/typings/src/room.ts b/typings/src/room.ts index 667c735..0eb07fe 100644 --- a/typings/src/room.ts +++ b/typings/src/room.ts @@ -36,9 +36,8 @@ export const enum MeetRoomStatus { export const enum MeetingEndAction { NONE = 'none', // No action is taken when the meeting ends - CLOSE = 'close', // The room is closed when the meeting ends - DELETE = 'delete', // The room is deleted when the meeting ends - DELETE_ALL = 'delete_all' // The room and its recordings are deleted when the meeting ends + CLOSE = 'close', // The room will be closed when the meeting ends + DELETE = 'delete' // The room (and its recordings if any) will be deleted when the meeting ends } export interface MeetRoomAutoDeletionPolicy {