Skip to content

Commit

Permalink
feat: org-wide webhooks (calcom#15144)
Browse files Browse the repository at this point in the history
* feat: include org in teams list

* feat: org-wide webhooks

* Move getOrgIdFromMemberOrTeamId to packages/lib
  • Loading branch information
kart1ka authored Jun 4, 2024
1 parent d72f7f8 commit 4017c0e
Show file tree
Hide file tree
Showing 20 changed files with 264 additions and 63 deletions.
8 changes: 7 additions & 1 deletion apps/web/pages/api/recorded-daily-video.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { getDownloadLinkOfCalVideoByRecordingId } from "@calcom/core/videoClient
import { sendDailyVideoRecordingEmails } 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";
import { getTeamIdFromEventType } from "@calcom/lib/getTeamIdFromEventType";
import logger from "@calcom/lib/logger";
import { safeStringify } from "@calcom/lib/safeStringify";
Expand Down Expand Up @@ -60,11 +61,16 @@ const triggerWebhook = async ({
// Send Webhook call if hooked to BOOKING.RECORDING_READY
const triggerForUser = !booking.teamId || (booking.teamId && booking.eventTypeParentId);

const organizerUserId = triggerForUser ? booking.userId : null;

const orgId = await getOrgIdFromMemberOrTeamId({ memberId: organizerUserId, teamId: booking.teamId });

const subscriberOptions = {
userId: triggerForUser ? booking.userId : null,
userId: organizerUserId,
eventTypeId: booking.eventTypeId,
triggerEvent: eventTrigger,
teamId: booking.teamId,
orgId,
};
const webhooks = await getWebhooks(subscriberOptions);

Expand Down
4 changes: 2 additions & 2 deletions apps/web/pages/settings/developer/webhooks/index.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import WeebhooksView from "@calcom/features/webhooks/pages/webhooks-view";
import WebhooksView from "@calcom/features/webhooks/pages/webhooks-view";

import type { CalPageWrapper } from "@components/PageWrapper";
import PageWrapper from "@components/PageWrapper";

const Page = WeebhooksView as CalPageWrapper;
const Page = WebhooksView as CalPageWrapper;
Page.PageWrapper = PageWrapper;

export default Page;
4 changes: 2 additions & 2 deletions apps/web/pages/settings/developer/webhooks/new.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import WeebhookNewView from "@calcom/features/webhooks/pages/webhook-new-view";
import WebhookNewView from "@calcom/features/webhooks/pages/webhook-new-view";

import type { CalPageWrapper } from "@components/PageWrapper";
import PageWrapper from "@components/PageWrapper";

const Page = WeebhookNewView as CalPageWrapper;
const Page = WebhookNewView as CalPageWrapper;
Page.PageWrapper = PageWrapper;

export default Page;
9 changes: 8 additions & 1 deletion packages/app-store/routing-forms/trpc/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { App_RoutingForms_Form, User } from "@prisma/client";

import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks";
import { sendGenericWebhookPayload } from "@calcom/features/webhooks/lib/sendPayload";
import getOrgIdFromMemberOrTeamId from "@calcom/lib/getOrgIdFromMemberOrTeamId";
import logger from "@calcom/lib/logger";
import { WebhookTriggerEvents } from "@calcom/prisma/client";
import type { Ensure } from "@calcom/types/utils";
Expand Down Expand Up @@ -33,9 +34,15 @@ export async function onFormSubmission(
};
}

const { userId, teamId } = getWebhookTargetEntity(form);

const orgId = await getOrgIdFromMemberOrTeamId({ memberId: userId, teamId });

const subscriberOptions = {
userId,
teamId,
orgId,
triggerEvent: WebhookTriggerEvents.FORM_SUBMITTED,
...getWebhookTargetEntity(form),
};

const webhooks = await getWebhooks(subscriberOptions);
Expand Down
7 changes: 7 additions & 0 deletions packages/features/bookings/lib/handleBookingRequested.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { sendAttendeeRequestEmail, sendOrganizerRequestEmail } from "@calcom/ema
import { getWebhookPayloadForBooking } from "@calcom/features/bookings/lib/getWebhookPayloadForBooking";
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";
Expand Down Expand Up @@ -39,12 +40,18 @@ export async function handleBookingRequested(args: {
await sendOrganizerRequestEmail({ ...evt });
await sendAttendeeRequestEmail({ ...evt }, evt.attendees[0]);

const orgId = await getOrgIdFromMemberOrTeamId({
memberId: booking.userId,
teamId: booking.eventType?.teamId,
});

try {
const subscribersBookingRequested = await getWebhooks({
userId: booking.userId,
eventTypeId: booking.eventTypeId,
triggerEvent: WebhookTriggerEvents.BOOKING_REQUESTED,
teamId: booking.eventType?.teamId,
orgId,
});

const webhookPayload = getWebhookPayloadForBooking({
Expand Down
7 changes: 6 additions & 1 deletion packages/features/bookings/lib/handleCancelBooking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { deleteWebhookScheduledTriggers } from "@calcom/features/webhooks/lib/sc
import sendPayload from "@calcom/features/webhooks/lib/sendOrSchedulePayload";
import type { EventTypeInfo } from "@calcom/features/webhooks/lib/sendPayload";
import { isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib";
import getOrgIdFromMemberOrTeamId from "@calcom/lib/getOrgIdFromMemberOrTeamId";
import { getTeamIdFromEventType } from "@calcom/lib/getTeamIdFromEventType";
import { HttpError } from "@calcom/lib/http-error";
import logger from "@calcom/lib/logger";
Expand Down Expand Up @@ -183,12 +184,16 @@ async function handler(req: CustomRequest) {
},
});
const triggerForUser = !teamId || (teamId && bookingToDelete.eventType?.parentId);
const organizerUserId = triggerForUser ? bookingToDelete.userId : null;

const orgId = await getOrgIdFromMemberOrTeamId({ memberId: organizerUserId, teamId });

const subscriberOptions = {
userId: triggerForUser ? bookingToDelete.userId : null,
userId: organizerUserId,
eventTypeId: bookingToDelete.eventTypeId as number,
triggerEvent: eventTrigger,
teamId,
orgId,
};
const eventTypeInfo: EventTypeInfo = {
eventTitle: bookingToDelete?.eventType?.title || null,
Expand Down
17 changes: 13 additions & 4 deletions packages/features/bookings/lib/handleConfirmation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { scheduleTrigger } from "@calcom/features/webhooks/lib/scheduleTrigger";
import sendPayload from "@calcom/features/webhooks/lib/sendOrSchedulePayload";
import type { EventTypeInfo } from "@calcom/features/webhooks/lib/sendPayload";
import { getVideoCallUrlFromCalEvent } from "@calcom/lib/CalEventParser";
import getOrgIdFromMemberOrTeamId from "@calcom/lib/getOrgIdFromMemberOrTeamId";
import { getTeamIdFromEventType } from "@calcom/lib/getTeamIdFromEventType";
import logger from "@calcom/lib/logger";
import { safeStringify } from "@calcom/lib/safeStringify";
Expand Down Expand Up @@ -302,23 +303,30 @@ export async function handleConfirmation(args: {

const triggerForUser = !teamId || (teamId && booking.eventType?.parentId);

const userId = triggerForUser ? booking.userId : null;

const orgId = await getOrgIdFromMemberOrTeamId({ memberId: userId, teamId });

const subscribersBookingCreated = await getWebhooks({
userId: triggerForUser ? booking.userId : null,
userId,
eventTypeId: booking.eventTypeId,
triggerEvent: WebhookTriggerEvents.BOOKING_CREATED,
teamId,
orgId,
});
const subscribersMeetingStarted = await getWebhooks({
userId: triggerForUser ? booking.userId : null,
userId,
eventTypeId: booking.eventTypeId,
triggerEvent: WebhookTriggerEvents.MEETING_STARTED,
teamId: booking.eventType?.teamId,
orgId,
});
const subscribersMeetingEnded = await getWebhooks({
userId: triggerForUser ? booking.userId : null,
userId,
eventTypeId: booking.eventTypeId,
triggerEvent: WebhookTriggerEvents.MEETING_ENDED,
teamId: booking.eventType?.teamId,
orgId,
});

const scheduleTriggerPromises: Promise<unknown>[] = [];
Expand Down Expand Up @@ -381,10 +389,11 @@ export async function handleConfirmation(args: {
if (paid) {
let paymentExternalId: string | undefined;
const subscriberMeetingPaid = await getWebhooks({
userId: triggerForUser ? booking.userId : null,
userId,
eventTypeId: booking.eventTypeId,
triggerEvent: WebhookTriggerEvents.BOOKING_PAID,
teamId: booking.eventType?.teamId,
orgId,
});
const bookingWithPayment = await prisma.booking.findFirst({
where: {
Expand Down
11 changes: 10 additions & 1 deletion packages/features/bookings/lib/handleNewBooking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ import { ErrorCode } from "@calcom/lib/errorCodes";
import { getErrorFromUnknown } from "@calcom/lib/errors";
import { extractBaseEmail } from "@calcom/lib/extract-base-email";
import { getBookerBaseUrl } from "@calcom/lib/getBookerUrl/server";
import getOrgIdFromMemberOrTeamId from "@calcom/lib/getOrgIdFromMemberOrTeamId";
import getPaymentAppData from "@calcom/lib/getPaymentAppData";
import { getTeamIdFromEventType } from "@calcom/lib/getTeamIdFromEventType";
import { HttpError } from "@calcom/lib/http-error";
Expand Down Expand Up @@ -1663,11 +1664,16 @@ async function handler(

const triggerForUser = !teamId || (teamId && eventType.parentId);

const organizerUserId = triggerForUser ? organizerUser.id : null;

const orgId = await getOrgIdFromMemberOrTeamId({ memberId: organizerUserId, teamId });

const subscriberOptions: GetSubscriberOptions = {
userId: triggerForUser ? organizerUser.id : null,
userId: organizerUserId,
eventTypeId,
triggerEvent: WebhookTriggerEvents.BOOKING_CREATED,
teamId,
orgId,
};

const eventTrigger: WebhookTriggerEvents = rescheduleUid
Expand All @@ -1681,13 +1687,15 @@ async function handler(
eventTypeId,
triggerEvent: WebhookTriggerEvents.MEETING_ENDED,
teamId,
orgId,
};

const subscriberOptionsMeetingStarted = {
userId: triggerForUser ? organizerUser.id : null,
eventTypeId,
triggerEvent: WebhookTriggerEvents.MEETING_STARTED,
teamId,
orgId,
};

// For seats, if the booking already exists then we want to add the new attendee to the existing booking
Expand Down Expand Up @@ -2335,6 +2343,7 @@ async function handler(
eventTypeId,
triggerEvent: WebhookTriggerEvents.BOOKING_PAYMENT_INITIATED,
teamId,
orgId,
};
await handleWebhookTrigger({
subscriberOptions: subscriberOptionsPaymentInitiated,
Expand Down
20 changes: 18 additions & 2 deletions packages/features/instant-meeting/handleInstantMeeting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { getFullName } from "@calcom/features/form-builder/utils";
import { sendGenericWebhookPayload } from "@calcom/features/webhooks/lib/sendPayload";
import { isPrismaObjOrUndefined } from "@calcom/lib";
import { WEBAPP_URL } from "@calcom/lib/constants";
import getOrgIdFromMemberOrTeamId from "@calcom/lib/getOrgIdFromMemberOrTeamId";
import logger from "@calcom/lib/logger";
import { getTranslation } from "@calcom/lib/server";
import prisma from "@calcom/prisma";
Expand All @@ -25,18 +26,32 @@ import { BookingStatus, WebhookTriggerEvents } from "@calcom/prisma/enums";
const handleInstantMeetingWebhookTrigger = async (args: {
eventTypeId: number;
webhookData: Record<string, unknown>;
teamId: number;
}) => {
const orgId = (await getOrgIdFromMemberOrTeamId({ teamId: args.teamId })) ?? 0;

try {
const eventTrigger = WebhookTriggerEvents.INSTANT_MEETING;

const subscribers = await prisma.webhook.findMany({
where: {
OR: [
{
teamId: {
in: [orgId, args.teamId],
},
},
{
eventTypeId: args.eventTypeId,
},
],
AND: {
eventTypeId: args.eventTypeId,
eventTriggers: {
has: eventTrigger,
},
active: true,
active: {
equals: true,
},
},
},
select: {
Expand Down Expand Up @@ -213,6 +228,7 @@ async function handler(req: NextApiRequest) {
await handleInstantMeetingWebhookTrigger({
eventTypeId: eventType.id,
webhookData,
teamId: eventType.team?.id,
});

return {
Expand Down
7 changes: 6 additions & 1 deletion packages/features/webhooks/lib/getWebhooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,15 @@ export type GetSubscriberOptions = {
eventTypeId?: number | null;
triggerEvent: WebhookTriggerEvents;
teamId?: number | null;
orgId?: number | null;
};

const getWebhooks = async (options: GetSubscriberOptions, prisma: PrismaClient = defaultPrisma) => {
const userId = options.userId ?? 0;
const eventTypeId = options.eventTypeId ?? 0;
const teamId = options.teamId ?? 0;
const orgId = options.orgId ?? 0;

// if we have userId and teamId it is a managed event type and should trigger for team and user
const allWebhooks = await prisma.webhook.findMany({
where: {
Expand All @@ -27,7 +30,9 @@ const getWebhooks = async (options: GetSubscriberOptions, prisma: PrismaClient =
eventTypeId,
},
{
teamId,
teamId: {
in: [teamId, orgId],
},
},
],
AND: {
Expand Down
64 changes: 59 additions & 5 deletions packages/features/webhooks/lib/scheduleTrigger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -355,18 +355,72 @@ export async function updateTriggerForExistingBookings(

if (Array.isArray(where.AND)) {
if (webhook.teamId) {
const teamEvents = await prisma.eventType.findMany({
const org = await prisma.team.findFirst({
where: {
teamId: webhook.teamId,
id: webhook.teamId,
isOrganization: true,
},
select: {
bookings: {
where,
id: true,
children: {
select: {
id: true,
},
},
members: {
select: {
userId: true,
},
},
},
});
// checking if teamId is an org id
if (org) {
const teamEvents = await prisma.eventType.findMany({
where: {
teamId: {
in: org.children.map((team) => team.id),
},
},
select: {
bookings: {
where,
},
},
});
const teamEventBookings = teamEvents.flatMap((event) => event.bookings);
const teamBookingsId = teamEventBookings.map((booking) => booking.id);
const orgMemberIds = org.members.map((member) => member.userId);
where.AND.push({
userId: {
in: orgMemberIds,
},
});
// don't want to get the team bookings again
where.AND.push({
id: {
notIn: teamBookingsId,
},
});
const userBookings = await prisma.booking.findMany({
where,
});
// add teams bookings and users bookings to get total org bookings
bookings = teamEventBookings.concat(userBookings);
} else {
const teamEvents = await prisma.eventType.findMany({
where: {
teamId: webhook.teamId,
},
select: {
bookings: {
where,
},
},
});

bookings = teamEvents.flatMap((event) => event.bookings);
bookings = teamEvents.flatMap((event) => event.bookings);
}
} else {
if (webhook.eventTypeId) {
where.AND.push({ eventTypeId: webhook.eventTypeId });
Expand Down
Loading

0 comments on commit 4017c0e

Please sign in to comment.