Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

perf: move webhooks to tasker #19249

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 35 additions & 43 deletions packages/features/bookings/lib/handleNewBooking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,7 @@ import { scheduleWorkflowReminders } from "@calcom/features/ee/workflows/lib/rem
import { getFullName } from "@calcom/features/form-builder/utils";
import { UsersRepository } from "@calcom/features/users/users.repository";
import type { GetSubscriberOptions } from "@calcom/features/webhooks/lib/getWebhooks";
import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks";
import {
deleteWebhookScheduledTriggers,
scheduleTrigger,
} from "@calcom/features/webhooks/lib/scheduleTrigger";
import { deleteWebhookScheduledTriggers } from "@calcom/features/webhooks/lib/scheduleTrigger";
import { getVideoCallUrlFromCalEvent } from "@calcom/lib/CalEventParser";
import { isRerouting, shouldIgnoreContactOwner } from "@calcom/lib/bookings/routing/utils";
import { getDefaultEvent, getUsernameList } from "@calcom/lib/defaultEvents";
Expand Down Expand Up @@ -1863,14 +1859,14 @@ async function handler(

// We are here so, booking doesn't require payment and booking is also created in DB already, through createBooking call
if (isConfirmedByDefault) {
const subscribersMeetingEnded = await monitorCallbackAsync(getWebhooks, subscriberOptionsMeetingEnded);
const subscribersMeetingStarted = await monitorCallbackAsync(
getWebhooks,
subscriberOptionsMeetingStarted
);
// const subscribersMeetingEnded = await monitorCallbackAsync(getWebhooks, subscriberOptionsMeetingEnded);
// const subscribersMeetingStarted = await monitorCallbackAsync(
// getWebhooks,
// subscriberOptionsMeetingStarted
// );

let deleteWebhookScheduledTriggerPromise: Promise<unknown> = Promise.resolve();
const scheduleTriggerPromises = [];
// const scheduleTriggerPromises = [];

if (rescheduleUid && originalRescheduledBooking) {
//delete all scheduled triggers for meeting ended and meeting started of booking
Expand All @@ -1881,40 +1877,36 @@ async function handler(
}

if (booking && booking.status === BookingStatus.ACCEPTED) {
const bookingWithCalEventResponses = {
...booking,
responses: reqBody.calEventResponses,
};
for (const subscriber of subscribersMeetingEnded) {
scheduleTriggerPromises.push(
scheduleTrigger({
booking: bookingWithCalEventResponses,
subscriberUrl: subscriber.subscriberUrl,
subscriber,
triggerEvent: WebhookTriggerEvents.MEETING_ENDED,
isDryRun,
})
);
}

for (const subscriber of subscribersMeetingStarted) {
scheduleTriggerPromises.push(
scheduleTrigger({
booking: bookingWithCalEventResponses,
subscriberUrl: subscriber.subscriberUrl,
subscriber,
triggerEvent: WebhookTriggerEvents.MEETING_STARTED,
isDryRun,
})
);
}
// const bookingWithCalEventResponses = {
// ...booking,
// responses: reqBody.calEventResponses,
// };
// for (const subscriber of subscribersMeetingEnded) {
// scheduleTriggerPromises.push(
// scheduleTrigger({
// booking: bookingWithCalEventResponses,
// subscriberUrl: subscriber.subscriberUrl,
// subscriber,
// triggerEvent: WebhookTriggerEvents.MEETING_ENDED,
// isDryRun,
// })
// );
// }
// for (const subscriber of subscribersMeetingStarted) {
// scheduleTriggerPromises.push(
// scheduleTrigger({
// booking: bookingWithCalEventResponses,
// subscriberUrl: subscriber.subscriberUrl,
// subscriber,
// triggerEvent: WebhookTriggerEvents.MEETING_STARTED,
// isDryRun,
// })
// );
// }
}

await Promise.all([deleteWebhookScheduledTriggerPromise, ...scheduleTriggerPromises]).catch((error) => {
loggerWithEventDetails.error(
"Error while scheduling or canceling webhook triggers",
JSON.stringify({ error })
);
await Promise.all([deleteWebhookScheduledTriggerPromise]).catch((error) => {
loggerWithEventDetails.error("Error while canceling webhook triggers", JSON.stringify({ error }));
});

// Send Webhook call if hooked to BOOKING_CREATED & BOOKING_RESCHEDULED
Expand Down
1 change: 1 addition & 0 deletions packages/features/tasker/tasker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ type TaskPayloads = {
typeof import("./tasks/translateEventTypeData").ZTranslateEventDataPayloadSchema
>;
createCRMEvent: z.infer<typeof import("./tasks/crm/schema").createCRMEventSchema>;
triggerWebhooks: z.infer<typeof import("./tasks/triggerWebhooks/schema").ZTriggerWebhooksPayloadSchema>;
};
export type TaskTypes = keyof TaskPayloads;
export type TaskHandler = (payload: string) => Promise<void>;
Expand Down
1 change: 1 addition & 0 deletions packages/features/tasker/tasks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const tasks: Record<TaskTypes, () => Promise<TaskHandler>> = {
translateEventTypeData: () =>
import("./translateEventTypeData").then((module) => module.translateEventTypeData),
createCRMEvent: () => import("./crm/createCRMEvent").then((module) => module.createCRMEvent),
triggerWebhooks: () => import("./triggerWebhooks").then((module) => module.triggerWebhooks),
};

export default tasks;
264 changes: 264 additions & 0 deletions packages/features/tasker/tasks/triggerWebhooks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,264 @@
import { handleWebhookTrigger } from "bookings/lib/handleWebhookTrigger";

import monitorCallbackAsync from "@calcom/core/sentryWrapper";
import dayjs from "@calcom/dayjs";
import { getOriginalRescheduledBooking } from "@calcom/features/bookings/lib/handleNewBooking/getOriginalRescheduledBooking";
import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks";
import type { GetSubscriberOptions } from "@calcom/features/webhooks/lib/getWebhooks";
import { scheduleTrigger } from "@calcom/features/webhooks/lib/scheduleTrigger";
import type { EventPayloadType, EventTypeInfo } from "@calcom/features/webhooks/lib/sendPayload";
import { isPrismaObjOrUndefined } from "@calcom/lib";
import logger from "@calcom/lib/logger";
import { getTranslation } from "@calcom/lib/server/i18n";
import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat";
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
import { WebhookTriggerEvents, BookingStatus } from "@calcom/prisma/enums";
import type { CalendarEvent } from "@calcom/types/Calendar";

import { ZTriggerWebhooksPayloadSchema } from "./schema";

const log = logger.getSubLogger({ prefix: ["trigger-webhooks"] });

const getBooking = async (bookingId: number) => {
const booking = await prisma.booking.findUniqueOrThrow({
where: { id: bookingId },
select: { ...bookingMinimalSelect, status: true },
});
return booking;
};

const generateWebhookData = async (
bookingId: number,
isConfirmedByDefault: boolean
): Promise<EventPayloadType> => {
const booking = await prisma.booking.findUniqueOrThrow({
where: { id: bookingId },
select: {
...bookingMinimalSelect,
eventType: {
select: {
seatsPerTimeSlot: true,
seatsShowAttendees: true,
seatsShowAvailabilityCount: true,
schedulingType: true,
currency: true,
description: true,
id: true,
slug: true,
length: true,
price: true,
requiresConfirmation: true,
metadata: true,
title: true,
team: {
select: {
id: true,
name: true,
parentId: true,
},
},
parentId: true,
parent: {
select: {
teamId: true,
},
},
},
},
responses: true,
rescheduled: true,
fromReschedule: true,
metadata: true,
smsReminderNumber: true,
userId: true,
location: true,
payment: {
select: {
id: true,
amount: true,
},
},
user: {
select: {
username: true,
email: true,
name: true,
timeZone: true,
timeFormat: true,
locale: true,
},
},
},
});

const tOrganizer = await getTranslation(booking.user?.locale ?? "en", "common");

const originalRescheduledBooking = booking.fromReschedule
? await getOriginalRescheduledBooking(booking.fromReschedule, !!booking.eventType?.seatsPerTimeSlot)
: null;

const evt: CalendarEvent = {
type: booking?.eventType?.slug ?? "",
title: booking.title,
description: booking.description,
customInputs: isPrismaObjOrUndefined(booking.customInputs),
startTime: booking.startTime.toISOString(),
endTime: booking.endTime.toISOString(),
organizer: {
email: booking.userPrimaryEmail ?? booking.user?.email ?? "",
name: booking.user?.name || "Unnamed",
username: booking.user?.username || undefined,
timeZone: booking.user?.timeZone ?? "Europe/London",
timeFormat: getTimeFormatStringFromUserTimeFormat(booking.user?.timeFormat),
language: { translate: tOrganizer, locale: booking.user?.locale ?? "en" },
},
eventTypeId: booking.eventType?.id,
team: booking.eventType?.team
? {
id: booking.eventType.team.id,
name: booking.eventType.team.name,
members: [],
}
: undefined,
// responses: booking.responses || null,
// attendees: attendeesList,
// location: platformBookingLocation ?? bookingLocation, // Will be processed by the EventManager later.
// conferenceCredentialId,
// destinationCalendar,
// hideCalendarNotes: booking.eventType.hideCalendarNotes,
// hideCalendarEventDetails: booking.eventType.hideCalendarEventDetails,
// requiresConfirmation: !isConfirmedByDefault,
// eventTypeId: booking.eventType?.id,
// // if seats are not enabled we should default true
// seatsShowAttendees: booking.eventType?.seatsPerTimeSlot ? booking.eventType?.seatsShowAttendees : true,
// seatsPerTimeSlot: booking.eventType?.seatsPerTimeSlot,
// seatsShowAvailabilityCount: booking.eventType?.seatsPerTimeSlot
// ? booking.eventType?.seatsShowAvailabilityCount
// : true,
// schedulingType: booking.eventType?.schedulingType,
// iCalUID,
// iCalSequence,
// platformClientId,
// platformRescheduleUrl,
// platformCancelUrl,
// platformBookingUrl,
oneTimePassword: isConfirmedByDefault ? null : undefined,
};

const eventTypeInfo: EventTypeInfo = {
eventTitle: booking.eventType?.title ?? "",
eventDescription: booking.eventType?.description ?? "",
// price: booking.payment?.price ?? 0,
currency: booking.eventType?.currency ?? "USD",
length: booking.eventType?.length ?? 0,
};

const webhookData: EventPayloadType = {
...evt,
...eventTypeInfo,
bookingId: booking?.id,
rescheduleId: originalRescheduledBooking?.id || undefined,
rescheduleUid: booking.fromReschedule ?? undefined,
rescheduleStartTime: originalRescheduledBooking?.startTime
? dayjs(originalRescheduledBooking?.startTime).utc().format()
: undefined,
};

return webhookData;
};

export const triggerWebhooks = async (payload: string) => {
const { userId, eventTypeId, triggerEvent, isConfirmedByDefault, teamId, orgId, oAuthClientId, bookingId } =
ZTriggerWebhooksPayloadSchema.parse(payload);

const booking = await getBooking(bookingId);

const webhookData = generateWebhookData(bookingId, isConfirmedByDefault);

if (isConfirmedByDefault) {
if (booking && booking.status === BookingStatus.ACCEPTED) {
const subscriberOptionsMeetingEnded: GetSubscriberOptions = {
userId,
eventTypeId,
triggerEvent: WebhookTriggerEvents.MEETING_ENDED,
teamId,
orgId,
oAuthClientId,
};

const subscriberOptionsMeetingStarted: GetSubscriberOptions = {
userId,
eventTypeId,
triggerEvent: WebhookTriggerEvents.MEETING_STARTED,
teamId,
orgId,
oAuthClientId,
};

const subscribersMeetingEnded = await monitorCallbackAsync(getWebhooks, subscriberOptionsMeetingEnded);
const subscribersMeetingStarted = await monitorCallbackAsync(
getWebhooks,
subscriberOptionsMeetingStarted
);

const scheduleTriggerPromises = [];

for (const subscriber of subscribersMeetingEnded) {
scheduleTriggerPromises.push(
scheduleTrigger({
booking,
subscriberUrl: subscriber.subscriberUrl,
subscriber,
triggerEvent: WebhookTriggerEvents.MEETING_ENDED,
})
);
}

for (const subscriber of subscribersMeetingStarted) {
scheduleTriggerPromises.push(
scheduleTrigger({
booking,
subscriberUrl: subscriber.subscriberUrl,
subscriber,
triggerEvent: WebhookTriggerEvents.MEETING_STARTED,
})
);
}

await Promise.all(scheduleTriggerPromises).catch((error) => {
log.error("Error while scheduling webhook triggers", JSON.stringify({ error }));
});
}

// Send Webhook call if hooked to BOOKING_CREATED & BOOKING_RESCHEDULED
await monitorCallbackAsync(handleWebhookTrigger, {
subscriberOptions: {
userId,
eventTypeId,
triggerEvent,
teamId,
orgId,
oAuthClientId,
},
eventTrigger: triggerEvent,
webhookData,
});
} else {
//

webhookData.status = "PENDING";

await monitorCallbackAsync(handleWebhookTrigger, {
subscriberOptions: {
userId,
eventTypeId,
triggerEvent,
teamId,
orgId,
oAuthClientId,
},
eventTrigger: triggerEvent,
webhookData,
});
}
};
14 changes: 14 additions & 0 deletions packages/features/tasker/tasks/triggerWebhooks/schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import z from "zod";

import { WebhookTriggerEvents } from "@calcom/prisma/enums";

export const ZTriggerWebhooksPayloadSchema = z.object({
bookingId: z.number(),
userId: z.number().nullable(),
eventTypeId: z.number(),
triggerEvent: z.nativeEnum(WebhookTriggerEvents),
teamId: z.number().nullable(),
orgId: z.number().nullable(),
oAuthClientId: z.string().nullable(),
isConfirmedByDefault: z.boolean(),
});
Loading