2
0
Files
cal/calcom/apps/web/pages/api/recorded-daily-video.ts
2024-08-09 00:39:27 +02:00

232 lines
7.8 KiB
TypeScript

import { createHmac } from "crypto";
import type { NextApiRequest, NextApiResponse } from "next";
import { getRoomNameFromRecordingId, getBatchProcessorJobAccessLink } from "@calcom/app-store/dailyvideo/lib";
import {
getDownloadLinkOfCalVideoByRecordingId,
submitBatchProcessorTranscriptionJob,
} from "@calcom/core/videoClient";
import { getAllTranscriptsAccessLinkFromRoomName } from "@calcom/core/videoClient";
import { sendDailyVideoRecordingEmails } from "@calcom/emails";
import { sendDailyVideoTranscriptEmails } from "@calcom/emails";
import { getTeamIdFromEventType } from "@calcom/lib/getTeamIdFromEventType";
import { HttpError } from "@calcom/lib/http-error";
import logger from "@calcom/lib/logger";
import { safeStringify } from "@calcom/lib/safeStringify";
import { defaultHandler } from "@calcom/lib/server";
import prisma from "@calcom/prisma";
import { getBooking } from "@calcom/web/lib/daily-webhook/getBooking";
import { getBookingReference } from "@calcom/web/lib/daily-webhook/getBookingReference";
import { getCalendarEvent } from "@calcom/web/lib/daily-webhook/getCalendarEvent";
import {
meetingEndedSchema,
recordingReadySchema,
batchProcessorJobFinishedSchema,
downloadLinkSchema,
testRequestSchema,
} from "@calcom/web/lib/daily-webhook/schema";
import {
triggerRecordingReadyWebhook,
triggerTranscriptionGeneratedWebhook,
} from "@calcom/web/lib/daily-webhook/triggerWebhooks";
const log = logger.getSubLogger({ prefix: ["daily-video-webhook-handler"] });
const computeSignature = (
hmacSecret: string,
reqBody: NextApiRequest["body"],
webhookTimestampHeader: string | string[] | undefined
) => {
const signature = `${webhookTimestampHeader}.${JSON.stringify(reqBody)}`;
const base64DecodedSecret = Buffer.from(hmacSecret, "base64");
const hmac = createHmac("sha256", base64DecodedSecret);
const computed_signature = hmac.update(signature).digest("base64");
return computed_signature;
};
const getDownloadLinkOfCalVideo = async (recordingId: string) => {
const response = await getDownloadLinkOfCalVideoByRecordingId(recordingId);
const downloadLinkResponse = downloadLinkSchema.parse(response);
const downloadLink = downloadLinkResponse.download_link;
return downloadLink;
};
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (!process.env.SENDGRID_API_KEY || !process.env.SENDGRID_EMAIL) {
return res.status(405).json({ message: "No SendGrid API key or email" });
}
if (testRequestSchema.safeParse(req.body).success) {
return res.status(200).json({ message: "Test request successful" });
}
const testMode = process.env.NEXT_PUBLIC_IS_E2E || process.env.INTEGRATION_TEST_MODE;
if (!testMode) {
const hmacSecret = process.env.DAILY_WEBHOOK_SECRET;
if (!hmacSecret) {
return res.status(405).json({ message: "No Daily Webhook Secret" });
}
const computed_signature = computeSignature(hmacSecret, req.body, req.headers["x-webhook-timestamp"]);
if (req.headers["x-webhook-signature"] !== computed_signature) {
return res.status(403).json({ message: "Signature does not match" });
}
}
log.debug(
"Daily video webhook Request Body:",
safeStringify({
body: req.body,
})
);
try {
if (req.body?.type === "recording.ready-to-download") {
const recordingReadyResponse = recordingReadySchema.safeParse(req.body);
if (!recordingReadyResponse.success) {
return res.status(400).send({
message: "Invalid Payload",
});
}
const { room_name, recording_id, status } = recordingReadyResponse.data.payload;
if (status !== "finished") {
return res.status(400).send({
message: "Recording not finished",
});
}
const bookingReference = await getBookingReference(room_name);
const booking = await getBooking(bookingReference.bookingId as number);
const evt = await getCalendarEvent(booking);
await prisma.booking.update({
where: {
uid: booking.uid,
},
data: {
isRecorded: true,
},
});
const downloadLink = await getDownloadLinkOfCalVideo(recording_id);
const teamId = await getTeamIdFromEventType({
eventType: {
team: { id: booking?.eventType?.teamId ?? null },
parentId: booking?.eventType?.parentId ?? null,
},
});
await triggerRecordingReadyWebhook({
evt,
downloadLink,
booking: {
userId: booking?.user?.id,
eventTypeId: booking.eventTypeId,
eventTypeParentId: booking.eventType?.parentId,
teamId,
},
});
try {
// Submit Transcription Batch Processor Job
await submitBatchProcessorTranscriptionJob(recording_id);
} catch (err) {
log.error("Failed to Submit Transcription Batch Processor Job:", safeStringify(err));
}
// send emails to all attendees only when user has team plan
await sendDailyVideoRecordingEmails(evt, downloadLink);
return res.status(200).json({ message: "Success" });
} else if (req.body.type === "meeting.ended") {
const meetingEndedResponse = meetingEndedSchema.safeParse(req.body);
if (!meetingEndedResponse.success) {
return res.status(400).send({
message: "Invalid Payload",
});
}
const { room } = meetingEndedResponse.data.payload;
const bookingReference = await getBookingReference(room);
const booking = await getBooking(bookingReference.bookingId as number);
const transcripts = await getAllTranscriptsAccessLinkFromRoomName(room);
if (!transcripts || !transcripts.length)
return res.status(200).json({ message: `No Transcripts found for room name ${room}` });
const evt = await getCalendarEvent(booking);
await sendDailyVideoTranscriptEmails(evt, transcripts);
return res.status(200).json({ message: "Success" });
} else if (req.body?.type === "batch-processor.job-finished") {
const batchProcessorJobFinishedResponse = batchProcessorJobFinishedSchema.safeParse(req.body);
if (!batchProcessorJobFinishedResponse.success) {
return res.status(400).send({
message: "Invalid Payload",
});
}
const { id, input } = batchProcessorJobFinishedResponse.data.payload;
const roomName = await getRoomNameFromRecordingId(input.recordingId);
const bookingReference = await getBookingReference(roomName);
const booking = await getBooking(bookingReference.bookingId as number);
const teamId = await getTeamIdFromEventType({
eventType: {
team: { id: booking?.eventType?.teamId ?? null },
parentId: booking?.eventType?.parentId ?? null,
},
});
const evt = await getCalendarEvent(booking);
const recording = await getDownloadLinkOfCalVideo(input.recordingId);
const batchProcessorJobAccessLink = await getBatchProcessorJobAccessLink(id);
await triggerTranscriptionGeneratedWebhook({
evt,
downloadLinks: {
transcription: batchProcessorJobAccessLink.transcription,
recording,
},
booking: {
userId: booking?.user?.id,
eventTypeId: booking.eventTypeId,
eventTypeParentId: booking.eventType?.parentId,
teamId,
},
});
return res.status(200).json({ message: "Success" });
} else {
log.error("Invalid type in /recorded-daily-video", req.body);
return res.status(200).json({ message: "Invalid type in /recorded-daily-video" });
}
} catch (err) {
log.error("Error in /recorded-daily-video", err);
if (err instanceof HttpError) {
return res.status(err.statusCode).json({ message: err.message });
} else {
return res.status(500).json({ message: "something went wrong" });
}
}
}
export default defaultHandler({
POST: Promise.resolve({ default: handler }),
});