Zapier Trigger: After Meeting ends (#3827)
* add new trigger with first simple job scheduling * fix DB update * use array to save scheduled jobs in booking * cancel scheduled jobs when zap disabled or zapier disconnected * schedule jobs only for confirmed bookings * schedule jobs for already existing bookings * fix bug to create workflow reminders when confirming recurring events * delete remove all zapier webhooks when api key is deleted * schedule job for all confirmed recurring bookings * fix zapier trigger and workflow reminders when cancelling recurring events * code clean up * code clean up * add migration * add type package for node-schedule * remove nodescheduler * add updated nodescheduler * move code to app-store * add meeting ended to webhook constants * udpate zapier README.md * implement QA suggestions * add default handler and fix imports * Type fix Co-authored-by: CarinaWolli <wollencarina@gmail.com> Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> Co-authored-by: Omar López <zomars@me.com>
This commit is contained in:
18 files changed
+452
-83
No files matched your search
@@ -7,11 +7,12 @@ export const WEBHOOK_TRIGGER_EVENTS_GROUPED_BY_APP = {
|
||||
WebhookTriggerEvents.BOOKING_CANCELLED,
|
||||
WebhookTriggerEvents.BOOKING_CREATED,
|
||||
WebhookTriggerEvents.BOOKING_RESCHEDULED,
|
||||
] as ["BOOKING_CANCELLED", "BOOKING_CREATED", "BOOKING_RESCHEDULED"],
|
||||
WebhookTriggerEvents.MEETING_ENDED,
|
||||
] as const,
|
||||
routing_forms: [WebhookTriggerEvents.FORM_SUBMITTED] as ["FORM_SUBMITTED"],
|
||||
};
|
||||
|
||||
export const WEBHOOK_TRIGGER_EVENTS = [
|
||||
...WEBHOOK_TRIGGER_EVENTS_GROUPED_BY_APP.core,
|
||||
...WEBHOOK_TRIGGER_EVENTS_GROUPED_BY_APP.routing_forms,
|
||||
] as ["BOOKING_CANCELLED", "BOOKING_CREATED", "BOOKING_RESCHEDULED", "FORM_SUBMITTED"];
|
||||
] as const;
|
||||
@@ -30,6 +30,7 @@ const getWebhooks = async (options: GetSubscriberOptions) => {
|
||||
},
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
subscriberUrl: true,
|
||||
payloadTemplate: true,
|
||||
appId: true,
|
||||
|
||||
@@ -1,8 +1,19 @@
|
||||
import { Booking, BookingStatus, Prisma, SchedulingType, User } from "@prisma/client";
|
||||
import {
|
||||
Booking,
|
||||
BookingStatus,
|
||||
Prisma,
|
||||
SchedulingType,
|
||||
User,
|
||||
WebhookTriggerEvents,
|
||||
Workflow,
|
||||
WorkflowsOnEventTypes,
|
||||
WorkflowStep,
|
||||
} from "@prisma/client";
|
||||
import type { NextApiRequest } from "next";
|
||||
import { z } from "zod";
|
||||
|
||||
import { refund } from "@calcom/app-store/stripepayment/lib/server";
|
||||
import { scheduleTrigger } from "@calcom/app-store/zapier/lib/nodeScheduler";
|
||||
import EventManager from "@calcom/core/EventManager";
|
||||
import { sendDeclinedEmails, sendScheduledEmails } from "@calcom/emails";
|
||||
import { scheduleWorkflowReminders } from "@calcom/features/ee/workflows/lib/reminders/reminderScheduler";
|
||||
@@ -14,6 +25,7 @@ import type { AdditionalInformation, CalendarEvent } from "@calcom/types/Calenda
|
||||
|
||||
import { getSession } from "@lib/auth";
|
||||
import { HttpError } from "@lib/core/http/error";
|
||||
import getSubscribers from "@lib/webhooks/subscriptions";
|
||||
|
||||
import { getTranslation } from "@server/lib/i18n";
|
||||
|
||||
@@ -30,6 +42,7 @@ const authorized = async (
|
||||
id: booking.eventTypeId || undefined,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
schedulingType: true,
|
||||
users: true,
|
||||
},
|
||||
@@ -129,6 +142,7 @@ async function patchHandler(req: NextApiRequest) {
|
||||
recurringEventId: true,
|
||||
status: true,
|
||||
smsReminderNumber: true,
|
||||
scheduledJobs: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -237,6 +251,21 @@ async function patchHandler(req: NextApiRequest) {
|
||||
log.error(error);
|
||||
}
|
||||
}
|
||||
let updatedBookings: {
|
||||
scheduledJobs: string[];
|
||||
id: number;
|
||||
startTime: Date;
|
||||
endTime: Date;
|
||||
uid: string;
|
||||
smsReminderNumber: string | null;
|
||||
eventType: {
|
||||
workflows: (WorkflowsOnEventTypes & {
|
||||
workflow: Workflow & {
|
||||
steps: WorkflowStep[];
|
||||
};
|
||||
})[];
|
||||
} | null;
|
||||
}[] = [];
|
||||
|
||||
if (recurringEventId) {
|
||||
// The booking to confirm is a recurring event and comes from /booking/recurring, proceeding to mark all related
|
||||
@@ -247,8 +276,9 @@ async function patchHandler(req: NextApiRequest) {
|
||||
status: BookingStatus.PENDING,
|
||||
},
|
||||
});
|
||||
unconfirmedRecurringBookings.map(async (recurringBooking) => {
|
||||
await prisma.booking.update({
|
||||
|
||||
const updateBookingsPromise = unconfirmedRecurringBookings.map((recurringBooking) => {
|
||||
return prisma.booking.update({
|
||||
where: {
|
||||
id: recurringBooking.id,
|
||||
},
|
||||
@@ -258,12 +288,35 @@ async function patchHandler(req: NextApiRequest) {
|
||||
create: scheduleResult.referencesToCreate,
|
||||
},
|
||||
},
|
||||
select: {
|
||||
eventType: {
|
||||
select: {
|
||||
workflows: {
|
||||
include: {
|
||||
workflow: {
|
||||
include: {
|
||||
steps: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
uid: true,
|
||||
startTime: true,
|
||||
endTime: true,
|
||||
smsReminderNumber: true,
|
||||
id: true,
|
||||
scheduledJobs: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
const updatedBookingsResult = await Promise.all(updateBookingsPromise);
|
||||
updatedBookings = updatedBookings.concat(updatedBookingsResult);
|
||||
} else {
|
||||
// @NOTE: be careful with this as if any error occurs before this booking doesn't get confirmed
|
||||
// Should perform update on booking (confirm) -> then trigger the rest handlers
|
||||
await prisma.booking.update({
|
||||
const updatedBooking = await prisma.booking.update({
|
||||
where: {
|
||||
id: bookingId,
|
||||
},
|
||||
@@ -273,13 +326,62 @@ async function patchHandler(req: NextApiRequest) {
|
||||
create: scheduleResult.referencesToCreate,
|
||||
},
|
||||
},
|
||||
select: {
|
||||
eventType: {
|
||||
select: {
|
||||
workflows: {
|
||||
include: {
|
||||
workflow: {
|
||||
include: {
|
||||
steps: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
uid: true,
|
||||
startTime: true,
|
||||
endTime: true,
|
||||
smsReminderNumber: true,
|
||||
id: true,
|
||||
scheduledJobs: true,
|
||||
},
|
||||
});
|
||||
updatedBookings.push(updatedBooking);
|
||||
}
|
||||
|
||||
//Workflows - set reminders for confirmed events
|
||||
if (booking.eventType?.workflows) {
|
||||
await scheduleWorkflowReminders(booking.eventType.workflows, booking.smsReminderNumber, evt, false);
|
||||
for (const updatedBooking of updatedBookings) {
|
||||
if (updatedBooking.eventType?.workflows) {
|
||||
const evtOfBooking = evt;
|
||||
evtOfBooking.startTime = updatedBooking.startTime.toISOString();
|
||||
evtOfBooking.endTime = updatedBooking.endTime.toISOString();
|
||||
evtOfBooking.uid = updatedBooking.uid;
|
||||
|
||||
await scheduleWorkflowReminders(
|
||||
updatedBooking.eventType.workflows,
|
||||
updatedBooking.smsReminderNumber,
|
||||
evtOfBooking,
|
||||
false
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// schedule job for zapier trigger 'when meeting ends'
|
||||
const subscriberOptionsMeetingEnded = {
|
||||
userId: booking.userId || 0,
|
||||
eventTypeId: booking.eventTypeId || 0,
|
||||
triggerEvent: WebhookTriggerEvents.MEETING_ENDED,
|
||||
};
|
||||
|
||||
const subscribersMeetingEnded = await getSubscribers(subscriberOptionsMeetingEnded);
|
||||
|
||||
subscribersMeetingEnded.forEach((subscriber) => {
|
||||
updatedBookings.forEach((booking) => {
|
||||
scheduleTrigger(booking, subscriber.subscriberUrl, subscriber);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
evt.rejectionReason = rejectionReason;
|
||||
if (recurringEventId) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import short from "short-uuid";
|
||||
import { v5 as uuidv5 } from "uuid";
|
||||
|
||||
import { handlePayment } from "@calcom/app-store/stripepayment/lib/server";
|
||||
import { cancelScheduledJobs, scheduleTrigger } from "@calcom/app-store/zapier/lib/nodeScheduler";
|
||||
import EventManager from "@calcom/core/EventManager";
|
||||
import { getUserAvailability } from "@calcom/core/getUserAvailability";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
@@ -791,13 +792,32 @@ async function handler(req: NextApiRequest) {
|
||||
|
||||
log.debug(`Booking ${organizerUser.username} completed`);
|
||||
|
||||
const eventTrigger: WebhookTriggerEvents = rescheduleUid ? "BOOKING_RESCHEDULED" : "BOOKING_CREATED";
|
||||
const eventTrigger: WebhookTriggerEvents = rescheduleUid
|
||||
? WebhookTriggerEvents.BOOKING_RESCHEDULED
|
||||
: WebhookTriggerEvents.BOOKING_CREATED;
|
||||
const subscriberOptions = {
|
||||
userId: organizerUser.id,
|
||||
eventTypeId,
|
||||
triggerEvent: eventTrigger,
|
||||
};
|
||||
|
||||
const subscriberOptionsMeetingEnded = {
|
||||
userId: organizerUser.id,
|
||||
eventTypeId,
|
||||
triggerEvent: WebhookTriggerEvents.MEETING_ENDED,
|
||||
};
|
||||
|
||||
const subscribersMeetingEnded = await getSubscribers(subscriberOptionsMeetingEnded);
|
||||
|
||||
subscribersMeetingEnded.forEach((subscriber) => {
|
||||
if (rescheduleUid && originalRescheduledBooking) {
|
||||
cancelScheduledJobs(originalRescheduledBooking);
|
||||
}
|
||||
if (booking && booking.status === BookingStatus.ACCEPTED) {
|
||||
scheduleTrigger(booking, subscriber.subscriberUrl, subscriber);
|
||||
}
|
||||
});
|
||||
|
||||
// Send Webhook call if hooked to BOOKING_CREATED & BOOKING_RESCHEDULED
|
||||
const subscribers = await getSubscribers(subscriberOptions);
|
||||
console.log("evt:", {
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
import { BookingStatus, Prisma, PrismaPromise, WebhookTriggerEvents, WorkflowMethods } from "@prisma/client";
|
||||
import {
|
||||
BookingStatus,
|
||||
Prisma,
|
||||
PrismaPromise,
|
||||
WebhookTriggerEvents,
|
||||
WorkflowMethods,
|
||||
WorkflowReminder,
|
||||
} from "@prisma/client";
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
import z from "zod";
|
||||
|
||||
import { getCalendar } from "@calcom/app-store/_utils/getCalendar";
|
||||
import { FAKE_DAILY_CREDENTIAL } from "@calcom/app-store/dailyvideo/lib/VideoApiAdapter";
|
||||
import { refund } from "@calcom/app-store/stripepayment/lib/server";
|
||||
import { cancelScheduledJobs } from "@calcom/app-store/zapier/lib/nodeScheduler";
|
||||
import { deleteMeeting } from "@calcom/core/videoClient";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { sendCancelledEmails } from "@calcom/emails";
|
||||
@@ -82,6 +90,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
destinationCalendar: true,
|
||||
smsReminderNumber: true,
|
||||
workflowReminders: true,
|
||||
scheduledJobs: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -164,6 +173,21 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
);
|
||||
await Promise.all(promises);
|
||||
|
||||
//Workflows - schedule reminders
|
||||
if (bookingToDelete.eventType?.workflows) {
|
||||
await sendCancelledReminders(
|
||||
bookingToDelete.eventType?.workflows,
|
||||
bookingToDelete.smsReminderNumber,
|
||||
evt
|
||||
);
|
||||
}
|
||||
|
||||
let updatedBookings: {
|
||||
uid: string;
|
||||
workflowReminders: WorkflowReminder[];
|
||||
scheduledJobs: string[];
|
||||
}[] = [];
|
||||
|
||||
// by cancelling first, and blocking whilst doing so; we can ensure a cancel
|
||||
// action always succeeds even if subsequent integrations fail cancellation.
|
||||
if (bookingToDelete.eventType?.recurringEvent && bookingToDelete.recurringEventId && allRemainingBookings) {
|
||||
@@ -181,8 +205,22 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
cancellationReason: cancellationReason,
|
||||
},
|
||||
});
|
||||
const allUpdatedBookings = await prisma.booking.findMany({
|
||||
where: {
|
||||
recurringEventId: bookingToDelete.recurringEventId,
|
||||
startTime: {
|
||||
gte: new Date(),
|
||||
},
|
||||
},
|
||||
select: {
|
||||
workflowReminders: true,
|
||||
uid: true,
|
||||
scheduledJobs: true,
|
||||
},
|
||||
});
|
||||
updatedBookings = updatedBookings.concat(allUpdatedBookings);
|
||||
} else {
|
||||
await prisma.booking.update({
|
||||
const updatedBooking = await prisma.booking.update({
|
||||
where: {
|
||||
uid,
|
||||
},
|
||||
@@ -190,7 +228,13 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
status: BookingStatus.CANCELLED,
|
||||
cancellationReason: cancellationReason,
|
||||
},
|
||||
select: {
|
||||
workflowReminders: true,
|
||||
uid: true,
|
||||
scheduledJobs: true,
|
||||
},
|
||||
});
|
||||
updatedBookings.push(updatedBooking);
|
||||
}
|
||||
|
||||
/** TODO: Remove this without breaking functionality */
|
||||
@@ -299,37 +343,35 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
},
|
||||
});
|
||||
|
||||
//Workflows - delete all reminders for that booking
|
||||
// delete scheduled jobs of cancelled bookings
|
||||
updatedBookings.forEach((booking) => {
|
||||
cancelScheduledJobs(booking);
|
||||
});
|
||||
|
||||
//Workflows - delete all reminders for bookings
|
||||
const remindersToDelete: PrismaPromise<Prisma.BatchPayload>[] = [];
|
||||
bookingToDelete.workflowReminders.forEach((reminder) => {
|
||||
if (reminder.scheduled && reminder.referenceId) {
|
||||
if (reminder.method === WorkflowMethods.EMAIL) {
|
||||
deleteScheduledEmailReminder(reminder.referenceId);
|
||||
} else if (reminder.method === WorkflowMethods.SMS) {
|
||||
deleteScheduledSMSReminder(reminder.referenceId);
|
||||
updatedBookings.forEach((booking) => {
|
||||
booking.workflowReminders.forEach((reminder) => {
|
||||
if (reminder.scheduled && reminder.referenceId) {
|
||||
if (reminder.method === WorkflowMethods.EMAIL) {
|
||||
deleteScheduledEmailReminder(reminder.referenceId);
|
||||
} else if (reminder.method === WorkflowMethods.SMS) {
|
||||
deleteScheduledSMSReminder(reminder.referenceId);
|
||||
}
|
||||
}
|
||||
}
|
||||
const reminderToDelete = prisma.workflowReminder.deleteMany({
|
||||
where: {
|
||||
id: reminder.id,
|
||||
},
|
||||
const reminderToDelete = prisma.workflowReminder.deleteMany({
|
||||
where: {
|
||||
id: reminder.id,
|
||||
},
|
||||
});
|
||||
remindersToDelete.push(reminderToDelete);
|
||||
});
|
||||
remindersToDelete.push(reminderToDelete);
|
||||
});
|
||||
|
||||
await Promise.all([apiDeletes, attendeeDeletes, bookingReferenceDeletes].concat(remindersToDelete));
|
||||
|
||||
await sendCancelledEmails(evt);
|
||||
|
||||
//Workflows - schedule reminders
|
||||
if (bookingToDelete.eventType?.workflows) {
|
||||
await sendCancelledReminders(
|
||||
bookingToDelete.eventType?.workflows,
|
||||
bookingToDelete.smsReminderNumber,
|
||||
evt
|
||||
);
|
||||
}
|
||||
|
||||
res.status(204).end();
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ If you run it on localhost, check out the [additional information](https://githu
|
||||
|
||||
## Triggers
|
||||
|
||||
Booking created, Booking rescheduled, Booking cancelled
|
||||
Booking created, Booking rescheduled, Booking cancelled, Meeting ended
|
||||
|
||||
### Booking created
|
||||
|
||||
@@ -54,7 +54,7 @@ Booking created, Booking rescheduled, Booking cancelled
|
||||
- PerformList: GET `<baseUrl>`/api/integrations/zapier/listBookings
|
||||
3. Test your API request
|
||||
|
||||
Create the other two triggers (booking rescheduled, booking cancelled) exactly like this one, just use the appropriate naming (e.g. booking_rescheduled instead of booking_created)
|
||||
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)
|
||||
|
||||
### Set ZAPIER_INVITE_LINK
|
||||
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { BookingStatus, WebhookTriggerEvents } from "@prisma/client";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { v4 } from "uuid";
|
||||
|
||||
import { scheduleTrigger } from "@calcom/app-store/zapier/lib/nodeScheduler";
|
||||
import findValidApiKey from "@calcom/features/ee/api-keys/lib/findValidApiKey";
|
||||
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const apiKey = req.query.apiKey as string;
|
||||
|
||||
if (!apiKey) {
|
||||
@@ -19,21 +22,43 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
|
||||
const { subscriberUrl, triggerEvent } = req.body;
|
||||
|
||||
if (req.method === "POST") {
|
||||
try {
|
||||
const createSubscription = await prisma.webhook.create({
|
||||
data: {
|
||||
id: v4(),
|
||||
try {
|
||||
const createSubscription = await prisma.webhook.create({
|
||||
data: {
|
||||
id: v4(),
|
||||
userId: validKey.userId,
|
||||
eventTriggers: [triggerEvent],
|
||||
subscriberUrl,
|
||||
active: true,
|
||||
appId: "zapier",
|
||||
},
|
||||
});
|
||||
|
||||
if (triggerEvent === WebhookTriggerEvents.MEETING_ENDED) {
|
||||
//schedule job for already existing bookings
|
||||
const bookings = await prisma.booking.findMany({
|
||||
where: {
|
||||
userId: validKey.userId,
|
||||
eventTriggers: [triggerEvent],
|
||||
subscriberUrl,
|
||||
active: true,
|
||||
appId: "zapier",
|
||||
startTime: {
|
||||
gte: new Date(),
|
||||
},
|
||||
status: BookingStatus.ACCEPTED,
|
||||
},
|
||||
});
|
||||
res.status(200).json(createSubscription);
|
||||
} catch (error) {
|
||||
return res.status(500).json({ message: "Could not create subscription." });
|
||||
|
||||
for (const booking of bookings) {
|
||||
scheduleTrigger(booking, createSubscription.subscriberUrl, {
|
||||
id: createSubscription.id,
|
||||
appId: createSubscription.appId,
|
||||
});
|
||||
}
|
||||
}
|
||||
res.status(200).json(createSubscription);
|
||||
} catch (error) {
|
||||
return res.status(500).json({ message: "Could not create subscription." });
|
||||
}
|
||||
}
|
||||
|
||||
export default defaultHandler({
|
||||
POST: Promise.resolve({ default: defaultResponder(handler) }),
|
||||
});
|
||||
@@ -1,10 +1,18 @@
|
||||
import { WebhookTriggerEvents } from "@prisma/client";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import z from "zod";
|
||||
|
||||
import findValidApiKey from "@calcom/features/ee/api-keys/lib/findValidApiKey";
|
||||
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const apiKey = req.query.apiKey as string;
|
||||
const querySchema = z.object({
|
||||
apiKey: z.string(),
|
||||
id: z.string(),
|
||||
});
|
||||
|
||||
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const { apiKey, id } = querySchema.parse(req.query);
|
||||
|
||||
if (!apiKey) {
|
||||
return res.status(401).json({ message: "No API key provided" });
|
||||
@@ -16,14 +24,44 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
return res.status(401).json({ message: "API key not valid" });
|
||||
}
|
||||
|
||||
const id = req.query.id as string;
|
||||
const webhook = await prisma.webhook.findFirst({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
|
||||
if (req.method === "DELETE") {
|
||||
await prisma.webhook.delete({
|
||||
if (webhook?.eventTriggers.includes(WebhookTriggerEvents.MEETING_ENDED)) {
|
||||
const bookingsWithScheduledJobs = await prisma.booking.findMany({
|
||||
where: {
|
||||
id,
|
||||
userId: validKey.userId,
|
||||
scheduledJobs: {
|
||||
isEmpty: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
res.status(204).json({ message: "Subscription is deleted." });
|
||||
for (const booking of bookingsWithScheduledJobs) {
|
||||
const updatedScheduledJobs = booking.scheduledJobs.filter(
|
||||
(scheduledJob) => scheduledJob !== `zapier_${webhook.id}`
|
||||
);
|
||||
await prisma.booking.update({
|
||||
where: {
|
||||
id: booking.id,
|
||||
},
|
||||
data: {
|
||||
scheduledJobs: updatedScheduledJobs,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.webhook.delete({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
res.status(204).json({ message: "Subscription is deleted." });
|
||||
}
|
||||
|
||||
export default defaultHandler({
|
||||
DELETE: Promise.resolve({ default: defaultResponder(handler) }),
|
||||
});
|
||||
@@ -1,9 +1,10 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import findValidApiKey from "@calcom/features/ee/api-keys/lib/findValidApiKey";
|
||||
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
|
||||
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const apiKey = req.query.apiKey as string;
|
||||
|
||||
if (!apiKey) {
|
||||
@@ -16,30 +17,32 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
return res.status(401).json({ message: "API key not valid" });
|
||||
}
|
||||
|
||||
if (req.method === "GET") {
|
||||
try {
|
||||
const bookings = await prisma.booking.findMany({
|
||||
take: 3,
|
||||
where: {
|
||||
userId: validKey.userId,
|
||||
},
|
||||
select: {
|
||||
...bookingMinimalSelect,
|
||||
location: true,
|
||||
attendees: {
|
||||
select: {
|
||||
name: true,
|
||||
email: true,
|
||||
timeZone: true,
|
||||
},
|
||||
try {
|
||||
const bookings = await prisma.booking.findMany({
|
||||
take: 3,
|
||||
where: {
|
||||
userId: validKey.userId,
|
||||
},
|
||||
select: {
|
||||
...bookingMinimalSelect,
|
||||
location: true,
|
||||
attendees: {
|
||||
select: {
|
||||
name: true,
|
||||
email: true,
|
||||
timeZone: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
res.status(201).json(bookings);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return res.status(500).json({ message: "Unable to get bookings." });
|
||||
}
|
||||
res.status(201).json(bookings);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return res.status(500).json({ message: "Unable to get bookings." });
|
||||
}
|
||||
}
|
||||
|
||||
export default defaultHandler({
|
||||
GET: Promise.resolve({ default: defaultResponder(handler) }),
|
||||
});
|
||||
@@ -0,0 +1,83 @@
|
||||
import schedule from "node-schedule";
|
||||
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
export async function scheduleTrigger(
|
||||
booking: { id: number; endTime: Date; scheduledJobs: string[] },
|
||||
subscriberUrl: string,
|
||||
subscriber: { id: string; appId: string | null }
|
||||
) {
|
||||
//schedule job to call subscriber url at the end of meeting
|
||||
const job = schedule.scheduleJob(
|
||||
`${subscriber.appId}_${subscriber.id}`,
|
||||
booking.endTime,
|
||||
async function () {
|
||||
const body = JSON.stringify(booking);
|
||||
await fetch(subscriberUrl, {
|
||||
method: "POST",
|
||||
body,
|
||||
});
|
||||
|
||||
//remove scheduled job from bookings once triggered
|
||||
const updatedScheduledJobs = booking.scheduledJobs.filter((scheduledJob) => {
|
||||
return scheduledJob !== `${subscriber.appId}_${subscriber.id}`;
|
||||
});
|
||||
|
||||
await prisma.booking.update({
|
||||
where: {
|
||||
id: booking.id,
|
||||
},
|
||||
data: {
|
||||
scheduledJobs: updatedScheduledJobs,
|
||||
},
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
//add scheduled job name to booking
|
||||
await prisma.booking.update({
|
||||
where: {
|
||||
id: booking.id,
|
||||
},
|
||||
data: {
|
||||
scheduledJobs: {
|
||||
push: job.name,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function cancelScheduledJobs(
|
||||
booking: { uid: string; scheduledJobs?: string[] },
|
||||
appId?: string | null
|
||||
) {
|
||||
let scheduledJobs = booking.scheduledJobs || [];
|
||||
|
||||
if (booking.scheduledJobs) {
|
||||
booking.scheduledJobs.forEach(async (scheduledJob) => {
|
||||
if (appId) {
|
||||
if (scheduledJob.startsWith(appId)) {
|
||||
if (schedule.scheduledJobs[scheduledJob]) {
|
||||
schedule.scheduledJobs[scheduledJob].cancel();
|
||||
}
|
||||
scheduledJobs = scheduledJobs?.filter((job) => scheduledJob !== job) || [];
|
||||
}
|
||||
} else {
|
||||
//if no specific appId given, delete all scheduled jobs of booking
|
||||
if (schedule.scheduledJobs[scheduledJob]) {
|
||||
schedule.scheduledJobs[scheduledJob].cancel();
|
||||
}
|
||||
scheduledJobs = [];
|
||||
}
|
||||
|
||||
await prisma.booking.update({
|
||||
where: {
|
||||
uid: booking.uid,
|
||||
},
|
||||
data: {
|
||||
scheduledJobs: scheduledJobs,
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -6,9 +6,11 @@
|
||||
"main": "./index.ts",
|
||||
"description": "Workflow automation for everyone. Use the Cal.com Zapier app to trigger your workflows when a booking was created, rescheduled or cancled.",
|
||||
"dependencies": {
|
||||
"@calcom/lib": "*"
|
||||
"@calcom/lib": "*",
|
||||
"node-schedule": "2.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@calcom/types": "*"
|
||||
"@calcom/types": "*",
|
||||
"@types/node-schedule": "^2.1.0"
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
import { WorkflowActions } from ".prisma/client";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
|
||||
const emailReminderTemplate = (
|
||||
|
||||
@@ -7,11 +7,12 @@ export const WEBHOOK_TRIGGER_EVENTS_GROUPED_BY_APP = {
|
||||
WebhookTriggerEvents.BOOKING_CANCELLED,
|
||||
WebhookTriggerEvents.BOOKING_CREATED,
|
||||
WebhookTriggerEvents.BOOKING_RESCHEDULED,
|
||||
] as ["BOOKING_CANCELLED", "BOOKING_CREATED", "BOOKING_RESCHEDULED"],
|
||||
WebhookTriggerEvents.MEETING_ENDED,
|
||||
] as const,
|
||||
routing_forms: [WebhookTriggerEvents.FORM_SUBMITTED] as ["FORM_SUBMITTED"],
|
||||
};
|
||||
|
||||
export const WEBHOOK_TRIGGER_EVENTS = [
|
||||
...WEBHOOK_TRIGGER_EVENTS_GROUPED_BY_APP.core,
|
||||
...WEBHOOK_TRIGGER_EVENTS_GROUPED_BY_APP.routing_forms,
|
||||
] as ["BOOKING_CANCELLED", "BOOKING_CREATED", "BOOKING_RESCHEDULED", "FORM_SUBMITTED"];
|
||||
] as const;
|
||||
@@ -30,6 +30,7 @@ const getWebhooks = async (options: GetSubscriberOptions) => {
|
||||
},
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
subscriberUrl: true,
|
||||
payloadTemplate: true,
|
||||
appId: true,
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
-- AlterEnum
|
||||
ALTER TYPE "WebhookTriggerEvents" ADD VALUE 'MEETING_ENDED';
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Booking" ADD COLUMN "scheduledJobs" TEXT[];
|
||||
@@ -291,6 +291,7 @@ model Booking {
|
||||
recurringEventId String?
|
||||
smsReminderNumber String?
|
||||
workflowReminders WorkflowReminder[]
|
||||
scheduledJobs String[]
|
||||
}
|
||||
|
||||
model Schedule {
|
||||
@@ -388,6 +389,7 @@ enum WebhookTriggerEvents {
|
||||
BOOKING_RESCHEDULED
|
||||
BOOKING_CANCELLED
|
||||
FORM_SUBMITTED
|
||||
MEETING_ENDED
|
||||
}
|
||||
|
||||
model Webhook {
|
||||
|
||||
@@ -6,6 +6,7 @@ import { z } from "zod";
|
||||
import app_RoutingForms from "@calcom/app-store/ee/routing_forms/trpc-router";
|
||||
import stripe, { closePayments } from "@calcom/app-store/stripepayment/lib/server";
|
||||
import getApps, { getLocationOptions } from "@calcom/app-store/utils";
|
||||
import { cancelScheduledJobs } from "@calcom/app-store/zapier/lib/nodeScheduler";
|
||||
import { getCalendarCredentials, getConnectedCalendars } from "@calcom/core/CalendarManager";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { sendCancelledEmails, sendFeedbackEmail } from "@calcom/emails";
|
||||
@@ -1226,6 +1227,32 @@ const loggedInViewerRouter = createProtectedRouter()
|
||||
}
|
||||
}
|
||||
|
||||
// if zapier get disconnected, delete zapier apiKey, delete zapier webhooks and cancel all scheduled jobs from zapier
|
||||
if (credential.app?.slug === "zapier") {
|
||||
await prisma.apiKey.deleteMany({
|
||||
where: {
|
||||
userId: ctx.user.id,
|
||||
appId: "zapier",
|
||||
},
|
||||
});
|
||||
await prisma.webhook.deleteMany({
|
||||
where: {
|
||||
appId: "zapier",
|
||||
},
|
||||
});
|
||||
const bookingsWithScheduledJobs = await prisma.booking.findMany({
|
||||
where: {
|
||||
userId: ctx.user.id,
|
||||
scheduledJobs: {
|
||||
isEmpty: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
for (const booking of bookingsWithScheduledJobs) {
|
||||
cancelScheduledJobs(booking, credential.appId);
|
||||
}
|
||||
}
|
||||
|
||||
// Validated that credential is user's above
|
||||
await prisma.credential.delete({
|
||||
where: {
|
||||
|
||||
@@ -113,6 +113,13 @@ export const apiKeysRouter = createProtectedRouter()
|
||||
}),
|
||||
async resolve({ ctx, input }) {
|
||||
const { id } = input;
|
||||
|
||||
const apiKeyToDelete = await ctx.prisma.apiKey.findFirst({
|
||||
where: {
|
||||
id
|
||||
}
|
||||
})
|
||||
|
||||
await ctx.prisma.user.update({
|
||||
where: {
|
||||
id: ctx.user.id,
|
||||
@@ -125,6 +132,16 @@ export const apiKeysRouter = createProtectedRouter()
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
//remove all existing zapier webhooks, as we always have only one zapier API key and the running zaps won't work any more if this key is deleted
|
||||
if(apiKeyToDelete && apiKeyToDelete.appId === 'zapier'){
|
||||
await ctx.prisma.webhook.deleteMany({
|
||||
where: {
|
||||
appId: 'zapier'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
id,
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user