import { MeetRecordingInfo, MeetRoom, MeetWebhookEvent, MeetWebhookEventType, MeetWebhookPayload, WebhookPreferences } from '@typings-ce'; import crypto from 'crypto'; import { inject, injectable } from 'inversify'; import { MEET_API_KEY } from '../environment.js'; import { LoggerService, MeetStorageService } from './index.js'; @injectable() export class OpenViduWebhookService { constructor( @inject(LoggerService) protected logger: LoggerService, @inject(MeetStorageService) protected globalPrefService: MeetStorageService ) {} async sendMeetingStartedWebhook(room: MeetRoom) { try { await this.sendWebhookEvent(MeetWebhookEventType.MEETING_STARTED, room); } catch (error) { this.logger.error(`Error sending meeting started webhook: ${error}`); } } async sendMeetingEndedWebhook(room: MeetRoom) { try { await this.sendWebhookEvent(MeetWebhookEventType.MEETING_ENDED, room); } catch (error) { this.logger.error(`Error sending meeting ended webhook: ${error}`); } } async sendRecordingStartedWebhook(recordingInfo: MeetRecordingInfo) { try { await this.sendWebhookEvent(MeetWebhookEventType.RECORDING_STARTED, recordingInfo); } catch (error) { this.logger.error(`Error sending recording started webhook: ${error}`); } } async sendRecordingUpdatedWebhook(recordingInfo: MeetRecordingInfo) { try { await this.sendWebhookEvent(MeetWebhookEventType.RECORDING_UPDATED, recordingInfo); } catch (error) { this.logger.error(`Error sending recording updated webhook: ${error}`); } } async sendRecordingEndedWebhook(recordingInfo: MeetRecordingInfo) { try { await this.sendWebhookEvent(MeetWebhookEventType.RECORDING_ENDED, recordingInfo); } catch (error) { this.logger.error(`Error sending recording ended webhook: ${error}`); } } protected async sendWebhookEvent(event: MeetWebhookEventType, payload: MeetWebhookPayload) { const webhookPreferences = await this.getWebhookPreferences(); if (!webhookPreferences.enabled) return; const creationDate = Date.now(); const data: MeetWebhookEvent = { event, creationDate, data: payload }; const signature = this.generateWebhookSignature(creationDate, data); this.logger.info(`Sending webhook event ${data.event}`); try { await this.fetchWithRetry(webhookPreferences.url!, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Timestamp': creationDate.toString(), 'X-Signature': signature }, body: JSON.stringify(data) }); } catch (error) { this.logger.error(`Error sending webhook event ${data.event}: ${error}`); throw error; } } protected generateWebhookSignature(timestamp: number, payload: object): string { return crypto .createHmac('sha256', MEET_API_KEY) .update(`${timestamp}.${JSON.stringify(payload)}`) .digest('hex'); } protected async fetchWithRetry(url: string, options: RequestInit, retries = 5, delay = 300): Promise { try { const response = await fetch(url, options); if (!response.ok) { throw new Error(`Request failed with status ${response.status}`); } } catch (error) { if (retries <= 0) { throw new Error(`Request failed: ${error}`); } this.logger.warn(`Retrying in ${delay / 1000} seconds... (${retries} retries left)`); await new Promise((resolve) => setTimeout(resolve, delay)); // Retry the request after a delay with exponential backoff return this.fetchWithRetry(url, options, retries - 1, delay * 2); } } protected async getWebhookPreferences(): Promise { try { const { webhooksPreferences } = await this.globalPrefService.getGlobalPreferences(); return webhooksPreferences; } catch (error) { this.logger.error('Error getting webhook preferences:', error); throw error; } } }