2
0

first commit

This commit is contained in:
2024-08-09 00:39:27 +02:00
commit 79688abe2e
5698 changed files with 497838 additions and 0 deletions

View File

@@ -0,0 +1,55 @@
import { HttpError } from "@calcom/lib/http-error";
import logger from "@calcom/lib/logger";
import { safeStringify } from "@calcom/lib/safeStringify";
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
const log = logger.getSubLogger({ prefix: ["daily-video-webhook-handler"] });
// TODO: use BookingRepository
export 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,
})
);
throw new HttpError({
message: `Booking of id ${bookingId} does not exist or does not contain daily video as location`,
statusCode: 404,
});
}
return booking;
};
export type getBookingResponse = Awaited<ReturnType<typeof getBooking>>;

View File

@@ -0,0 +1,24 @@
import { HttpError } from "@calcom/lib/http-error";
import logger from "@calcom/lib/logger";
import { safeStringify } from "@calcom/lib/safeStringify";
import { BookingReferenceRepository } from "@calcom/lib/server/repository/bookingReference";
const log = logger.getSubLogger({ prefix: ["daily-video-webhook-handler"] });
export const getBookingReference = async (roomName: string) => {
const bookingReference = await BookingReferenceRepository.findDailyVideoReferenceByRoomName({ roomName });
if (!bookingReference || !bookingReference.bookingId) {
log.error(
"bookingReference not found error:",
safeStringify({
bookingReference,
roomName,
})
);
throw new HttpError({ message: "Booking reference not found", statusCode: 200 });
}
return bookingReference;
};

View File

@@ -0,0 +1,40 @@
import { getTranslation } from "@calcom/lib/server/i18n";
import type { CalendarEvent } from "@calcom/types/Calendar";
import type { getBookingResponse } from "./getBooking";
export const getCalendarEvent = async (booking: getBookingResponse) => {
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);
};

View File

@@ -0,0 +1,63 @@
import { z } from "zod";
const commonSchema = z
.object({
version: z.string(),
type: z.string(),
id: z.string(),
event_ts: z.number().optional(),
})
.passthrough();
export 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(),
});
export 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(),
}),
});
export const batchProcessorJobFinishedSchema = commonSchema.extend({
payload: z
.object({
id: z.string(),
status: z.string(),
input: z.object({
sourceType: z.string(),
recordingId: z.string(),
}),
output: z
.object({
transcription: z.array(z.object({ format: z.string() }).passthrough()),
})
.passthrough(),
})
.passthrough(),
});
export type TBatchProcessorJobFinished = z.infer<typeof batchProcessorJobFinishedSchema>;
export const downloadLinkSchema = z.object({
download_link: z.string(),
});
export const testRequestSchema = z.object({
test: z.enum(["test"]),
});

View File

@@ -0,0 +1,244 @@
import {
createBookingScenario,
getScenarioData,
TestData,
getDate,
getMockBookingAttendee,
getOrganizer,
getBooker,
} from "@calcom/web/test/utils/bookingScenario/bookingScenario";
import { expectWebhookToHaveBeenCalledWith } from "@calcom/web/test/utils/bookingScenario/expects";
import type { Request, Response } from "express";
import type { NextApiRequest, NextApiResponse } from "next";
import { createMocks } from "node-mocks-http";
import { describe, afterEach, test, vi, beforeEach, beforeAll } from "vitest";
import { appStoreMetadata } from "@calcom/app-store/apps.metadata.generated";
import { getRoomNameFromRecordingId, getBatchProcessorJobAccessLink } from "@calcom/app-store/dailyvideo/lib";
import { getDownloadLinkOfCalVideoByRecordingId } from "@calcom/core/videoClient";
import prisma from "@calcom/prisma";
import { WebhookTriggerEvents } from "@calcom/prisma/enums";
import { BookingStatus } from "@calcom/prisma/enums";
import handler from "@calcom/web/pages/api/recorded-daily-video";
type CustomNextApiRequest = NextApiRequest & Request;
type CustomNextApiResponse = NextApiResponse & Response;
beforeAll(() => {
// Setup env vars
vi.stubEnv("SENDGRID_API_KEY", "FAKE_SENDGRID_API_KEY");
vi.stubEnv("SENDGRID_EMAIL", "FAKE_SENDGRID_EMAIL");
});
vi.mock("@calcom/app-store/dailyvideo/lib", () => {
return {
getRoomNameFromRecordingId: vi.fn(),
getBatchProcessorJobAccessLink: vi.fn(),
};
});
vi.mock("@calcom/core/videoClient", () => {
return {
getDownloadLinkOfCalVideoByRecordingId: vi.fn(),
};
});
const BATCH_PROCESSOR_JOB_FINSISHED_PAYLOAD = {
version: "1.1.0",
type: "batch-processor.job-finished",
id: "77b1cb9e-cd79-43cd-bad6-3ccaccba26be",
payload: {
id: "77b1cb9e-cd79-43cd-bad6-3ccaccba26be",
status: "finished",
input: {
sourceType: "recordingId",
recordingId: "eb9e84de-783e-4e14-875d-94700ee4b976",
},
output: {
transcription: [
{
format: "json",
s3Config: {
key: "transcript.json",
bucket: "daily-bucket",
region: "us-west-2",
},
},
{
format: "srt",
s3Config: {
key: "transcript.srt",
bucket: "daily-bucket",
region: "us-west-2",
},
},
{
format: "txt",
s3Config: {
key: "transcript.txt",
bucket: "daily-bucket",
region: "us-west-2",
},
},
{
format: "vtt",
s3Config: {
key: "transcript.vtt",
bucket: "daily-bucket",
region: "us-west-2",
},
},
],
},
},
event_ts: 1717688213.803,
};
const timeout = process.env.CI ? 5000 : 20000;
const TRANSCRIPTION_ACCESS_LINK = {
id: "MOCK_ID",
preset: "transcript",
status: "finished",
transcription: [
{
format: "json",
link: "https://download.json",
},
{
format: "srt",
link: "https://download.srt",
},
],
};
describe("Handler: /api/recorded-daily-video", () => {
beforeEach(() => {
fetchMock.resetMocks();
});
afterEach(() => {
vi.resetAllMocks();
fetchMock.resetMocks();
});
test(
`Batch Processor Job finished triggers RECORDING_TRANSCRIPTION_GENERATED webhooks`,
async () => {
const organizer = getOrganizer({
name: "Organizer",
email: "organizer@example.com",
id: 101,
schedules: [TestData.schedules.IstWorkHours],
});
const bookingUid = "n5Wv3eHgconAED2j4gcVhP";
const iCalUID = `${bookingUid}@Cal.com`;
const subscriberUrl = "http://my-webhook.example.com";
const recordingDownloadLink = "https://download-link.com";
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
const booker = getBooker({
email: "booker@example.com",
name: "Booker",
});
await createBookingScenario(
getScenarioData({
webhooks: [
{
userId: organizer.id,
eventTriggers: [WebhookTriggerEvents.RECORDING_TRANSCRIPTION_GENERATED],
subscriberUrl,
active: true,
eventTypeId: 1,
appId: null,
},
],
eventTypes: [
{
id: 1,
slotInterval: 15,
length: 15,
users: [
{
id: 101,
},
],
},
],
bookings: [
{
uid: bookingUid,
eventTypeId: 1,
status: BookingStatus.ACCEPTED,
startTime: `${plus1DateString}T05:00:00.000Z`,
endTime: `${plus1DateString}T05:15:00.000Z`,
userId: organizer.id,
metadata: {
videoCallUrl: "https://existing-daily-video-call-url.example.com",
},
references: [
{
type: appStoreMetadata.dailyvideo.type,
uid: "MOCK_ID",
meetingId: "MOCK_ID",
meetingPassword: "MOCK_PASS",
meetingUrl: "http://mock-dailyvideo.example.com",
credentialId: null,
},
],
attendees: [
getMockBookingAttendee({
id: 2,
name: booker.name,
email: booker.email,
locale: "en",
timeZone: "Asia/Kolkata",
noShow: false,
}),
],
iCalUID,
},
],
organizer,
apps: [TestData.apps["daily-video"]],
})
);
vi.mocked(getRoomNameFromRecordingId).mockResolvedValue("MOCK_ID");
vi.mocked(getBatchProcessorJobAccessLink).mockResolvedValue(TRANSCRIPTION_ACCESS_LINK);
vi.mocked(getDownloadLinkOfCalVideoByRecordingId).mockResolvedValue({
download_link: recordingDownloadLink,
});
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "POST",
body: BATCH_PROCESSOR_JOB_FINSISHED_PAYLOAD,
prisma,
});
await handler(req, res);
await expectWebhookToHaveBeenCalledWith(subscriberUrl, {
triggerEvent: WebhookTriggerEvents.RECORDING_TRANSCRIPTION_GENERATED,
payload: {
type: "Test Booking Title",
uid: bookingUid,
downloadLinks: {
transcription: TRANSCRIPTION_ACCESS_LINK.transcription,
recording: recordingDownloadLink,
},
organizer: {
email: organizer.email,
name: organizer.name,
timeZone: organizer.timeZone,
language: { locale: "en" },
utcOffset: 330,
},
},
});
},
timeout
);
});

View File

@@ -0,0 +1,112 @@
import type { TGetTranscriptAccessLink } from "@calcom/app-store/dailyvideo/zod";
import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks";
import sendPayload from "@calcom/features/webhooks/lib/sendOrSchedulePayload";
import getOrgIdFromMemberOrTeamId from "@calcom/lib/getOrgIdFromMemberOrTeamId";
import logger from "@calcom/lib/logger";
import { safeStringify } from "@calcom/lib/safeStringify";
import { WebhookTriggerEvents } from "@calcom/prisma/enums";
import type { CalendarEvent } from "@calcom/types/Calendar";
const log = logger.getSubLogger({ prefix: ["daily-video-webhook-handler:triggerRecordingReadyWebhook"] });
type Booking = {
userId: number | undefined;
eventTypeId: number | null;
eventTypeParentId: number | null | undefined;
teamId?: number | null;
};
const getWebhooksByEventTrigger = async (eventTrigger: WebhookTriggerEvents, booking: Booking) => {
const isTeamBooking = booking.teamId;
const isBookingForManagedEventtype = booking.teamId && booking.eventTypeParentId;
const triggerForUser = !isTeamBooking || isBookingForManagedEventtype;
const organizerUserId = triggerForUser ? booking.userId : null;
const orgId = await getOrgIdFromMemberOrTeamId({ memberId: organizerUserId, teamId: booking.teamId });
const subscriberOptions = {
userId: organizerUserId,
eventTypeId: booking.eventTypeId,
triggerEvent: eventTrigger,
teamId: booking.teamId,
orgId,
};
return getWebhooks(subscriberOptions);
};
export const triggerRecordingReadyWebhook = async ({
evt,
downloadLink,
booking,
}: {
evt: CalendarEvent;
downloadLink: string;
booking: Booking;
}) => {
const eventTrigger: WebhookTriggerEvents = "RECORDING_READY";
const webhooks = await getWebhooksByEventTrigger(eventTrigger, booking);
log.debug(
"Webhooks:",
safeStringify({
webhooks,
})
);
const promises = webhooks.map((webhook) =>
sendPayload(webhook.secret, eventTrigger, new Date().toISOString(), webhook, {
...evt,
downloadLink,
}).catch((e) => {
log.error(
`Error executing webhook for event: ${eventTrigger}, URL: ${webhook.subscriberUrl}, bookingId: ${evt.bookingId}, bookingUid: ${evt.uid}`,
safeStringify(e)
);
})
);
await Promise.all(promises);
};
export const triggerTranscriptionGeneratedWebhook = async ({
evt,
downloadLinks,
booking,
}: {
evt: CalendarEvent;
downloadLinks?: {
transcription: TGetTranscriptAccessLink["transcription"];
recording: string;
};
booking: Booking;
}) => {
const webhooks = await getWebhooksByEventTrigger(
WebhookTriggerEvents.RECORDING_TRANSCRIPTION_GENERATED,
booking
);
log.debug(
"Webhooks:",
safeStringify({
webhooks,
})
);
const promises = webhooks.map((webhook) =>
sendPayload(
webhook.secret,
WebhookTriggerEvents.RECORDING_TRANSCRIPTION_GENERATED,
new Date().toISOString(),
webhook,
{
...evt,
downloadLinks,
}
).catch((e) => {
log.error(
`Error executing webhook for event: ${WebhookTriggerEvents.RECORDING_TRANSCRIPTION_GENERATED}, URL: ${webhook.subscriberUrl}, bookingId: ${evt.bookingId}, bookingUid: ${evt.uid}`,
safeStringify(e)
);
})
);
await Promise.all(promises);
};