openvidu/backend/src/services/openvidu-webhook.service.ts

123 lines
3.7 KiB
TypeScript

import crypto from 'crypto';
import { inject, injectable } from '../config/dependency-injector.config.js';
import { Room } from 'livekit-server-sdk';
import { LoggerService } from './logger.service.js';
import { MEET_API_KEY } from '../environment.js';
import {
MeetWebhookEvent,
MeetWebhookEventType,
MeetRecordingInfo,
MeetWebhookPayload,
WebhookPreferences
} from '@typings-ce';
import { MeetStorageService } from './storage/storage.service.js';
@injectable()
export class OpenViduWebhookService {
constructor(
@inject(LoggerService) protected logger: LoggerService,
@inject(MeetStorageService) protected globalPrefService: MeetStorageService
) {}
// TODO: Implement Room webhooks
async sendRoomFinishedWebhook(room: Room) {
// try {
// await this.sendWebhookEvent(MeetWebhookEventType.ROOM_FINISHED, data);
// } catch (error) {
// this.logger.error(`Error sending room finished 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}`);
}
}
private 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;
}
}
private generateWebhookSignature(timestamp: number, payload: object): string {
return crypto
.createHmac('sha256', MEET_API_KEY)
.update(`${timestamp}.${JSON.stringify(payload)}`)
.digest('hex');
}
private async fetchWithRetry(url: string, options: RequestInit, retries = 5, delay = 300): Promise<void> {
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);
}
}
private async getWebhookPreferences(): Promise<WebhookPreferences> {
try {
const { webhooksPreferences } = await this.globalPrefService.getGlobalPreferences();
return webhooksPreferences;
} catch (error) {
this.logger.error('Error getting webhook preferences:', error);
throw error;
}
}
}