2023-02-13 12:41:30 +00:00
|
|
|
import Wreck from "@hapi/wreck";
|
2024-05-14 15:31:44 +02:00
|
|
|
import { withDb, AppDatabase } from "../../lib/db.js";
|
|
|
|
|
import { twilioClientFor } from "../../lib/common.js";
|
2023-02-13 12:41:30 +00:00
|
|
|
import { CallInstance } from "twilio/lib/rest/api/v2010/account/call";
|
2024-05-14 15:31:44 +02:00
|
|
|
import workerUtils from "../../lib/utils.js";
|
2025-08-20 11:37:39 +02:00
|
|
|
import { createLogger } from "@link-stack/logger";
|
|
|
|
|
|
|
|
|
|
const logger = createLogger('bridge-worker-twilio-recording');
|
2023-02-13 12:41:30 +00:00
|
|
|
|
|
|
|
|
interface WebhookPayload {
|
|
|
|
|
startTime: string;
|
|
|
|
|
endTime: string;
|
|
|
|
|
to: string;
|
|
|
|
|
from: string;
|
|
|
|
|
duration: string;
|
|
|
|
|
callSid: string;
|
|
|
|
|
recording: string;
|
|
|
|
|
mimeType: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const getTwilioRecording = async (url: string) => {
|
|
|
|
|
try {
|
|
|
|
|
const { payload } = await Wreck.get(url);
|
|
|
|
|
return { recording: payload as Buffer };
|
|
|
|
|
} catch (error: any) {
|
2025-08-20 11:37:39 +02:00
|
|
|
logger.error(error.output);
|
2023-02-13 12:41:30 +00:00
|
|
|
return { error: error.output };
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const formatPayload = (
|
|
|
|
|
call: CallInstance,
|
2024-04-21 16:59:50 +02:00
|
|
|
recording: Buffer,
|
2023-02-13 12:41:30 +00:00
|
|
|
): WebhookPayload => {
|
|
|
|
|
return {
|
|
|
|
|
startTime: call.startTime.toISOString(),
|
|
|
|
|
endTime: call.endTime.toISOString(),
|
|
|
|
|
to: call.toFormatted,
|
|
|
|
|
from: call.fromFormatted,
|
|
|
|
|
duration: call.duration,
|
|
|
|
|
callSid: call.sid,
|
|
|
|
|
recording: recording.toString("base64"),
|
|
|
|
|
mimeType: "audio/mpeg",
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const notifyWebhooks = async (
|
|
|
|
|
db: AppDatabase,
|
|
|
|
|
voiceLineId: string,
|
|
|
|
|
call: CallInstance,
|
2024-04-21 16:59:50 +02:00
|
|
|
recording: Buffer,
|
2023-02-13 12:41:30 +00:00
|
|
|
) => {
|
|
|
|
|
const webhooks = await db.webhooks.findAllByBackendId("voice", voiceLineId);
|
|
|
|
|
if (webhooks && webhooks.length === 0) return;
|
|
|
|
|
|
2024-06-05 15:12:48 +02:00
|
|
|
webhooks.forEach(({ id }: any) => {
|
2023-02-13 12:41:30 +00:00
|
|
|
const payload = formatPayload(call, recording);
|
|
|
|
|
workerUtils.addJob(
|
|
|
|
|
"notify-webhook",
|
|
|
|
|
{
|
|
|
|
|
payload,
|
|
|
|
|
webhookId: id,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
// this de-depuplicates the job
|
|
|
|
|
jobKey: `webhook-${id}-call-${call.sid}`,
|
2024-04-21 16:59:50 +02:00
|
|
|
},
|
2023-02-13 12:41:30 +00:00
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
interface TwilioRecordingTaskOptions {
|
|
|
|
|
accountSid: string;
|
|
|
|
|
callSid: string;
|
|
|
|
|
recordingSid: string;
|
|
|
|
|
voiceLineId: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const twilioRecordingTask = async (
|
2024-04-21 16:59:50 +02:00
|
|
|
options: TwilioRecordingTaskOptions,
|
2023-02-13 12:41:30 +00:00
|
|
|
): Promise<void> =>
|
|
|
|
|
withDb(async (db: AppDatabase) => {
|
|
|
|
|
const { voiceLineId, accountSid, callSid, recordingSid } = options;
|
|
|
|
|
|
|
|
|
|
const voiceLine = await db.voiceLines.findById({ id: voiceLineId });
|
|
|
|
|
if (!voiceLine) return;
|
|
|
|
|
|
|
|
|
|
const provider = await db.voiceProviders.findByTwilioAccountSid(accountSid);
|
|
|
|
|
if (!provider) return;
|
|
|
|
|
|
|
|
|
|
const client = twilioClientFor(provider);
|
|
|
|
|
const meta = await client.recordings(recordingSid).fetch();
|
|
|
|
|
|
|
|
|
|
const mp3Url = "https://api.twilio.com/" + meta.uri.slice(0, -4) + "mp3";
|
|
|
|
|
const { recording, error } = await getTwilioRecording(mp3Url);
|
|
|
|
|
if (error) {
|
|
|
|
|
throw new Error(`failed to get recording for call ${callSid}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const call = await client.calls(callSid).fetch();
|
|
|
|
|
await notifyWebhooks(db, voiceLineId, call, recording!);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export default twilioRecordingTask;
|