From f5c16f0d6bbe7b472c606e6a710a06e77fb980d2 Mon Sep 17 00:00:00 2001 From: alannnc Date: Fri, 6 Sep 2024 12:48:05 -0700 Subject: [PATCH] feat: OOO webhook and zapier (#15434) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * zapier and webhook * instructions for zapier * Fix types * fix types * revert adding zap template * Update packages/app-store/zapier/README.md Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com> * Update packages/app-store/zapier/README.md Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com> * Update packages/app-store/zapier/README.md Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com> * Update packages/features/webhooks/lib/scheduleTrigger.ts Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com> * wip * Fixes from comments, no more sendPayloadNoBooking * fix subscriberOptions teamIds param type * types * Update packages/features/webhooks/lib/sendPayload.ts * Re add zapierPayload to don't break old zaps * instead of metadata use oooEntry inside webhookPayload * undo comment message * Types * reset changes on yarn.lock * review changes * fix types * fix types * tentative fix for types in webhook * type improvements * fix description + clean up * revert yarn.lock changes * allow custom template for ooo entry * type fixes * type fix * fix donwloadLinks in payload * simplify some types * allow array or number for teamId * same payload for test trigger * code clean up * fix no show e2e test * translate text for webhook payload and fix conditional * add test for ooo_created webhooks * reset files * merge fix * fix test data test id * remove unused variable * only trigger for accepted memberships * remove unused variable --------- Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com> Co-authored-by: CarinaWolli Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com> Co-authored-by: Keith Williams Co-authored-by: Omar López --- apps/web/lib/daily-webhook/triggerWebhooks.ts | 21 +-- .../my-account/out-of-office/index.tsx | 2 +- apps/web/playwright/bookings-list.e2e.ts | 2 +- apps/web/playwright/webhook.e2e.ts | 136 ++++++++++++++++++ apps/web/public/static/locales/en/common.json | 1 + packages/app-store/zapier/README.md | 27 +++- packages/app-store/zapier/api/index.ts | 1 + .../api/subscriptions/listOOOEntries.ts | 76 ++++++++++ .../lib/getWebhookPayloadForBooking.ts | 7 +- .../bookings/lib/handleConfirmation.ts | 53 ++++--- .../features/bookings/lib/handleNewBooking.ts | 4 +- .../handleSeats/cancel/cancelAttendeeSeat.ts | 22 ++- .../bookings/lib/handleSeats/handleSeats.ts | 3 +- .../bookings/lib/handleWebhookTrigger.ts | 14 +- packages/features/handleMarkNoShow.ts | 1 - .../webhooks/components/WebhookForm.tsx | 4 +- packages/features/webhooks/lib/constants.ts | 1 + packages/features/webhooks/lib/getWebhooks.ts | 7 +- .../features/webhooks/lib/scheduleTrigger.ts | 54 +++++++ packages/features/webhooks/lib/sendPayload.ts | 119 ++++++++++----- .../migration.sql | 2 + packages/prisma/schema.prisma | 1 + .../loggedInViewer/outOfOffice.handler.ts | 119 ++++++++++++++- .../viewer/bookings/confirm.handler.ts | 4 +- .../bookings/requestReschedule.handler.ts | 3 +- .../routers/viewer/webhook/create.handler.ts | 45 +++--- .../viewer/webhook/testTrigger.handler.ts | 3 +- 27 files changed, 606 insertions(+), 126 deletions(-) create mode 100644 packages/app-store/zapier/api/subscriptions/listOOOEntries.ts create mode 100644 packages/prisma/migrations/20240611054408_add_ooo_created_as_webhook_enum/migration.sql diff --git a/apps/web/lib/daily-webhook/triggerWebhooks.ts b/apps/web/lib/daily-webhook/triggerWebhooks.ts index 562a8b70dc..3aa7ab7502 100644 --- a/apps/web/lib/daily-webhook/triggerWebhooks.ts +++ b/apps/web/lib/daily-webhook/triggerWebhooks.ts @@ -1,6 +1,7 @@ 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 type { EventPayloadType } from "@calcom/features/webhooks/lib/sendPayload"; import getOrgIdFromMemberOrTeamId from "@calcom/lib/getOrgIdFromMemberOrTeamId"; import logger from "@calcom/lib/logger"; import { safeStringify } from "@calcom/lib/safeStringify"; @@ -53,11 +54,13 @@ export const triggerRecordingReadyWebhook = async ({ }) ); + const payload: EventPayloadType = { + ...evt, + downloadLink, + }; + const promises = webhooks.map((webhook) => - sendPayload(webhook.secret, eventTrigger, new Date().toISOString(), webhook, { - ...evt, - downloadLink, - }).catch((e) => { + sendPayload(webhook.secret, eventTrigger, new Date().toISOString(), webhook, payload).catch((e) => { log.error( `Error executing webhook for event: ${eventTrigger}, URL: ${webhook.subscriberUrl}, bookingId: ${evt.bookingId}, bookingUid: ${evt.uid}`, safeStringify(e) @@ -91,16 +94,18 @@ export const triggerTranscriptionGeneratedWebhook = async ({ }) ); + const payload: EventPayloadType = { + ...evt, + downloadLinks, + }; + const promises = webhooks.map((webhook) => sendPayload( webhook.secret, WebhookTriggerEvents.RECORDING_TRANSCRIPTION_GENERATED, new Date().toISOString(), webhook, - { - ...evt, - downloadLinks, - } + payload ).catch((e) => { log.error( `Error executing webhook for event: ${WebhookTriggerEvents.RECORDING_TRANSCRIPTION_GENERATED}, URL: ${webhook.subscriberUrl}, bookingId: ${evt.bookingId}, bookingUid: ${evt.uid}`, diff --git a/apps/web/pages/settings/my-account/out-of-office/index.tsx b/apps/web/pages/settings/my-account/out-of-office/index.tsx index b69c3daff8..f2b845c8f4 100644 --- a/apps/web/pages/settings/my-account/out-of-office/index.tsx +++ b/apps/web/pages/settings/my-account/out-of-office/index.tsx @@ -23,7 +23,7 @@ const OutOfOfficePage = () => { const { t } = useLocale(); const params = useSearchParams(); - const openModalOnStart = !!params?.get("om"); + const openModalOnStart = !!params?.get("open"); useEffect(() => { if (openModalOnStart) { setOpenModal(true); diff --git a/apps/web/playwright/bookings-list.e2e.ts b/apps/web/playwright/bookings-list.e2e.ts index 77dbbf5289..75eae49531 100644 --- a/apps/web/playwright/bookings-list.e2e.ts +++ b/apps/web/playwright/bookings-list.e2e.ts @@ -107,7 +107,7 @@ test.describe("Bookings", () => { createdAt: "[redacted/dynamic]", payload: { message: "first@cal.com marked as no-show", - attendees: [{ email: "first@cal.com", noShow: true, utcOffset: null }], + attendees: [{ email: "first@cal.com", noShow: true }], bookingUid: bookingWhereFirstUserIsOrganizer?.uid, bookingId: bookingWhereFirstUserIsOrganizer?.id, }, diff --git a/apps/web/playwright/webhook.e2e.ts b/apps/web/playwright/webhook.e2e.ts index ed758e8338..63829ce61a 100644 --- a/apps/web/playwright/webhook.e2e.ts +++ b/apps/web/playwright/webhook.e2e.ts @@ -741,3 +741,139 @@ test.describe("FORM_SUBMITTED", async () => { webhookReceiver.close(); }); }); + +test.describe("OOO_CREATED", async () => { + test("on creating an OOO, triggers OOO webhook", async ({ page, users, webhooks }) => { + const user = await users.create(); + await user.apiLogin(); + const webhookReceiver = await webhooks.createReceiver(); + + await page.goto("/settings/my-account/out-of-office"); + + await page.getByTestId("add_entry_ooo").click(); + await page.getByTestId("reason_select").click(); + + await page.getByTestId("select-option-4").click(); + + await page.getByTestId("notes_input").click(); + await page.getByTestId("notes_input").fill("Demo notes"); + await page.getByTestId("create-or-edit-entry-ooo-redirect").click(); + + await expect(page.locator(`data-testid=table-redirect-n-a`)).toBeVisible(); + + await webhookReceiver.waitForRequestCount(1); + + const [request] = webhookReceiver.requestList; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const body = request.body as any; + body.createdAt = dynamic; + body.payload.oooEntry.createdAt = dynamic; + body.payload.oooEntry.updatedAt = dynamic; + body.payload.oooEntry.end = dynamic; + body.payload.oooEntry.id = dynamic; + body.payload.oooEntry.start = dynamic; + body.payload.oooEntry.updatedAt = dynamic; + body.payload.oooEntry.user.id = dynamic; + body.payload.oooEntry.user.email = dynamic; + body.payload.oooEntry.user.username = dynamic; + body.payload.oooEntry.uuid = dynamic; + expect(body).toEqual({ + createdAt: "[redacted/dynamic]", + payload: { + oooEntry: { + createdAt: "[redacted/dynamic]", + end: "[redacted/dynamic]", + id: "[redacted/dynamic]", + notes: "Demo notes", + reason: { + emoji: "🤒", + reason: "ooo_reasons_sick_leave", + }, + reasonId: 4, + start: "[redacted/dynamic]", + toUser: null, + updatedAt: "[redacted/dynamic]", + user: { + id: "[redacted/dynamic]", + email: "[redacted/dynamic]", + name: null, + timeZone: "Europe/London", + username: "[redacted/dynamic]", + }, + uuid: "[redacted/dynamic]", + }, + }, + triggerEvent: "OOO_CREATED", + }); + + webhookReceiver.close(); + }); + + test("on creating an OOO inside a team, triggers OOO webhook", async ({ page, users, webhooks }) => { + const user = await users.create(null, { + hasTeam: true, + }); + await user.apiLogin(); + const { webhookReceiver, teamId } = await webhooks.createTeamReceiver(); + + await page.goto("/settings/my-account/out-of-office"); + + await page.getByTestId("add_entry_ooo").click(); + await page.getByTestId("reason_select").click(); + + await page.getByTestId("select-option-4").click(); + + await page.getByTestId("notes_input").click(); + await page.getByTestId("notes_input").fill("Demo notes"); + await page.getByTestId("create-or-edit-entry-ooo-redirect").click(); + + await expect(page.locator(`data-testid=table-redirect-n-a`)).toBeVisible(); + + await webhookReceiver.waitForRequestCount(1); + + const [request] = webhookReceiver.requestList; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const body = request.body as any; + body.createdAt = dynamic; + body.payload.oooEntry.createdAt = dynamic; + body.payload.oooEntry.updatedAt = dynamic; + body.payload.oooEntry.end = dynamic; + body.payload.oooEntry.id = dynamic; + body.payload.oooEntry.start = dynamic; + body.payload.oooEntry.updatedAt = dynamic; + body.payload.oooEntry.user.id = dynamic; + body.payload.oooEntry.user.email = dynamic; + body.payload.oooEntry.user.username = dynamic; + body.payload.oooEntry.uuid = dynamic; + expect(body).toEqual({ + triggerEvent: "OOO_CREATED", + createdAt: "[redacted/dynamic]", + payload: { + oooEntry: { + createdAt: "[redacted/dynamic]", + end: "[redacted/dynamic]", + id: "[redacted/dynamic]", + notes: "Demo notes", + reason: { + emoji: "🤒", + reason: "ooo_reasons_sick_leave", + }, + reasonId: 4, + start: "[redacted/dynamic]", + toUser: null, + updatedAt: "[redacted/dynamic]", + user: { + id: "[redacted/dynamic]", + email: "[redacted/dynamic]", + name: null, + timeZone: "Europe/London", + username: "[redacted/dynamic]", + }, + uuid: "[redacted/dynamic]", + }, + }, + }); + + webhookReceiver.close(); + }); +}); diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index b1be16f3bb..57177a7521 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -487,6 +487,7 @@ "meeting_ended": "Meeting Ended", "form_submitted": "Form Submitted", "booking_paid": "Booking Paid", + "ooo_created": "Out of Office Created", "booking_no_show_updated": "Booking No-Show Updated", "event_triggers": "Event Triggers", "subscriber_url": "Subscriber URL", diff --git a/packages/app-store/zapier/README.md b/packages/app-store/zapier/README.md index 8518d3817a..d21fa12d09 100644 --- a/packages/app-store/zapier/README.md +++ b/packages/app-store/zapier/README.md @@ -33,7 +33,7 @@ If you run it on localhost, check out the [additional information](https://githu ## Triggers -Booking created, Booking rescheduled, Booking cancelled, Meeting ended +Booking created, Booking rescheduled, Booking cancelled, Meeting ended, Out Of Office Created ### Booking created @@ -56,9 +56,30 @@ Booking created, Booking rescheduled, Booking cancelled, Meeting ended Create the other triggers (booking rescheduled, booking cancelled and meeting ended) exactly like this one, just use the appropriate naming (e.g. booking_rescheduled instead of booking_created) +### Out Of Office Created + +1. Settings + - Key: ooo_created + - Name: Out Of Office Created + - Noun: OOO Entry + - Description: Triggers when a new Out Of Office entry is created. +2. API Configuration (apiKey is set automatically, leave it like it is): + - Trigger Type: REST Hook + - Subscribe: POST ``/api/integrations/zapier/addSubscription + - Request Body + - subscriberUrl: {{bundle.targetUrl}} + - triggerEvent: OOO_CREATED + - Unsubscribe: DELETE ``/api/integrations/zapier/deleteSubscription + - URL Params (in addition to apiKey) + - id: {{bundle.subscribeData.id}} + - PerformList: GET ``/api/integrations/zapier/listOOOEntries +3. Test your API request +4. Note: When creating the ZAP you need to remember that data is stored in the { payload: { oooEntry: { ... } } } + + ### Set ZAPIER_INVITE_LINK -The invite link can be found under under Manage → Sharing. +The invite link can be found under Manage → Sharing. ## Localhost @@ -67,5 +88,5 @@ Localhost urls can not be used as the base URL for api endpoints Possible solution: using [https://ngrok.com/](https://ngrok.com/) 1. Create Account -2. [Download](https://ngrok.com/download) gnork and start a tunnel to your running localhost +2. [Download](https://ngrok.com/download) ngrok and start a tunnel to your running localhost - Use forwarding url as your baseUrl for the URL endpoints diff --git a/packages/app-store/zapier/api/index.ts b/packages/app-store/zapier/api/index.ts index d32a6d07b0..cb7825aad7 100644 --- a/packages/app-store/zapier/api/index.ts +++ b/packages/app-store/zapier/api/index.ts @@ -1,5 +1,6 @@ export { default as add } from "./add"; export { default as listBookings } from "./subscriptions/listBookings"; +export { default as listOOOEntries } from "./subscriptions/listOOOEntries"; export { default as deleteSubscription } from "./subscriptions/deleteSubscription"; export { default as addSubscription } from "./subscriptions/addSubscription"; export { default as me } from "./subscriptions/me"; diff --git a/packages/app-store/zapier/api/subscriptions/listOOOEntries.ts b/packages/app-store/zapier/api/subscriptions/listOOOEntries.ts new file mode 100644 index 0000000000..aaa21835c4 --- /dev/null +++ b/packages/app-store/zapier/api/subscriptions/listOOOEntries.ts @@ -0,0 +1,76 @@ +import type { NextApiRequest, NextApiResponse } from "next"; + +import { listOOOEntries } from "@calcom/features/webhooks/lib/scheduleTrigger"; +import { defaultHandler, defaultResponder } from "@calcom/lib/server"; +import { WebhookTriggerEvents } from "@calcom/prisma/enums"; + +import { validateAccountOrApiKey } from "../../lib/validateAccountOrApiKey"; + +export const selectOOOEntries = { + id: true, + start: true, + end: true, + createdAt: true, + updatedAt: true, + notes: true, + reason: { + select: { + reason: true, + emoji: true, + }, + }, + reasonId: true, + user: { + select: { + id: true, + name: true, + email: true, + timeZone: true, + }, + }, + toUser: { + select: { + id: true, + name: true, + email: true, + timeZone: true, + }, + }, + uuid: true, +}; + +async function handler(req: NextApiRequest, res: NextApiResponse) { + const { account: authorizedAccount, appApiKey: validKey } = await validateAccountOrApiKey(req, [ + "READ_PROFILE", + ]); + if (!authorizedAccount && !validKey) { + res.status(401).json({ message: "Unauthorized" }); + return; + } + const oooEntries = await listOOOEntries(validKey, authorizedAccount); + + if (!oooEntries) { + res.status(500).json({ message: "Unable to get out of office entries list." }); + return; + } + if (oooEntries.length === 0) { + res.status(200).json([]); + return; + } + // Wrap entries in metadata object + const response = oooEntries.map((oooEntry) => { + return { + createdAt: oooEntry.createdAt, + triggerEvent: WebhookTriggerEvents.OOO_CREATED, + payload: { + oooEntry, + }, + }; + }); + res.status(200).json(response); + return; +} + +export default defaultHandler({ + GET: Promise.resolve({ default: defaultResponder(handler) }), +}); diff --git a/packages/features/bookings/lib/getWebhookPayloadForBooking.ts b/packages/features/bookings/lib/getWebhookPayloadForBooking.ts index a003fbdc35..8e75040fd8 100644 --- a/packages/features/bookings/lib/getWebhookPayloadForBooking.ts +++ b/packages/features/bookings/lib/getWebhookPayloadForBooking.ts @@ -1,4 +1,4 @@ -import type { EventTypeInfo } from "@calcom/features/webhooks/lib/sendPayload"; +import type { EventPayloadType, EventTypeInfo } from "@calcom/features/webhooks/lib/sendPayload"; import type { CalendarEvent } from "@calcom/types/Calendar"; export const getWebhookPayloadForBooking = ({ @@ -29,9 +29,12 @@ export const getWebhookPayloadForBooking = ({ currency: booking.eventType?.currency, length: booking.eventType?.length, }; - return { + + const payload: EventPayloadType = { ...evt, ...eventTypeInfo, bookingId: booking.id, }; + + return payload; }; diff --git a/packages/features/bookings/lib/handleConfirmation.ts b/packages/features/bookings/lib/handleConfirmation.ts index 5414e503a5..373a0b6807 100644 --- a/packages/features/bookings/lib/handleConfirmation.ts +++ b/packages/features/bookings/lib/handleConfirmation.ts @@ -13,7 +13,7 @@ import type { Workflow } from "@calcom/features/ee/workflows/lib/types"; import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks"; 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 type { EventPayloadType, 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"; @@ -397,16 +397,24 @@ export async function handleConfirmation(args: { length: eventType?.length, }; + const payload: EventPayloadType = { + ...evt, + ...eventTypeInfo, + bookingId, + eventTypeId: eventType?.id, + status: "ACCEPTED", + smsReminderNumber: booking.smsReminderNumber || undefined, + metadata: meetingUrl ? { videoCallUrl: meetingUrl } : undefined, + }; + const promises = subscribersBookingCreated.map((sub) => - sendPayload(sub.secret, WebhookTriggerEvents.BOOKING_CREATED, new Date().toISOString(), sub, { - ...evt, - ...eventTypeInfo, - bookingId, - eventTypeId: eventType?.id, - status: "ACCEPTED", - smsReminderNumber: booking.smsReminderNumber || undefined, - metadata: meetingUrl ? { videoCallUrl: meetingUrl } : undefined, - }).catch((e) => { + sendPayload( + sub.secret, + WebhookTriggerEvents.BOOKING_CREATED, + new Date().toISOString(), + sub, + payload + ).catch((e) => { log.error( `Error executing webhook for event: ${WebhookTriggerEvents.BOOKING_CREATED}, URL: ${sub.subscriberUrl}, bookingId: ${evt.bookingId}, bookingUid: ${evt.uid}`, safeStringify(e) @@ -452,19 +460,20 @@ export async function handleConfirmation(args: { eventTitle: eventType?.title, externalId: paymentExternalId, }; + + payload.paymentId = bookingWithPayment?.payment?.[0].id; + payload.metadata = { + ...(paid ? paymentMetadata : {}), + }; + const bookingPaidSubscribers = subscriberMeetingPaid.map((sub) => - sendPayload(sub.secret, WebhookTriggerEvents.BOOKING_PAID, new Date().toISOString(), sub, { - ...evt, - ...eventTypeInfo, - bookingId, - eventTypeId: eventType?.id, - status: "ACCEPTED", - smsReminderNumber: booking.smsReminderNumber || undefined, - paymentId: bookingWithPayment?.payment?.[0].id, - metadata: { - ...(paid ? paymentMetadata : {}), - }, - }).catch((e) => { + sendPayload( + sub.secret, + WebhookTriggerEvents.BOOKING_PAID, + new Date().toISOString(), + sub, + payload + ).catch((e) => { log.error( `Error executing webhook for event: ${WebhookTriggerEvents.BOOKING_PAID}, URL: ${sub.subscriberUrl}, bookingId: ${evt.bookingId}, bookingUid: ${evt.uid}`, safeStringify(e) diff --git a/packages/features/bookings/lib/handleNewBooking.ts b/packages/features/bookings/lib/handleNewBooking.ts index 2f7f876086..dc99444a70 100644 --- a/packages/features/bookings/lib/handleNewBooking.ts +++ b/packages/features/bookings/lib/handleNewBooking.ts @@ -84,7 +84,7 @@ import type { } from "@calcom/types/Calendar"; import type { EventResult, PartialReference } from "@calcom/types/EventManager"; -import type { EventTypeInfo } from "../../webhooks/lib/sendPayload"; +import type { EventPayloadType, EventTypeInfo } from "../../webhooks/lib/sendPayload"; import { getAllCredentials } from "./getAllCredentialsForUsersOnEvent/getAllCredentials"; import { refreshCredentials } from "./getAllCredentialsForUsersOnEvent/refreshCredentials"; import getBookingDataSchema from "./getBookingDataSchema"; @@ -1547,7 +1547,7 @@ async function handler( } : undefined; - const webhookData = { + const webhookData: EventPayloadType = { ...evt, ...eventTypeInfo, bookingId: booking?.id, diff --git a/packages/features/bookings/lib/handleSeats/cancel/cancelAttendeeSeat.ts b/packages/features/bookings/lib/handleSeats/cancel/cancelAttendeeSeat.ts index 06624de91d..2f77deaded 100644 --- a/packages/features/bookings/lib/handleSeats/cancel/cancelAttendeeSeat.ts +++ b/packages/features/bookings/lib/handleSeats/cancel/cancelAttendeeSeat.ts @@ -2,7 +2,7 @@ import { getCalendar } from "@calcom/app-store/_utils/getCalendar"; import { updateMeeting } from "@calcom/core/videoClient"; import { sendCancelledSeatEmails } from "@calcom/emails"; import sendPayload from "@calcom/features/webhooks/lib/sendOrSchedulePayload"; -import type { EventTypeInfo } from "@calcom/features/webhooks/lib/sendPayload"; +import type { EventPayloadType, EventTypeInfo } from "@calcom/features/webhooks/lib/sendPayload"; import { HttpError } from "@calcom/lib/http-error"; import logger from "@calcom/lib/logger"; import { safeStringify } from "@calcom/lib/safeStringify"; @@ -129,13 +129,21 @@ async function cancelAttendeeSeat( ] : []; + const payload: EventPayloadType = { + ...evt, + ...eventTypeInfo, + status: "CANCELLED", + smsReminderNumber: bookingToDelete.smsReminderNumber || undefined, + }; + const promises = webhooks.map((webhook) => - sendPayload(webhook.secret, WebhookTriggerEvents.BOOKING_CANCELLED, new Date().toISOString(), webhook, { - ...evt, - ...eventTypeInfo, - status: "CANCELLED", - smsReminderNumber: bookingToDelete.smsReminderNumber || undefined, - }).catch((e) => { + sendPayload( + webhook.secret, + WebhookTriggerEvents.BOOKING_CANCELLED, + new Date().toISOString(), + webhook, + payload + ).catch((e) => { logger.error( `Error executing webhook for event: ${WebhookTriggerEvents.BOOKING_CANCELLED}, URL: ${webhook.subscriberUrl}, bookingId: ${evt.bookingId}, bookingUid: ${evt.uid}`, safeStringify(e) diff --git a/packages/features/bookings/lib/handleSeats/handleSeats.ts b/packages/features/bookings/lib/handleSeats/handleSeats.ts index 4ceb30650d..1a342abee3 100644 --- a/packages/features/bookings/lib/handleSeats/handleSeats.ts +++ b/packages/features/bookings/lib/handleSeats/handleSeats.ts @@ -2,6 +2,7 @@ import dayjs from "@calcom/dayjs"; import { handleWebhookTrigger } from "@calcom/features/bookings/lib/handleWebhookTrigger"; import { scheduleWorkflowReminders } from "@calcom/features/ee/workflows/lib/reminders/reminderScheduler"; +import type { EventPayloadType } from "@calcom/features/webhooks/lib/sendPayload"; import { ErrorCode } from "@calcom/lib/errorCodes"; import { HttpError } from "@calcom/lib/http-error"; import prisma from "@calcom/prisma"; @@ -125,7 +126,7 @@ const handleSeats = async (newSeatedBookingObject: NewSeatedBookingObject) => { loggerWithEventDetails.error("Error while scheduling workflow reminders", JSON.stringify({ error })); } - const webhookData = { + const webhookData: EventPayloadType = { ...evt, ...eventTypeInfo, uid: resultBooking?.uid || uid, diff --git a/packages/features/bookings/lib/handleWebhookTrigger.ts b/packages/features/bookings/lib/handleWebhookTrigger.ts index 64696cefca..abbcc85582 100644 --- a/packages/features/bookings/lib/handleWebhookTrigger.ts +++ b/packages/features/bookings/lib/handleWebhookTrigger.ts @@ -1,14 +1,14 @@ import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks"; import type { GetSubscriberOptions } from "@calcom/features/webhooks/lib/getWebhooks"; import sendPayload from "@calcom/features/webhooks/lib/sendOrSchedulePayload"; -import type { WebhookDataType } from "@calcom/features/webhooks/lib/sendPayload"; +import { isEventPayload, type WebhookPayloadType } from "@calcom/features/webhooks/lib/sendPayload"; import logger from "@calcom/lib/logger"; import { safeStringify } from "@calcom/lib/safeStringify"; export async function handleWebhookTrigger(args: { subscriberOptions: GetSubscriberOptions; eventTrigger: string; - webhookData: Omit; + webhookData: WebhookPayloadType; }) { try { const subscribers = await getWebhooks(args.subscriberOptions); @@ -16,10 +16,12 @@ export async function handleWebhookTrigger(args: { const promises = subscribers.map((sub) => sendPayload(sub.secret, args.eventTrigger, new Date().toISOString(), sub, args.webhookData).catch( (e) => { - logger.error( - `Error executing webhook for event: ${args.eventTrigger}, URL: ${sub.subscriberUrl}, bookingId: ${args.webhookData.bookingId}, bookingUid: ${args.webhookData.uid}`, - safeStringify(e) - ); + if (isEventPayload(args.webhookData)) { + logger.error( + `Error executing webhook for event: ${args.eventTrigger}, URL: ${sub.subscriberUrl}, booking id: ${args.webhookData.bookingId}, booking uid: ${args.webhookData.uid}`, + safeStringify(e) + ); + } } ) ); diff --git a/packages/features/handleMarkNoShow.ts b/packages/features/handleMarkNoShow.ts index b1e0dc76a8..becf9724cd 100644 --- a/packages/features/handleMarkNoShow.ts +++ b/packages/features/handleMarkNoShow.ts @@ -95,7 +95,6 @@ const handleMarkNoShow = async ({ await webhooks.sendPayload({ ...payload, /** We send webhook message pre-translated, on client we already handle this */ - // @ts-expect-error payload is too booking specific, we need to refactor this bookingUid, bookingId, }); diff --git a/packages/features/webhooks/components/WebhookForm.tsx b/packages/features/webhooks/components/WebhookForm.tsx index 6c730c057a..49573b0b85 100644 --- a/packages/features/webhooks/components/WebhookForm.tsx +++ b/packages/features/webhooks/components/WebhookForm.tsx @@ -43,6 +43,7 @@ const WEBHOOK_TRIGGER_EVENTS_GROUPED_BY_APP_V2: Record(false); const hasSecretKey = !!props?.webhook?.secret; - // const currentSecret = props?.webhook?.secret; useEffect(() => { if (changeSecret) { @@ -290,7 +290,7 @@ const WebhookForm = (props: { diff --git a/packages/features/webhooks/lib/constants.ts b/packages/features/webhooks/lib/constants.ts index f3e21203d4..5e2336e702 100644 --- a/packages/features/webhooks/lib/constants.ts +++ b/packages/features/webhooks/lib/constants.ts @@ -17,6 +17,7 @@ export const WEBHOOK_TRIGGER_EVENTS_GROUPED_BY_APP = { WebhookTriggerEvents.INSTANT_MEETING, WebhookTriggerEvents.RECORDING_TRANSCRIPTION_GENERATED, WebhookTriggerEvents.BOOKING_NO_SHOW_UPDATED, + WebhookTriggerEvents.OOO_CREATED, ] as const, "routing-forms": [WebhookTriggerEvents.FORM_SUBMITTED] as const, }; diff --git a/packages/features/webhooks/lib/getWebhooks.ts b/packages/features/webhooks/lib/getWebhooks.ts index 87e7860f2a..82b9f11629 100644 --- a/packages/features/webhooks/lib/getWebhooks.ts +++ b/packages/features/webhooks/lib/getWebhooks.ts @@ -6,15 +6,16 @@ export type GetSubscriberOptions = { userId?: number | null; eventTypeId?: number | null; triggerEvent: WebhookTriggerEvents; - teamId?: number | null; + teamId?: number | number[] | null; orgId?: number | null; oAuthClientId?: string | null; }; const getWebhooks = async (options: GetSubscriberOptions, prisma: PrismaClient = defaultPrisma) => { + const teamId = options.teamId; const userId = options.userId ?? 0; const eventTypeId = options.eventTypeId ?? 0; - const teamId = options.teamId ?? 0; + const teamIds = Array.isArray(teamId) ? teamId : [teamId ?? 0]; const orgId = options.orgId ?? 0; const oAuthClientId = options.oAuthClientId ?? ""; @@ -33,7 +34,7 @@ const getWebhooks = async (options: GetSubscriberOptions, prisma: PrismaClient = }, { teamId: { - in: [teamId, orgId], + in: [...teamIds, orgId], }, }, { platformOAuthClientId: oAuthClientId }, diff --git a/packages/features/webhooks/lib/scheduleTrigger.ts b/packages/features/webhooks/lib/scheduleTrigger.ts index f52399a829..31eddb5ec1 100644 --- a/packages/features/webhooks/lib/scheduleTrigger.ts +++ b/packages/features/webhooks/lib/scheduleTrigger.ts @@ -1,6 +1,7 @@ import type { Prisma, Webhook, Booking } from "@prisma/client"; import { v4 } from "uuid"; +import { selectOOOEntries } from "@calcom/app-store/zapier/api/subscriptions/listOOOEntries"; import { getHumanReadableLocationValue } from "@calcom/core/location"; import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses"; import logger from "@calcom/lib/logger"; @@ -451,3 +452,56 @@ export async function updateTriggerForExistingBookings( ); await Promise.all(promise); } + +export async function listOOOEntries( + appApiKey?: ApiKey, + account?: { + id: number; + name: string | null; + isTeam: boolean; + } | null +) { + const userId = appApiKey ? appApiKey.userId : account && !account.isTeam ? account.id : null; + const teamId = appApiKey ? appApiKey.teamId : account && account.isTeam ? account.id : null; + + try { + const where: Prisma.OutOfOfficeEntryWhereInput = {}; + if (teamId) { + where.user = { + teams: { + some: { + teamId, + }, + }, + }; + } else if (userId) { + where.userId = userId; + } + + // early return + if (!where.userId && !where.user) { + return []; + } + + const oooEntries = await prisma.outOfOfficeEntry.findMany({ + where: { + ...where, + }, + take: 3, + orderBy: { + id: "desc", + }, + select: selectOOOEntries, + }); + + if (oooEntries.length === 0) { + return []; + } + return oooEntries; + } catch (err) { + log.error( + `Error retrieving list of ooo entries for user ${userId}. or teamId ${teamId}`, + safeStringify(err) + ); + } +} diff --git a/packages/features/webhooks/lib/sendPayload.ts b/packages/features/webhooks/lib/sendPayload.ts index 304639e9d0..cda9abb73e 100644 --- a/packages/features/webhooks/lib/sendPayload.ts +++ b/packages/features/webhooks/lib/sendPayload.ts @@ -33,7 +33,7 @@ export type WithUTCOffsetType = T & { export type BookingNoShowUpdatedPayload = { message: string; bookingUid: string; - bookingId: number; + bookingId?: number; attendees: { email: string; noShow: boolean }[]; }; @@ -44,9 +44,39 @@ export type TranscriptionGeneratedPayload = { }; }; -export type WebhookDataType = CalendarEvent & +export type OOOEntryPayloadType = { + oooEntry: { + id: number; + start: string; + end: string; + createdAt: string; + updatedAt: string; + notes: string | null; + reason: { + emoji?: string; + reason?: string; + }; + reasonId: number; + user: { + id: number; + name: string | null; + username: string | null; + timeZone: string; + email: string; + }; + toUser: { + id: number; + name?: string | null; + username?: string | null; + timeZone?: string; + email?: string; + } | null; + uuid: string; + }; +}; + +export type EventPayloadType = CalendarEvent & TranscriptionGeneratedPayload & - // BookingNoShowUpdatedPayload & // This breaks all other webhooks EventTypeInfo & { metadata?: { [key: string]: string | number | boolean | null }; bookingId?: number; @@ -56,8 +86,6 @@ export type WebhookDataType = CalendarEvent & rescheduleUid?: string; rescheduleStartTime?: string; rescheduleEndTime?: string; - triggerEvent: string; - createdAt: string; downloadLink?: string; paymentId?: number; rescheduledBy?: string; @@ -65,28 +93,30 @@ export type WebhookDataType = CalendarEvent & paymentData?: Payment; }; -function addUTCOffset( - data: Omit -): WithUTCOffsetType { - if (data.organizer?.timeZone) { - (data.organizer as Person & UTCOffset).utcOffset = getUTCOffsetByTimezone( - data.organizer.timeZone, - data.startTime - ); +export type WebhookPayloadType = EventPayloadType | OOOEntryPayloadType | BookingNoShowUpdatedPayload; + +type WebhookDataType = WebhookPayloadType & { triggerEvent: string; createdAt: string }; + +function addUTCOffset(data: WebhookPayloadType): WithUTCOffsetType { + if (isEventPayload(data)) { + if (data.organizer?.timeZone) { + (data.organizer as Person & UTCOffset).utcOffset = getUTCOffsetByTimezone( + data.organizer.timeZone, + data.startTime + ); + } + + if (data.attendees?.length) { + (data.attendees as (Person & UTCOffset)[]).forEach((attendee) => { + attendee.utcOffset = getUTCOffsetByTimezone(attendee.timeZone, data.startTime); + }); + } } - if (data?.attendees?.length) { - (data.attendees as (Person & UTCOffset)[]).forEach((attendee) => { - attendee.utcOffset = getUTCOffsetByTimezone(attendee.timeZone, data.startTime); - }); - } - - return data as WithUTCOffsetType; + return data as WithUTCOffsetType; } -function getZapierPayload( - data: WithUTCOffsetType -): string { +function getZapierPayload(data: WithUTCOffsetType): string { const attendees = (data.attendees as (Person & UTCOffset)[]).map((attendee) => { return { name: attendee.name, @@ -150,34 +180,51 @@ export function jsonParse(jsonString: string) { return false; } +export function isOOOEntryPayload(data: WebhookPayloadType): data is OOOEntryPayloadType { + return "oooEntry" in data; +} + +export function isNoShowPayload(data: WebhookPayloadType): data is BookingNoShowUpdatedPayload { + return "message" in data; +} + +export function isEventPayload(data: WebhookPayloadType): data is EventPayloadType { + return !isNoShowPayload(data) && !isOOOEntryPayload(data); +} + const sendPayload = async ( secretKey: string | null, triggerEvent: string, createdAt: string, webhook: Pick, - data: Omit + data: WebhookPayloadType ) => { const { appId, payloadTemplate: template } = webhook; const contentType = !template || jsonParse(template) ? "application/json" : "application/x-www-form-urlencoded"; - data.description = data.description || data.additionalNotes; data = addUTCOffset(data); let body; - /* Zapier id is hardcoded in the DB, we send the raw data for this case */ - if (appId === "zapier") { - body = getZapierPayload({ ...data, createdAt }); - } else if (template) { - body = applyTemplate(template, { ...data, triggerEvent, createdAt }, contentType); - } else { - body = JSON.stringify({ - triggerEvent: triggerEvent, - createdAt: createdAt, - payload: data, - }); + if (isEventPayload(data)) { + data.description = data.description || data.additionalNotes; + if (appId === "zapier") { + body = getZapierPayload({ ...data, createdAt }); + } + } + + if (body === undefined) { + if (template && (isOOOEntryPayload(data) || isEventPayload(data) || isNoShowPayload(data))) { + body = applyTemplate(template, { ...data, triggerEvent, createdAt }, contentType); + } else { + body = JSON.stringify({ + triggerEvent: triggerEvent, + createdAt: createdAt, + payload: data, + }); + } } return _sendPayload(secretKey, webhook, body, contentType); diff --git a/packages/prisma/migrations/20240611054408_add_ooo_created_as_webhook_enum/migration.sql b/packages/prisma/migrations/20240611054408_add_ooo_created_as_webhook_enum/migration.sql new file mode 100644 index 0000000000..92d1e9e9ae --- /dev/null +++ b/packages/prisma/migrations/20240611054408_add_ooo_created_as_webhook_enum/migration.sql @@ -0,0 +1,2 @@ +-- AlterEnum +ALTER TYPE "WebhookTriggerEvents" ADD VALUE 'OOO_CREATED'; diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index bf78dd7d7e..a7af4dbc7f 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -754,6 +754,7 @@ enum WebhookTriggerEvents { RECORDING_READY INSTANT_MEETING RECORDING_TRANSCRIPTION_GENERATED + OOO_CREATED } model Webhook { diff --git a/packages/trpc/server/routers/loggedInViewer/outOfOffice.handler.ts b/packages/trpc/server/routers/loggedInViewer/outOfOffice.handler.ts index 81bcf3ab71..53a1110abe 100644 --- a/packages/trpc/server/routers/loggedInViewer/outOfOffice.handler.ts +++ b/packages/trpc/server/routers/loggedInViewer/outOfOffice.handler.ts @@ -1,7 +1,13 @@ +import type { Prisma } from "@prisma/client"; import { v4 as uuidv4 } from "uuid"; +import { selectOOOEntries } from "@calcom/app-store/zapier/api/subscriptions/listOOOEntries"; import dayjs from "@calcom/dayjs"; import { sendBookingRedirectNotification } from "@calcom/emails"; +import type { GetSubscriberOptions } from "@calcom/features/webhooks/lib/getWebhooks"; +import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks"; +import type { OOOEntryPayloadType } from "@calcom/features/webhooks/lib/sendPayload"; +import sendPayload from "@calcom/features/webhooks/lib/sendPayload"; import { getTranslation } from "@calcom/lib/server"; import prisma from "@calcom/prisma"; import type { TrpcSessionUser } from "@calcom/trpc/server/trpc"; @@ -9,6 +15,7 @@ import type { TrpcSessionUser } from "@calcom/trpc/server/trpc"; import { TRPCError } from "@trpc/server"; import type { TOutOfOfficeDelete, TOutOfOfficeInputSchema } from "./outOfOffice.schema"; +import { WebhookTriggerEvents } from ".prisma/client"; type TBookingRedirect = { ctx: { @@ -44,7 +51,7 @@ export const outOfOfficeCreateOrUpdate = async ({ ctx, input }: TBookingRedirect throw new TRPCError({ code: "BAD_REQUEST", message: "start_date_must_be_in_the_future" }); } - let toUserId; + let toUserId: number | null = null; if (input.toTeamUserId) { const user = await prisma.user.findUnique({ @@ -130,7 +137,7 @@ export const outOfOfficeCreateOrUpdate = async ({ ctx, input }: TBookingRedirect toUserId: true, }, where: { - userId: toUserId, + ...(toUserId && { userId: toUserId }), toUserId: ctx.user.id, // Check for time overlap or collision OR: [ @@ -200,7 +207,43 @@ export const outOfOfficeCreateOrUpdate = async ({ ctx, input }: TBookingRedirect toUserId: toUserId ? toUserId : null, }, }); - + let resultRedirect: Prisma.OutOfOfficeEntryGetPayload<{ select: typeof selectOOOEntries }> | null = null; + if (createdOrUpdatedOutOfOffice) { + const findRedirect = await prisma.outOfOfficeEntry.findFirst({ + where: { + uuid: createdOrUpdatedOutOfOffice.uuid, + }, + select: selectOOOEntries, + }); + if (findRedirect) { + resultRedirect = findRedirect; + } + } + if (!resultRedirect) { + return; + } + const toUser = toUserId + ? await prisma.user.findFirst({ + where: { + id: toUserId, + }, + select: { + name: true, + username: true, + timeZone: true, + email: true, + }, + }) + : null; + const reason = await prisma.outOfOfficeReason.findFirst({ + where: { + id: input.reasonId, + }, + select: { + reason: true, + emoji: true, + }, + }); if (toUserId) { // await send email to notify user const userToNotify = await prisma.user.findFirst({ @@ -275,6 +318,76 @@ export const outOfOfficeCreateOrUpdate = async ({ ctx, input }: TBookingRedirect } } + const memberships = await prisma.membership.findMany({ + where: { + userId: ctx.user.id, + accepted: true, + }, + }); + + const teamIds = memberships.map((membership) => membership.teamId); + + // Send webhook to notify other services + const subscriberOptions: GetSubscriberOptions = { + userId: ctx.user.id, + teamId: teamIds, + orgId: ctx.user.organizationId, + triggerEvent: WebhookTriggerEvents.OOO_CREATED, + }; + + const subscribers = await getWebhooks(subscriberOptions); + + const payload: OOOEntryPayloadType = { + oooEntry: { + id: createdOrUpdatedOutOfOffice.id, + start: dayjs(createdOrUpdatedOutOfOffice.start) + .tz(ctx.user.timeZone, true) + .format("YYYY-MM-DDTHH:mm:ssZ"), + end: dayjs(createdOrUpdatedOutOfOffice.end).tz(ctx.user.timeZone, true).format("YYYY-MM-DDTHH:mm:ssZ"), + createdAt: createdOrUpdatedOutOfOffice.createdAt.toISOString(), + updatedAt: createdOrUpdatedOutOfOffice.updatedAt.toISOString(), + notes: createdOrUpdatedOutOfOffice.notes, + reason: { + emoji: reason?.emoji, + reason: reason?.reason, + }, + reasonId: input.reasonId, + user: { + id: ctx.user.id, + name: ctx.user.name, + username: ctx.user.username, + email: ctx.user.email, + timeZone: ctx.user.timeZone, + }, + toUser: toUserId + ? { + id: toUserId, + name: toUser?.name, + username: toUser?.username, + email: toUser?.email, + timeZone: toUser?.timeZone, + } + : null, + uuid: createdOrUpdatedOutOfOffice.uuid, + }, + }; + + await Promise.all( + subscribers.map(async (subscriber) => { + sendPayload( + subscriber.secret, + WebhookTriggerEvents.OOO_CREATED, + dayjs().toISOString(), + { + appId: subscriber.appId, + subscriberUrl: subscriber.subscriberUrl, + payloadTemplate: subscriber.payloadTemplate, + }, + payload + ); + }) + ); + return {}; }; diff --git a/packages/trpc/server/routers/viewer/bookings/confirm.handler.ts b/packages/trpc/server/routers/viewer/bookings/confirm.handler.ts index dc33184a12..b0011a32c8 100644 --- a/packages/trpc/server/routers/viewer/bookings/confirm.handler.ts +++ b/packages/trpc/server/routers/viewer/bookings/confirm.handler.ts @@ -8,7 +8,7 @@ import { sendDeclinedEmails } from "@calcom/emails"; import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses"; import { handleConfirmation } from "@calcom/features/bookings/lib/handleConfirmation"; import { handleWebhookTrigger } from "@calcom/features/bookings/lib/handleWebhookTrigger"; -import type { EventTypeInfo } from "@calcom/features/webhooks/lib/sendPayload"; +import type { EventPayloadType, EventTypeInfo } from "@calcom/features/webhooks/lib/sendPayload"; import { isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib"; import { getBookerBaseUrl } from "@calcom/lib/getBookerUrl/server"; import getOrgIdFromMemberOrTeamId from "@calcom/lib/getOrgIdFromMemberOrTeamId"; @@ -399,7 +399,7 @@ export const confirmHandler = async ({ ctx, input }: ConfirmOptions) => { currency: booking.eventType?.currency, length: booking.eventType?.length, }; - const webhookData = { + const webhookData: EventPayloadType = { ...evt, ...eventTypeInfo, bookingId, diff --git a/packages/trpc/server/routers/viewer/bookings/requestReschedule.handler.ts b/packages/trpc/server/routers/viewer/bookings/requestReschedule.handler.ts index 6d5540a0e8..dbb77d48a3 100644 --- a/packages/trpc/server/routers/viewer/bookings/requestReschedule.handler.ts +++ b/packages/trpc/server/routers/viewer/bookings/requestReschedule.handler.ts @@ -296,10 +296,11 @@ export const requestRescheduleHandler = async ({ ctx, input }: RequestReschedule userId, eventTypeId: bookingToReschedule.eventTypeId as number, triggerEvent: eventTrigger, - teamId, + teamId: teamId ? [teamId] : null, orgId, }; const webhooks = await getWebhooks(subscriberOptions); + const promises = webhooks.map((webhook) => sendPayload(webhook.secret, eventTrigger, new Date().toISOString(), webhook, { ...evt, diff --git a/packages/trpc/server/routers/viewer/webhook/create.handler.ts b/packages/trpc/server/routers/viewer/webhook/create.handler.ts index de8f74f2de..7d410a63bc 100644 --- a/packages/trpc/server/routers/viewer/webhook/create.handler.ts +++ b/packages/trpc/server/routers/viewer/webhook/create.handler.ts @@ -1,4 +1,5 @@ import type { Webhook } from "@prisma/client"; +import type { Prisma } from "@prisma/client"; import { v4 } from "uuid"; import { updateTriggerForExistingBookings } from "@calcom/features/webhooks/lib/scheduleTrigger"; @@ -17,33 +18,29 @@ type CreateOptions = { }; export const createHandler = async ({ ctx, input }: CreateOptions) => { + const { user } = ctx; + + const webhookData: Prisma.WebhookCreateInput = { + id: v4(), + ...input, + }; + if (input.platform && user.role !== "ADMIN") { + throw new TRPCError({ code: "UNAUTHORIZED" }); + } + + // Add userId if platform, eventTypeId, and teamId are not provided + if (!input.platform && !input.eventTypeId && !input.teamId) { + webhookData.user = { connect: { id: user.id } }; + } + let newWebhook: Webhook; - if (input.platform) { - const { user } = ctx; - if (user?.role !== "ADMIN") { - throw new TRPCError({ code: "UNAUTHORIZED" }); - } + try { newWebhook = await prisma.webhook.create({ - data: { - id: v4(), - ...input, - }, - }); - } else if (input.eventTypeId || input.teamId) { - newWebhook = await prisma.webhook.create({ - data: { - id: v4(), - ...input, - }, - }); - } else { - newWebhook = await prisma.webhook.create({ - data: { - id: v4(), - userId: ctx.user.id, - ...input, - }, + data: webhookData, }); + } catch (error) { + // Avoid printing raw prisma error on frontend + throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Failed to create webhook" }); } await updateTriggerForExistingBookings(newWebhook, [], newWebhook.eventTriggers); diff --git a/packages/trpc/server/routers/viewer/webhook/testTrigger.handler.ts b/packages/trpc/server/routers/viewer/webhook/testTrigger.handler.ts index b4a0c0f46f..bc8a465c22 100644 --- a/packages/trpc/server/routers/viewer/webhook/testTrigger.handler.ts +++ b/packages/trpc/server/routers/viewer/webhook/testTrigger.handler.ts @@ -1,3 +1,4 @@ +import type { EventPayloadType } from "@calcom/features/webhooks/lib/sendPayload"; import sendPayload from "@calcom/features/webhooks/lib/sendPayload"; import { getErrorFromUnknown } from "@calcom/lib/errors"; import { getTranslation } from "@calcom/lib/server/i18n"; @@ -17,7 +18,7 @@ export const testTriggerHandler = async ({ ctx: _ctx, input }: TestTriggerOption translate: translation, }; - const data = { + const data: EventPayloadType = { type: "Test", title: "Test trigger event", description: "",