feat: OOO webhook and zapier (#15434)
* 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 <wollencarina@gmail.com> Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com> Co-authored-by: Keith Williams <keithwillcode@gmail.com> Co-authored-by: Omar López <zomars@me.com>
This commit is contained in:
co-authored by
Carina Wollendorfer
CarinaWolli
Udit Takkar
Keith Williams
Omar López
parent
105e7d3e28
commit
f5c16f0d6b
@@ -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}`,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 `<baseUrl>`/api/integrations/zapier/addSubscription
|
||||
- Request Body
|
||||
- subscriberUrl: {{bundle.targetUrl}}
|
||||
- triggerEvent: OOO_CREATED
|
||||
- Unsubscribe: DELETE `<baseUrl>`/api/integrations/zapier/deleteSubscription
|
||||
- URL Params (in addition to apiKey)
|
||||
- id: {{bundle.subscribeData.id}}
|
||||
- PerformList: GET `<baseUrl>`/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
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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) }),
|
||||
});
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<WebhookDataType, "createdAt" | "triggerEvent">;
|
||||
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)
|
||||
);
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -43,6 +43,7 @@ const WEBHOOK_TRIGGER_EVENTS_GROUPED_BY_APP_V2: Record<string, WebhookTriggerEve
|
||||
{ value: WebhookTriggerEvents.MEETING_STARTED, label: "meeting_started" },
|
||||
{ value: WebhookTriggerEvents.RECORDING_READY, label: "recording_ready" },
|
||||
{ value: WebhookTriggerEvents.INSTANT_MEETING, label: "instant_meeting" },
|
||||
{ value: WebhookTriggerEvents.OOO_CREATED, label: "ooo_created" },
|
||||
{
|
||||
value: WebhookTriggerEvents.RECORDING_TRANSCRIPTION_GENERATED,
|
||||
label: "recording_transcription_generated",
|
||||
@@ -100,7 +101,6 @@ const WebhookForm = (props: {
|
||||
const [newSecret, setNewSecret] = useState("");
|
||||
const [changeSecret, setChangeSecret] = useState<boolean>(false);
|
||||
const hasSecretKey = !!props?.webhook?.secret;
|
||||
// const currentSecret = props?.webhook?.secret;
|
||||
|
||||
useEffect(() => {
|
||||
if (changeSecret) {
|
||||
@@ -290,7 +290,7 @@ const WebhookForm = (props: {
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={!formMethods.formState.isDirty && !changeSecret}
|
||||
loading={formMethods.formState.isSubmitting || formMethods.formState.isSubmitted}>
|
||||
loading={formMethods.formState.isSubmitting}>
|
||||
{props?.webhook?.id ? t("save") : t("create_webhook")}
|
||||
</Button>
|
||||
</SectionBottomActions>
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ export type WithUTCOffsetType<T> = 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<WebhookDataType, "createdAt" | "triggerEvent">
|
||||
): WithUTCOffsetType<WebhookDataType> {
|
||||
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<WebhookPayloadType> {
|
||||
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<WebhookDataType>;
|
||||
return data as WithUTCOffsetType<WebhookPayloadType>;
|
||||
}
|
||||
|
||||
function getZapierPayload(
|
||||
data: WithUTCOffsetType<CalendarEvent & EventTypeInfo & { status?: string; createdAt: string }>
|
||||
): string {
|
||||
function getZapierPayload(data: WithUTCOffsetType<EventPayloadType & { createdAt: string }>): 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<Webhook, "subscriberUrl" | "appId" | "payloadTemplate">,
|
||||
data: Omit<WebhookDataType, "createdAt" | "triggerEvent">
|
||||
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);
|
||||
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
-- AlterEnum
|
||||
ALTER TYPE "WebhookTriggerEvents" ADD VALUE 'OOO_CREATED';
|
||||
@@ -754,6 +754,7 @@ enum WebhookTriggerEvents {
|
||||
RECORDING_READY
|
||||
INSTANT_MEETING
|
||||
RECORDING_TRANSCRIPTION_GENERATED
|
||||
OOO_CREATED
|
||||
}
|
||||
|
||||
model Webhook {
|
||||
|
||||
@@ -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 {};
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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: "",
|
||||
|
||||
Reference in New Issue
Block a user