import { AuthMode, AuthType, GlobalPreferences, MeetRecordingInfo, MeetRoom } from '@typings-ce'; import { inject, injectable } from 'inversify'; import ms from 'ms'; import { MEET_NAME_ID, MEET_SECRET, MEET_USER, MEET_WEBHOOK_ENABLED, MEET_WEBHOOK_URL } from '../../environment.js'; import { MeetLock, PasswordHelper } from '../../helpers/index.js'; import { errorRoomNotFound, internalError, OpenViduMeetError } from '../../models/error.model.js'; import { LoggerService, MutexService, StorageFactory, StorageProvider } from '../index.js'; /** * A service for managing storage operations related to OpenVidu Meet rooms and preferences. * * This service provides an abstraction layer over the underlying storage implementation, * handling initialization, retrieval, and persistence of global preferences and room data. * * @template GPrefs - Type for global preferences, extends GlobalPreferences * @template MRoom - Type for room data, extends MeetRoom */ @injectable() export class MeetStorageService< GPrefs extends GlobalPreferences = GlobalPreferences, MRoom extends MeetRoom = MeetRoom, MRec extends MeetRecordingInfo = MeetRecordingInfo > { protected storageProvider: StorageProvider; constructor( @inject(LoggerService) protected logger: LoggerService, @inject(StorageFactory) protected storageFactory: StorageFactory, @inject(MutexService) protected mutexService: MutexService ) { this.storageProvider = this.storageFactory.create(); } /** * Initializes default preferences if not already initialized. * @returns {Promise} Default global preferences. */ async initializeGlobalPreferences(): Promise { try { // Acquire a global lock to prevent multiple initializations at the same time when running in HA mode const lock = await this.mutexService.acquire(MeetLock.getGlobalPreferencesLock(), ms('30s')); if (!lock) { this.logger.warn( 'Unable to acquire lock for global preferences initialization. May be already initialized by another instance.' ); return; } const preferences = await this.getDefaultPreferences(); this.logger.verbose('Initializing global preferences with default values'); await this.storageProvider.initialize(preferences); } catch (error) { this.handleError(error, 'Error initializing default preferences'); } } /** * Retrieves the global preferences, initializing them if necessary. * @returns {Promise} */ async getGlobalPreferences(): Promise { let preferences = await this.storageProvider.getGlobalPreferences(); if (preferences) return preferences as GPrefs; await this.initializeGlobalPreferences(); preferences = await this.storageProvider.getGlobalPreferences(); if (!preferences) { this.logger.error('Global preferences not found after initialization'); throw internalError('getting global preferences'); } return preferences as GPrefs; } /** * Saves the global preferences to the storage provider. * @param {GPrefs} preferences * @returns {Promise} */ async saveGlobalPreferences(preferences: GPrefs): Promise { this.logger.info('Saving global preferences'); return this.storageProvider.saveGlobalPreferences(preferences) as Promise; } /** * Saves the meet room to the storage provider. * * @param meetRoom - The meeting room object to be saved * @returns A promise that resolves to the saved meeting room object */ async saveMeetRoom(meetRoom: MRoom): Promise { this.logger.info(`Saving OpenVidu room ${meetRoom.roomId}`); return this.storageProvider.saveMeetRoom(meetRoom) as Promise; } /** * Retrieves a paginated list of meeting rooms from the storage provider. * * @param maxItems - Optional maximum number of rooms to retrieve in a single request * @param nextPageToken - Optional token for pagination to get the next page of results * @returns A promise that resolves to an object containing: * - rooms: Array of MRoom objects representing the meeting rooms * - isTruncated: Boolean indicating if there are more results available * - nextPageToken: Optional token for retrieving the next page of results */ async getMeetRooms( maxItems?: number, nextPageToken?: string ): Promise<{ rooms: MRoom[]; isTruncated: boolean; nextPageToken?: string; }> { return this.storageProvider.getMeetRooms(maxItems, nextPageToken) as Promise<{ rooms: MRoom[]; isTruncated: boolean; nextPageToken?: string; }>; } /** * Retrieves the preferences associated with a specific room. * * @param roomId - The unique identifier for the room. * @returns A promise that resolves to the room's preferences. * @throws Error if the room preferences are not found. */ async getMeetRoom(roomId: string): Promise { const meetRoom = await this.storageProvider.getMeetRoom(roomId); if (!meetRoom) { this.logger.error(`Room not found for room ${roomId}`); throw errorRoomNotFound(roomId); } return meetRoom as MRoom; } /** * Deletes multiple meeting rooms from storage. * * @param roomIds - Array of room identifiers to be deleted * @returns A promise that resolves when all rooms have been successfully deleted * @throws May throw an error if the deletion operation fails for any of the rooms */ async deleteMeetRooms(roomIds: string[]): Promise { return this.storageProvider.deleteMeetRooms(roomIds); } /** * Retrieves metadata for an archived room by its ID. * * @param roomId - The unique identifier of the room to retrieve metadata for * @returns A promise that resolves to partial room metadata if found, or null if not found */ async getArchivedRoomMetadata(roomId: string): Promise | null> { return this.storageProvider.getArchivedRoomMetadata(roomId) as Promise | null>; } /** * Archives the metadata for a specific room. * * @param roomId - The unique identifier of the room whose metadata should be archived * @returns A Promise that resolves when the archival operation is complete * @throws May throw an error if the archival operation fails or if the room ID is invalid */ async archiveRoomMetadata(roomId: string): Promise { return this.storageProvider.archiveRoomMetadata(roomId); } /** * Updates the metadata of an archived room. * * @param roomId - The unique identifier of the room whose archived metadata should be updated * @returns A promise that resolves when the archived room metadata has been successfully updated * @throws May throw an error if the room ID is invalid or if the storage operation fails */ async updateArchivedRoomMetadata(roomId: string): Promise { return this.storageProvider.updateArchivedRoomMetadata(roomId); } /** * Saves recording metadata to the storage provider. * * @param recordingInfo - The recording metadata object to be saved * @returns A promise that resolves to the saved recording metadata object */ async saveRecordingMetadata(recordingInfo: MRec): Promise { return this.storageProvider.saveRecordingMetadata(recordingInfo) as Promise; } async getRecordingMetadata(recordingId: string): Promise<{ recordingInfo: MRec; metadataFilePath: string }> { return this.storageProvider.getRecordingMetadata(recordingId) as Promise<{ recordingInfo: MRec; metadataFilePath: string; }>; } /** * Returns the default global preferences. * @returns {GPrefs} */ protected async getDefaultPreferences(): Promise { return { projectId: MEET_NAME_ID, webhooksPreferences: { enabled: MEET_WEBHOOK_ENABLED === 'true', url: MEET_WEBHOOK_URL }, securityPreferences: { roomCreationPolicy: { allowRoomCreation: true, requireAuthentication: true }, authentication: { authMode: AuthMode.NONE, method: { type: AuthType.SINGLE_USER, credentials: { username: MEET_USER, passwordHash: await PasswordHelper.hashPassword(MEET_SECRET) } } } } } as GPrefs; } /** * Handles errors and logs them. * @param {any} error * @param {string} message */ protected handleError(error: OpenViduMeetError | unknown, message: string) { if (error instanceof OpenViduMeetError) { this.logger.error(`${message}: ${error.message}`); } else { this.logger.error(`${message}: Unexpected error`); } } }