- Create new @link-stack/logger package wrapping Pino for structured logging - Replace all console.log/error/warn statements across the monorepo - Configure environment-aware logging (pretty-print in dev, JSON in prod) - Add automatic redaction of sensitive fields (passwords, tokens, etc.) - Remove dead commented-out logger file from bridge-worker - Follow Pino's standard argument order (context object first, message second) - Support log levels via LOG_LEVEL environment variable - Export TypeScript types for better IDE support This provides consistent, structured logging across all applications and packages, making debugging easier and production logs more parseable.
104 lines
2.7 KiB
TypeScript
104 lines
2.7 KiB
TypeScript
import Wreck from "@hapi/wreck";
|
|
import { withDb, AppDatabase } from "../../lib/db.js";
|
|
import { twilioClientFor } from "../../lib/common.js";
|
|
import { CallInstance } from "twilio/lib/rest/api/v2010/account/call";
|
|
import workerUtils from "../../lib/utils.js";
|
|
import { createLogger } from "@link-stack/logger";
|
|
|
|
const logger = createLogger('bridge-worker-twilio-recording');
|
|
|
|
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) {
|
|
logger.error(error.output);
|
|
return { error: error.output };
|
|
}
|
|
};
|
|
|
|
const formatPayload = (
|
|
call: CallInstance,
|
|
recording: Buffer,
|
|
): 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,
|
|
recording: Buffer,
|
|
) => {
|
|
const webhooks = await db.webhooks.findAllByBackendId("voice", voiceLineId);
|
|
if (webhooks && webhooks.length === 0) return;
|
|
|
|
webhooks.forEach(({ id }: any) => {
|
|
const payload = formatPayload(call, recording);
|
|
workerUtils.addJob(
|
|
"notify-webhook",
|
|
{
|
|
payload,
|
|
webhookId: id,
|
|
},
|
|
{
|
|
// this de-depuplicates the job
|
|
jobKey: `webhook-${id}-call-${call.sid}`,
|
|
},
|
|
);
|
|
});
|
|
};
|
|
|
|
interface TwilioRecordingTaskOptions {
|
|
accountSid: string;
|
|
callSid: string;
|
|
recordingSid: string;
|
|
voiceLineId: string;
|
|
}
|
|
|
|
const twilioRecordingTask = async (
|
|
options: TwilioRecordingTaskOptions,
|
|
): 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;
|