diff --git a/apps/web/pages/api/recorded-daily-video.ts b/apps/web/pages/api/recorded-daily-video.ts index 9d6fc4d5d4..035f2317c7 100644 --- a/apps/web/pages/api/recorded-daily-video.ts +++ b/apps/web/pages/api/recorded-daily-video.ts @@ -7,7 +7,9 @@ import { getDownloadLinkOfCalVideoByRecordingId, submitBatchProcessorTranscriptionJob, } from "@calcom/core/videoClient"; +import { getAllTranscriptsAccessLinkFromRoomName } from "@calcom/core/videoClient"; import { sendDailyVideoRecordingEmails } from "@calcom/emails"; +import { sendDailyVideoTranscriptEmails } from "@calcom/emails"; import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks"; import sendPayload from "@calcom/features/webhooks/lib/sendOrSchedulePayload"; import getOrgIdFromMemberOrTeamId from "@calcom/lib/getOrgIdFromMemberOrTeamId"; @@ -21,26 +23,40 @@ import type { CalendarEvent } from "@calcom/types/Calendar"; const log = logger.getSubLogger({ prefix: ["daily-video-webhook-handler"] }); -const schema = z +const commonSchema = z .object({ version: z.string(), type: z.string(), id: z.string(), - payload: z.object({ - recording_id: z.string(), - end_ts: z.number().optional(), - room_name: z.string(), - start_ts: z.number().optional(), - status: z.string(), - - max_participants: z.number().optional(), - duration: z.number().optional(), - s3_key: z.string().optional(), - }), event_ts: z.number().optional(), }) .passthrough(); +const meetingEndedSchema = commonSchema.extend({ + payload: z + .object({ + meeting_id: z.string(), + end_ts: z.number().optional(), + room: z.string(), + start_ts: z.number().optional(), + }) + .passthrough(), +}); + +const recordingReadySchema = commonSchema.extend({ + payload: z.object({ + recording_id: z.string(), + end_ts: z.number().optional(), + room_name: z.string(), + start_ts: z.number().optional(), + status: z.string(), + + max_participants: z.number().optional(), + duration: z.number().optional(), + s3_key: z.string().optional(), + }), +}); + const downloadLinkSchema = z.object({ download_link: z.string(), }); @@ -98,6 +114,102 @@ const triggerWebhook = async ({ await Promise.all(promises); }; +const getBooking = async (bookingId: number) => { + const booking = await prisma.booking.findUniqueOrThrow({ + where: { + id: bookingId, + }, + select: { + ...bookingMinimalSelect, + uid: true, + location: true, + isRecorded: true, + eventTypeId: true, + eventType: { + select: { + teamId: true, + parentId: true, + }, + }, + user: { + select: { + id: true, + timeZone: true, + email: true, + name: true, + locale: true, + destinationCalendar: true, + }, + }, + }, + }); + + if (!booking) { + log.error( + "Couldn't find Booking Id:", + safeStringify({ + bookingId, + }) + ); + } + return booking; +}; + +const getBookingReference = async (roomName: string) => { + const bookingReference = await prisma.bookingReference.findFirst({ + where: { type: "daily_video", uid: roomName, meetingId: roomName }, + select: { bookingId: true }, + }); + + if (!bookingReference) { + log.error( + "bookingReference not found error:", + safeStringify({ + bookingReference, + roomName, + }) + ); + } + + return bookingReference; +}; + +const getCalendarEvent = async (booking: Awaited>) => { + const t = await getTranslation(booking?.user?.locale ?? "en", "common"); + + const attendeesListPromises = booking.attendees.map(async (attendee) => { + return { + id: attendee.id, + name: attendee.name, + email: attendee.email, + timeZone: attendee.timeZone, + language: { + translate: await getTranslation(attendee.locale ?? "en", "common"), + locale: attendee.locale ?? "en", + }, + }; + }); + + const attendeesList = await Promise.all(attendeesListPromises); + const evt: CalendarEvent = { + type: booking.title, + title: booking.title, + description: booking.description || undefined, + startTime: booking.startTime.toISOString(), + endTime: booking.endTime.toISOString(), + organizer: { + email: booking?.userPrimaryEmail || booking.user?.email || "Email-less", + name: booking.user?.name || "Nameless", + timeZone: booking.user?.timeZone || "Europe/London", + language: { translate: t, locale: booking?.user?.locale ?? "en" }, + }, + attendees: attendeesList, + uid: booking.uid, + }; + + return Promise.resolve(evt); +}; + const testRequestSchema = z.object({ test: z.enum(["test"]), }); @@ -125,164 +237,121 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { return res.status(403).json({ message: "Signature does not match" }); } - const response = schema.safeParse(req.body); - log.debug( - "Daily video recording webhook Request Body:", + "Daily video webhook Request Body:", safeStringify({ - response, + body: req.body, }) ); - if (!response.success || response.data.type !== "recording.ready-to-download") { - return res.status(400).send({ - message: "Invalid Payload", - }); - } - - const { room_name, recording_id, status } = response.data.payload; - - if (status !== "finished") { - return res.status(400).send({ - message: "Recording not finished", - }); - } - try { - const bookingReference = await prisma.bookingReference.findFirst({ - where: { type: "daily_video", uid: room_name, meetingId: room_name }, - select: { bookingId: true }, - }); + if (req.body?.type === "recording.ready-to-download") { + const recordingReadyResponse = recordingReadySchema.safeParse(req.body); - if (!bookingReference || !bookingReference.bookingId) { - log.error( - "bookingReference not found error:", - safeStringify({ - bookingReference, - room_name, - recording_id, - }) - ); - return res.status(200).send({ message: "Booking reference not found" }); - } + if (!recordingReadyResponse.success) { + return res.status(400).send({ + message: "Invalid Payload", + }); + } - const booking = await prisma.booking.findUniqueOrThrow({ - where: { - id: bookingReference.bookingId, - }, - select: { - ...bookingMinimalSelect, - uid: true, - location: true, - isRecorded: true, - eventTypeId: true, - eventType: { - select: { - teamId: true, - parentId: true, - }, + 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); + + if (!bookingReference || !bookingReference.bookingId) { + return res.status(200).send({ message: "Booking reference not found" }); + } + + const booking = await getBooking(bookingReference.bookingId); + + if (!booking) { + return res.status(404).send({ + message: `Booking of room_name ${room_name} does not exist or does not contain daily video as location`, + }); + } + + const evt = await getCalendarEvent(booking); + + await prisma.booking.update({ + where: { + uid: booking.uid, }, - user: { - select: { - id: true, - timeZone: true, - email: true, - name: true, - locale: true, - destinationCalendar: true, - }, + data: { + isRecorded: true, }, - }, - }); - - if (!booking) { - log.error( - "Booking:", - safeStringify({ - booking, - }) - ); - - return res.status(404).send({ - message: `Booking of room_name ${room_name} does not exist or does not contain daily video as location`, }); - } - const t = await getTranslation(booking?.user?.locale ?? "en", "common"); - const attendeesListPromises = booking.attendees.map(async (attendee) => { - return { - id: attendee.id, - name: attendee.name, - email: attendee.email, - timeZone: attendee.timeZone, - language: { - translate: await getTranslation(attendee.locale ?? "en", "common"), - locale: attendee.locale ?? "en", + const response = await getDownloadLinkOfCalVideoByRecordingId(recording_id); + const downloadLinkResponse = downloadLinkSchema.parse(response); + const downloadLink = downloadLinkResponse.download_link; + + const teamId = await getTeamIdFromEventType({ + eventType: { + team: { id: booking?.eventType?.teamId ?? null }, + parentId: booking?.eventType?.parentId ?? null, }, - }; - }); + }); - const attendeesList = await Promise.all(attendeesListPromises); + await triggerWebhook({ + evt, + downloadLink, + booking: { + userId: booking?.user?.id, + eventTypeId: booking.eventTypeId, + eventTypeParentId: booking.eventType?.parentId, + teamId, + }, + }); - await prisma.booking.update({ - where: { - uid: booking.uid, - }, - data: { - isRecorded: true, - }, - }); + try { + // Submit Transcription Batch Processor Job + await submitBatchProcessorTranscriptionJob(recording_id); + } catch (err) { + log.error("Failed to Submit Transcription Batch Processor Job:", safeStringify(err)); + } - const response = await getDownloadLinkOfCalVideoByRecordingId(recording_id); - const downloadLinkResponse = downloadLinkSchema.parse(response); - const downloadLink = downloadLinkResponse.download_link; + // 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 evt: CalendarEvent = { - type: booking.title, - title: booking.title, - description: booking.description || undefined, - startTime: booking.startTime.toISOString(), - endTime: booking.endTime.toISOString(), - organizer: { - email: booking?.userPrimaryEmail || booking.user?.email || "Email-less", - name: booking.user?.name || "Nameless", - timeZone: booking.user?.timeZone || "Europe/London", - language: { translate: t, locale: booking?.user?.locale ?? "en" }, - }, - attendees: attendeesList, - uid: booking.uid, - }; + const bookingReference = await getBookingReference(room); + if (!bookingReference || !bookingReference.bookingId) { + return res.status(200).send({ message: "Booking reference not found" }); + } - const teamId = await getTeamIdFromEventType({ - eventType: { - team: { id: booking?.eventType?.teamId ?? null }, - parentId: booking?.eventType?.parentId ?? null, - }, - }); + const booking = await getBooking(bookingReference.bookingId); - await triggerWebhook({ - evt, - downloadLink, - booking: { - userId: booking?.user?.id, - eventTypeId: booking.eventTypeId, - eventTypeParentId: booking.eventType?.parentId, - teamId, - }, - }); + if (!booking) { + return res.status(404).send({ + message: `Booking of room_name ${room} does not exist or does not contain daily video as location`, + }); + } - try { - // Submit Transcription Batch Processor Job - await submitBatchProcessorTranscriptionJob(recording_id); - } catch (err) { - log.error("Failed to Submit Transcription Batch Processor Job:", safeStringify(err)); + 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" }); } - - // send emails to all attendees only when user has team plan - await sendDailyVideoRecordingEmails(evt, downloadLink); - return res.status(200).json({ message: "Success" }); } catch (err) { - console.error("Error in /recorded-daily-video", err); + log.error("Error in /recorded-daily-video", err); return res.status(500).json({ message: "something went wrong" }); } } diff --git a/apps/web/pages/api/send-daily-video-transcript.ts b/apps/web/pages/api/send-daily-video-transcript.ts deleted file mode 100644 index dbb296a34c..0000000000 --- a/apps/web/pages/api/send-daily-video-transcript.ts +++ /dev/null @@ -1,182 +0,0 @@ -import { createHmac } from "crypto"; -import type { NextApiRequest, NextApiResponse } from "next"; -import { z } from "zod"; - -import { getAllTranscriptsAccessLinkFromRoomName } from "@calcom/core/videoClient"; -import { sendDailyVideoTranscriptEmails } from "@calcom/emails"; -import logger from "@calcom/lib/logger"; -import { safeStringify } from "@calcom/lib/safeStringify"; -import { defaultHandler } from "@calcom/lib/server"; -import { getTranslation } from "@calcom/lib/server/i18n"; -import prisma, { bookingMinimalSelect } from "@calcom/prisma"; -import type { CalendarEvent } from "@calcom/types/Calendar"; - -const testRequestSchema = z.object({ - test: z.enum(["test"]), -}); - -const log = logger.getSubLogger({ prefix: ["send-daily-video-transcript-handler"] }); - -const schema = z - .object({ - version: z.string(), - type: z.string(), - id: z.string(), - payload: z - .object({ - meeting_id: z.string(), - end_ts: z.number().optional(), - room: z.string(), - start_ts: z.number().optional(), - }) - .passthrough(), - event_ts: z.number().optional(), - }) - .passthrough(); - -async function handler(req: NextApiRequest, res: NextApiResponse) { - if (testRequestSchema.safeParse(req.body).success) { - return res.status(200).json({ message: "Test request successful" }); - } - - const hmacSecret = process.env.DAILY_MEETING_ENDED_WEBHOOK_SECRET; - if (!hmacSecret) { - return res.status(405).json({ message: "No Daily Webhook Secret" }); - } - - const signature = `${req.headers["x-webhook-timestamp"]}.${JSON.stringify(req.body)}`; - const base64DecodedSecret = Buffer.from(hmacSecret, "base64"); - const hmac = createHmac("sha256", base64DecodedSecret); - const computed_signature = hmac.update(signature).digest("base64"); - - if (req.headers["x-webhook-signature"] !== computed_signature) { - return res.status(403).json({ message: "Signature does not match" }); - } - - const response = schema.safeParse(req.body); - - log.debug( - "Daily video transcript webhook Request Body:", - safeStringify({ - response, - }) - ); - - if (!response.success || response.data.type !== "meeting.ended") { - return res.status(400).send({ - message: "Invalid Payload", - }); - } - - const { room, meeting_id } = response.data.payload; - - try { - const bookingReference = await prisma.bookingReference.findFirst({ - where: { type: "daily_video", uid: room, meetingId: room }, - select: { bookingId: true }, - }); - - if (!bookingReference || !bookingReference.bookingId) { - log.error( - "bookingReference Not found:", - safeStringify({ - bookingReference, - requestBody: req.body, - }) - ); - return res.status(404).send({ message: "Booking reference not found" }); - } - - const booking = await prisma.booking.findUniqueOrThrow({ - where: { - id: bookingReference.bookingId, - }, - select: { - ...bookingMinimalSelect, - uid: true, - location: true, - isRecorded: true, - eventTypeId: true, - eventType: { - select: { - teamId: true, - parentId: true, - }, - }, - user: { - select: { - id: true, - timeZone: true, - email: true, - name: true, - locale: true, - destinationCalendar: true, - }, - }, - }, - }); - - if (!booking) { - log.error( - "Booking Not Found:", - safeStringify({ - booking, - requestBody: req.body, - }) - ); - - return res.status(404).send({ - message: `Booking of room_name ${room} does not exist or does not contain daily video as location`, - }); - } - - const transcripts = await getAllTranscriptsAccessLinkFromRoomName(room); - - if (!transcripts || !transcripts.length) - return res.status(200).json({ message: `No Transcripts found for room name ${room}` }); - - const t = await getTranslation(booking?.user?.locale ?? "en", "common"); - const attendeesListPromises = booking.attendees.map(async (attendee) => { - return { - id: attendee.id, - name: attendee.name, - email: attendee.email, - timeZone: attendee.timeZone, - language: { - translate: await getTranslation(attendee.locale ?? "en", "common"), - locale: attendee.locale ?? "en", - }, - }; - }); - - const attendeesList = await Promise.all(attendeesListPromises); - - // Send emails - const evt: CalendarEvent = { - type: booking.title, - title: booking.title, - description: booking.description || undefined, - startTime: booking.startTime.toISOString(), - endTime: booking.endTime.toISOString(), - organizer: { - email: booking?.userPrimaryEmail || booking.user?.email || "Email-less", - name: booking.user?.name || "Nameless", - timeZone: booking.user?.timeZone || "Europe/London", - language: { translate: t, locale: booking?.user?.locale ?? "en" }, - }, - attendees: attendeesList, - uid: booking.uid, - }; - - await sendDailyVideoTranscriptEmails(evt, transcripts); - - return res.status(200).json({ message: "Success" }); - } catch (err) { - console.error("Error in /send-daily-video-transcript", err); - return res.status(500).json({ message: "something went wrong" }); - } -} - -export default defaultHandler({ - POST: Promise.resolve({ default: handler }), -});