236 lines
7.4 KiB
TypeScript
236 lines
7.4 KiB
TypeScript
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 { AuthService, LoggerService, MeetStorageService } from './index.js';
|
|
import { errorWebhookUrlUnreachable } from '../models/error.model.js';
|
|
|
|
@injectable()
|
|
export class OpenViduWebhookService {
|
|
constructor(
|
|
@inject(LoggerService) protected logger: LoggerService,
|
|
@inject(MeetStorageService) protected globalPrefService: MeetStorageService,
|
|
@inject(AuthService) protected authService: AuthService
|
|
) {}
|
|
|
|
/**
|
|
* Sends a webhook notification when a meeting has started.
|
|
*
|
|
* This method triggers a background webhook event to notify external systems
|
|
* that a meeting session has begun for the specified room.
|
|
*
|
|
* @param room - The meeting room object containing room details
|
|
*/
|
|
sendMeetingStartedWebhook(room: MeetRoom) {
|
|
this.sendWebhookEventInBackground(MeetWebhookEventType.MEETING_STARTED, room, `Room ID: ${room.roomId}`);
|
|
}
|
|
|
|
/**
|
|
* Sends a webhook notification when a meeting has ended.
|
|
*
|
|
* This method triggers a background webhook event to notify external systems
|
|
* that a meeting session has concluded for the specified room.
|
|
*
|
|
* @param room - The MeetRoom object containing details of the ended meeting
|
|
*/
|
|
sendMeetingEndedWebhook(room: MeetRoom) {
|
|
this.sendWebhookEventInBackground(MeetWebhookEventType.MEETING_ENDED, room, `Room ID: ${room.roomId}`);
|
|
}
|
|
|
|
/**
|
|
* Sends a webhook event notification when a recording has started.
|
|
*
|
|
* This method triggers a background webhook event to notify external systems
|
|
* that a meeting recording has been initiated.
|
|
*
|
|
* @param recordingInfo - The recording information containing details about the started recording
|
|
*/
|
|
sendRecordingUpdatedWebhook(recordingInfo: MeetRecordingInfo) {
|
|
this.sendWebhookEventInBackground(
|
|
MeetWebhookEventType.RECORDING_UPDATED,
|
|
recordingInfo,
|
|
`Recording ID: ${recordingInfo.recordingId}`
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Sends a webhook notification when a recording has started.
|
|
*
|
|
* This method triggers a background webhook event to notify external services
|
|
* that a meeting recording has begun. The webhook includes the recording
|
|
* information and uses the recording ID for identification purposes.
|
|
*
|
|
* @param recordingInfo - The recording information containing details about the started recording
|
|
*/
|
|
sendRecordingStartedWebhook(recordingInfo: MeetRecordingInfo) {
|
|
this.sendWebhookEventInBackground(
|
|
MeetWebhookEventType.RECORDING_STARTED,
|
|
recordingInfo,
|
|
`Recording ID: ${recordingInfo.recordingId}`
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Sends a webhook notification when a recording has ended.
|
|
*
|
|
* This method triggers a background webhook event to notify external systems
|
|
* that a meeting recording has completed.
|
|
*
|
|
* @param recordingInfo - The recording information containing details about the ended recording
|
|
*/
|
|
sendRecordingEndedWebhook(recordingInfo: MeetRecordingInfo) {
|
|
this.sendWebhookEventInBackground(
|
|
MeetWebhookEventType.RECORDING_ENDED,
|
|
recordingInfo,
|
|
`Recording ID: ${recordingInfo.recordingId}`
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Tests a webhook URL by sending a test event to it.
|
|
*
|
|
* This method sends a test event to the specified webhook URL to verify if it is reachable and functioning correctly.
|
|
* If the request fails, it throws an error indicating that the webhook URL is unreachable.
|
|
*
|
|
* @param url - The webhook URL to test
|
|
*/
|
|
async testWebhookUrl(url: string) {
|
|
const creationDate = Date.now();
|
|
const data = {
|
|
event: 'testEvent',
|
|
creationDate,
|
|
data: {
|
|
message: 'This is a test webhook event'
|
|
}
|
|
};
|
|
|
|
try {
|
|
await this.sendRequest(url, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify(data)
|
|
});
|
|
} catch (error) {
|
|
this.logger.error(`Error testing webhook URL ${url}: ${error}`);
|
|
throw errorWebhookUrlUnreachable(url);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sends a webhook event asynchronously in the background without blocking the main execution flow.
|
|
* If the webhook fails, logs a warning message with the error details and optional context information.
|
|
*
|
|
* @param event - The type of webhook event to send
|
|
* @param payload - The data payload to include with the webhook event
|
|
* @param context - Optional context string to include in error messages for debugging purposes
|
|
*/
|
|
protected sendWebhookEventInBackground(
|
|
event: MeetWebhookEventType,
|
|
payload: MeetWebhookPayload,
|
|
context?: string
|
|
): void {
|
|
this.sendWebhookEvent(event, payload).catch((error) => {
|
|
const contextInfo = context ? ` (${context})` : '';
|
|
this.logger.warn(`Background webhook ${event} failed${contextInfo}: ${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
|
|
};
|
|
|
|
this.logger.info(`Sending webhook event ${data.event}`);
|
|
|
|
try {
|
|
const signature = await this.generateWebhookSignature(creationDate, data);
|
|
|
|
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} to '${webhookPreferences.url}':`, error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
protected async generateWebhookSignature(timestamp: number, payload: object): Promise<string> {
|
|
const apiKey = await this.getApiKey();
|
|
return crypto
|
|
.createHmac('sha256', apiKey)
|
|
.update(`${timestamp}.${JSON.stringify(payload)}`)
|
|
.digest('hex');
|
|
}
|
|
|
|
protected async fetchWithRetry(url: string, options: RequestInit, retries = 5, delay = 300): Promise<void> {
|
|
try {
|
|
await this.sendRequest(url, options);
|
|
} 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 sendRequest(url: string, options: RequestInit): Promise<void> {
|
|
const response = await fetch(url, options);
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`Request failed with status ${response.status}`);
|
|
}
|
|
}
|
|
|
|
protected 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;
|
|
}
|
|
}
|
|
|
|
protected async getApiKey(): Promise<string> {
|
|
const apiKeys = await this.authService.getApiKeys();
|
|
|
|
if (apiKeys.length === 0) {
|
|
// If no API keys are configured, check if the MEET_API_KEY environment variable is set
|
|
if (MEET_API_KEY) {
|
|
return MEET_API_KEY;
|
|
}
|
|
|
|
throw new Error('There are no API keys configured yet. Please, create one to use webhooks.');
|
|
}
|
|
|
|
// Return the first API key
|
|
return apiKeys[0].key;
|
|
}
|
|
}
|