232 lines
7.8 KiB
TypeScript
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 }),
|
|
});
|