mirror of
https://github.com/hanzoai/calendar.git
synced 2026-10-30 14:46:43 +00:00
* perf: improve cal video webhook * save format * refactor: use errorMs
240 lines
8.6 KiB
TypeScript
240 lines
8.6 KiB
TypeScript
import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir";
|
|
import { createHmac } from "node:crypto";
|
|
import { headers } from "next/headers";
|
|
import type { NextRequest } from "next/server";
|
|
import { NextResponse } from "next/server";
|
|
|
|
import { getRoomNameFromRecordingId, getBatchProcessorJobAccessLink } from "@calcom/app-store/dailyvideo/lib";
|
|
import { BookingRepository } from "@calcom/features/bookings/repositories/BookingRepository";
|
|
import {
|
|
sendDailyVideoRecordingEmails,
|
|
sendDailyVideoTranscriptEmails,
|
|
} from "@calcom/emails/daily-video-emails";
|
|
import {
|
|
getAllTranscriptsAccessLinkFromMeetingId,
|
|
submitBatchProcessorTranscriptionJob,
|
|
} from "@calcom/features/conferencing/lib/videoClient";
|
|
import { WEBAPP_URL } from "@calcom/lib/constants";
|
|
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 { generateVideoToken } from "@calcom/lib/videoTokens";
|
|
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,
|
|
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: any, webhookTimestampHeader: string | null) => {
|
|
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 getProxyDownloadLinkOfCalVideo = async (recordingId: string) => {
|
|
const token = generateVideoToken(recordingId);
|
|
const downloadLink = `${WEBAPP_URL}/api/video/recording?token=${token}`;
|
|
return downloadLink;
|
|
};
|
|
|
|
export async function postHandler(request: NextRequest) {
|
|
const body = await request.json();
|
|
|
|
if (testRequestSchema.safeParse(body).success) {
|
|
return NextResponse.json({ message: "Test request successful" });
|
|
}
|
|
|
|
const headersList = await headers();
|
|
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 NextResponse.json({ message: "No Daily Webhook Secret" }, { status: 405 });
|
|
}
|
|
|
|
const webhookTimestamp = headersList.get("x-webhook-timestamp");
|
|
const computed_signature = computeSignature(hmacSecret, body, webhookTimestamp);
|
|
|
|
if (headersList.get("x-webhook-signature") !== computed_signature) {
|
|
return NextResponse.json({ message: "Signature does not match" }, { status: 403 });
|
|
}
|
|
}
|
|
|
|
log.info(
|
|
"Daily video webhook Request Body:",
|
|
safeStringify({
|
|
body,
|
|
})
|
|
);
|
|
|
|
try {
|
|
if (body?.type === "recording.ready-to-download") {
|
|
const recordingReadyResponse = recordingReadySchema.safeParse(body);
|
|
|
|
if (!recordingReadyResponse.success) {
|
|
return NextResponse.json({ message: "Invalid Payload" }, { status: 400 });
|
|
}
|
|
|
|
const { room_name, recording_id, status } = recordingReadyResponse.data.payload;
|
|
|
|
if (status !== "finished") {
|
|
return NextResponse.json({ message: "Recording not finished" }, { status: 400 });
|
|
}
|
|
|
|
const bookingReference = await getBookingReference(room_name);
|
|
const booking = await getBooking(bookingReference.bookingId as number);
|
|
|
|
const bookingRepository = new BookingRepository(prisma);
|
|
|
|
const [evt, updateRecordStatus, downloadLink, teamId] = await Promise.all([
|
|
getCalendarEvent(booking),
|
|
bookingRepository.updateRecordedStatus({
|
|
bookingUid: booking.uid,
|
|
isRecorded: true,
|
|
}),
|
|
getProxyDownloadLinkOfCalVideo(recording_id),
|
|
getTeamIdFromEventType({
|
|
eventType: {
|
|
team: { id: booking?.eventType?.teamId ?? null },
|
|
parentId: booking?.eventType?.parentId ?? null,
|
|
},
|
|
}),
|
|
]);
|
|
|
|
const tasks = [
|
|
{
|
|
fn: triggerRecordingReadyWebhook({
|
|
evt,
|
|
downloadLink,
|
|
booking: {
|
|
userId: booking?.user?.id,
|
|
eventTypeId: booking.eventTypeId,
|
|
eventTypeParentId: booking.eventType?.parentId,
|
|
teamId,
|
|
},
|
|
}),
|
|
errorMsg: "trigger recording ready webhook",
|
|
},
|
|
{
|
|
fn: submitBatchProcessorTranscriptionJob(recording_id),
|
|
errorMsg: "submit transcription batch processor job",
|
|
},
|
|
{
|
|
fn: sendDailyVideoRecordingEmails(evt, downloadLink),
|
|
errorMsg: "send recording emails",
|
|
},
|
|
];
|
|
|
|
const results = await Promise.allSettled(tasks.map((t) => t.fn));
|
|
|
|
results.forEach((result, index) => {
|
|
if (result.status === "rejected") {
|
|
log.error(`Failed to ${tasks[index].errorMsg}:`, safeStringify(result.reason));
|
|
}
|
|
});
|
|
|
|
return NextResponse.json({ message: "Success" });
|
|
} else if (body.type === "meeting.ended") {
|
|
const meetingEndedResponse = meetingEndedSchema.safeParse(body);
|
|
if (!meetingEndedResponse.success) {
|
|
return NextResponse.json({ message: "Invalid Payload" }, { status: 400 });
|
|
}
|
|
|
|
const { room, meeting_id } = meetingEndedResponse.data.payload;
|
|
|
|
const bookingReference = await getBookingReference(room);
|
|
const booking = await getBooking(bookingReference.bookingId as number);
|
|
|
|
if (!booking.eventType?.canSendCalVideoTranscriptionEmails) {
|
|
return NextResponse.json({
|
|
message: `Transcription emails are disabled for this event type ${booking.eventTypeId}`,
|
|
});
|
|
}
|
|
|
|
const transcripts = await getAllTranscriptsAccessLinkFromMeetingId(meeting_id);
|
|
|
|
if (!transcripts || !transcripts.length)
|
|
return NextResponse.json({
|
|
message: `No Transcripts found for room name ${room} and meeting id ${meeting_id}`,
|
|
});
|
|
|
|
const evt = await getCalendarEvent(booking);
|
|
await sendDailyVideoTranscriptEmails(evt, transcripts);
|
|
|
|
return NextResponse.json({ message: "Success" });
|
|
} else if (body?.type === "batch-processor.job-finished") {
|
|
const batchProcessorJobFinishedResponse = batchProcessorJobFinishedSchema.safeParse(body);
|
|
|
|
if (!batchProcessorJobFinishedResponse.success) {
|
|
return NextResponse.json({ message: "Invalid Payload" }, { status: 400 });
|
|
}
|
|
|
|
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, recording, batchProcessorJobAccessLink] = await Promise.all([
|
|
getCalendarEvent(booking),
|
|
getProxyDownloadLinkOfCalVideo(input.recordingId),
|
|
getBatchProcessorJobAccessLink(id),
|
|
]);
|
|
|
|
await triggerTranscriptionGeneratedWebhook({
|
|
evt,
|
|
downloadLinks: {
|
|
transcription: batchProcessorJobAccessLink.transcription,
|
|
recording,
|
|
},
|
|
booking: {
|
|
userId: booking?.user?.id,
|
|
eventTypeId: booking.eventTypeId,
|
|
eventTypeParentId: booking.eventType?.parentId,
|
|
teamId,
|
|
},
|
|
});
|
|
|
|
return NextResponse.json({ message: "Success" });
|
|
} else {
|
|
log.error("Invalid type in /recorded-daily-video", body);
|
|
return NextResponse.json({
|
|
message: "Invalid type in /recorded-daily-video",
|
|
});
|
|
}
|
|
} catch (err) {
|
|
log.error("Error in /recorded-daily-video", err);
|
|
|
|
if (err instanceof HttpError) {
|
|
return NextResponse.json({ message: err.message }, { status: err.statusCode });
|
|
} else {
|
|
return NextResponse.json({ message: "something went wrong" }, { status: 500 });
|
|
}
|
|
}
|
|
}
|
|
|
|
export const POST = defaultResponderForAppDir(postHandler);
|