2022-07-14 00:10:45 +00:00
|
|
|
/* Schedule any workflow reminder that falls within 72 hours for email */
|
2023-08-29 11:56:26 +00:00
|
|
|
import type { Prisma } from "@prisma/client";
|
2022-07-14 00:10:45 +00:00
|
|
|
import client from "@sendgrid/client";
|
|
|
|
import sgMail from "@sendgrid/mail";
|
2023-08-29 11:56:26 +00:00
|
|
|
import { createEvent } from "ics";
|
|
|
|
import type { DateArray } from "ics";
|
2022-07-14 00:10:45 +00:00
|
|
|
import type { NextApiRequest, NextApiResponse } from "next";
|
2023-08-29 11:56:26 +00:00
|
|
|
import { RRule } from "rrule";
|
|
|
|
import { v4 as uuidv4 } from "uuid";
|
2022-07-14 00:10:45 +00:00
|
|
|
|
|
|
|
import dayjs from "@calcom/dayjs";
|
2023-04-18 10:08:09 +00:00
|
|
|
import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses";
|
2023-08-29 11:56:26 +00:00
|
|
|
import { parseRecurringEvent } from "@calcom/lib";
|
2022-07-14 00:10:45 +00:00
|
|
|
import { defaultHandler } from "@calcom/lib/server";
|
2023-07-19 14:30:37 +00:00
|
|
|
import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat";
|
2022-07-28 19:58:26 +00:00
|
|
|
import prisma from "@calcom/prisma";
|
2023-05-02 11:44:05 +00:00
|
|
|
import { WorkflowActions, WorkflowMethods, WorkflowTemplates } from "@calcom/prisma/enums";
|
2022-12-18 02:04:06 +00:00
|
|
|
import { bookingMetadataSchema } from "@calcom/prisma/zod-utils";
|
2022-07-14 00:10:45 +00:00
|
|
|
|
2023-03-10 14:28:42 +00:00
|
|
|
import type { VariablesType } from "../lib/reminders/templates/customTemplate";
|
|
|
|
import customTemplate from "../lib/reminders/templates/customTemplate";
|
2022-07-28 19:58:26 +00:00
|
|
|
import emailReminderTemplate from "../lib/reminders/templates/emailReminderTemplate";
|
2022-07-14 00:10:45 +00:00
|
|
|
|
|
|
|
const sendgridAPIKey = process.env.SENDGRID_API_KEY as string;
|
|
|
|
const senderEmail = process.env.SENDGRID_EMAIL as string;
|
|
|
|
|
|
|
|
sgMail.setApiKey(sendgridAPIKey);
|
|
|
|
|
2023-08-29 11:56:26 +00:00
|
|
|
type Booking = Prisma.BookingGetPayload<{
|
|
|
|
include: {
|
|
|
|
eventType: true;
|
|
|
|
user: true;
|
|
|
|
attendees: true;
|
|
|
|
};
|
|
|
|
}>;
|
|
|
|
|
|
|
|
function getiCalEventAsString(booking: Booking) {
|
|
|
|
let recurrenceRule: string | undefined = undefined;
|
|
|
|
const recurringEvent = parseRecurringEvent(booking.eventType?.recurringEvent);
|
|
|
|
if (recurringEvent?.count) {
|
|
|
|
recurrenceRule = new RRule(recurringEvent).toString().replace("RRULE:", "");
|
|
|
|
}
|
|
|
|
|
|
|
|
const uid = uuidv4();
|
|
|
|
|
|
|
|
const icsEvent = createEvent({
|
|
|
|
uid,
|
|
|
|
startInputType: "utc",
|
|
|
|
start: dayjs(booking.startTime.toISOString() || "")
|
|
|
|
.utc()
|
|
|
|
.toArray()
|
|
|
|
.slice(0, 6)
|
|
|
|
.map((v, i) => (i === 1 ? v + 1 : v)) as DateArray,
|
|
|
|
duration: {
|
|
|
|
minutes: dayjs(booking.endTime.toISOString() || "").diff(
|
|
|
|
dayjs(booking.startTime.toISOString() || ""),
|
|
|
|
"minute"
|
|
|
|
),
|
|
|
|
},
|
|
|
|
title: booking.eventType?.title || "",
|
|
|
|
description: booking.description || "",
|
|
|
|
location: booking.location || "",
|
|
|
|
organizer: {
|
|
|
|
email: booking.user?.email || "",
|
|
|
|
name: booking.user?.name || "",
|
|
|
|
},
|
|
|
|
attendees: [
|
|
|
|
{
|
|
|
|
name: booking.attendees[0].name,
|
|
|
|
email: booking.attendees[0].email,
|
|
|
|
partstat: "ACCEPTED",
|
|
|
|
role: "REQ-PARTICIPANT",
|
|
|
|
rsvp: true,
|
|
|
|
},
|
|
|
|
],
|
|
|
|
method: "REQUEST",
|
|
|
|
...{ recurrenceRule },
|
|
|
|
status: "CONFIRMED",
|
|
|
|
});
|
|
|
|
|
|
|
|
if (icsEvent.error) {
|
|
|
|
throw icsEvent.error;
|
|
|
|
}
|
|
|
|
|
|
|
|
return icsEvent.value;
|
|
|
|
}
|
|
|
|
|
2022-07-14 00:10:45 +00:00
|
|
|
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
|
|
|
const apiKey = req.headers.authorization || req.query.apiKey;
|
|
|
|
if (process.env.CRON_API_KEY !== apiKey) {
|
|
|
|
res.status(401).json({ message: "Not authenticated" });
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!process.env.SENDGRID_API_KEY || !process.env.SENDGRID_EMAIL) {
|
|
|
|
res.status(405).json({ message: "No SendGrid API key or email" });
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2023-07-21 15:36:39 +00:00
|
|
|
const sandboxMode = process.env.NEXT_PUBLIC_IS_E2E ? true : false;
|
|
|
|
|
2023-04-03 09:25:20 +00:00
|
|
|
//delete batch_ids with already past scheduled date from scheduled_sends
|
|
|
|
const remindersToDelete = await prisma.workflowReminder.findMany({
|
|
|
|
where: {
|
|
|
|
method: WorkflowMethods.EMAIL,
|
|
|
|
cancelled: true,
|
|
|
|
scheduledDate: {
|
|
|
|
lte: dayjs().toISOString(),
|
|
|
|
},
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
|
|
|
for (const reminder of remindersToDelete) {
|
|
|
|
try {
|
|
|
|
await client.request({
|
|
|
|
url: `/v3/user/scheduled_sends/${reminder.referenceId}`,
|
|
|
|
method: "DELETE",
|
|
|
|
});
|
|
|
|
} catch (error) {
|
|
|
|
console.log(`Error deleting batch id from scheduled_sends: ${error}`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-07-14 00:10:45 +00:00
|
|
|
await prisma.workflowReminder.deleteMany({
|
|
|
|
where: {
|
|
|
|
method: WorkflowMethods.EMAIL,
|
|
|
|
scheduledDate: {
|
|
|
|
lte: dayjs().toISOString(),
|
|
|
|
},
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
2023-02-20 17:40:08 +00:00
|
|
|
//cancel reminders for cancelled/rescheduled bookings that are scheduled within the next hour
|
|
|
|
const remindersToCancel = await prisma.workflowReminder.findMany({
|
|
|
|
where: {
|
|
|
|
cancelled: true,
|
2023-04-03 09:25:20 +00:00
|
|
|
scheduled: true, //if it is false then they are already cancelled
|
2023-02-20 17:40:08 +00:00
|
|
|
scheduledDate: {
|
|
|
|
lte: dayjs().add(1, "hour").toISOString(),
|
|
|
|
},
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
2023-04-03 09:25:20 +00:00
|
|
|
for (const reminder of remindersToCancel) {
|
|
|
|
try {
|
2023-02-20 17:40:08 +00:00
|
|
|
await client.request({
|
|
|
|
url: "/v3/user/scheduled_sends",
|
|
|
|
method: "POST",
|
|
|
|
body: {
|
|
|
|
batch_id: reminder.referenceId,
|
|
|
|
status: "cancel",
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
2023-04-03 09:25:20 +00:00
|
|
|
await prisma.workflowReminder.update({
|
2023-02-20 17:40:08 +00:00
|
|
|
where: {
|
|
|
|
id: reminder.id,
|
|
|
|
},
|
2023-04-03 09:25:20 +00:00
|
|
|
data: {
|
|
|
|
scheduled: false, // to know which reminder already got cancelled (to avoid error from cancelling the same reminders again)
|
|
|
|
},
|
2023-02-20 17:40:08 +00:00
|
|
|
});
|
2023-04-03 09:25:20 +00:00
|
|
|
} catch (error) {
|
|
|
|
console.log(`Error cancelling scheduled Emails: ${error}`);
|
2023-02-20 17:40:08 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-07-14 00:10:45 +00:00
|
|
|
//find all unscheduled Email reminders
|
|
|
|
const unscheduledReminders = await prisma.workflowReminder.findMany({
|
|
|
|
where: {
|
|
|
|
method: WorkflowMethods.EMAIL,
|
|
|
|
scheduled: false,
|
2022-12-02 13:43:07 +00:00
|
|
|
scheduledDate: {
|
|
|
|
lte: dayjs().add(72, "hour").toISOString(),
|
|
|
|
},
|
2023-05-19 14:41:44 +00:00
|
|
|
OR: [{ cancelled: false }, { cancelled: null }],
|
2022-07-14 00:10:45 +00:00
|
|
|
},
|
|
|
|
include: {
|
|
|
|
workflowStep: true,
|
|
|
|
booking: {
|
|
|
|
include: {
|
|
|
|
eventType: true,
|
|
|
|
user: true,
|
|
|
|
attendees: true,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
2022-08-01 10:20:21 +00:00
|
|
|
if (!unscheduledReminders.length) {
|
|
|
|
res.status(200).json({ message: "No Emails to schedule" });
|
|
|
|
return;
|
|
|
|
}
|
2022-07-14 00:10:45 +00:00
|
|
|
|
2022-08-01 10:20:21 +00:00
|
|
|
for (const reminder of unscheduledReminders) {
|
2023-03-10 14:28:42 +00:00
|
|
|
if (!reminder.workflowStep || !reminder.booking) {
|
|
|
|
continue;
|
|
|
|
}
|
2022-12-02 13:43:07 +00:00
|
|
|
try {
|
|
|
|
let sendTo;
|
|
|
|
|
|
|
|
switch (reminder.workflowStep.action) {
|
|
|
|
case WorkflowActions.EMAIL_HOST:
|
2023-03-10 14:28:42 +00:00
|
|
|
sendTo = reminder.booking.user?.email;
|
2022-12-02 13:43:07 +00:00
|
|
|
break;
|
|
|
|
case WorkflowActions.EMAIL_ATTENDEE:
|
2023-03-10 14:28:42 +00:00
|
|
|
sendTo = reminder.booking.attendees[0].email;
|
2022-12-02 13:43:07 +00:00
|
|
|
break;
|
|
|
|
case WorkflowActions.EMAIL_ADDRESS:
|
|
|
|
sendTo = reminder.workflowStep.sendTo;
|
|
|
|
}
|
|
|
|
|
|
|
|
const name =
|
|
|
|
reminder.workflowStep.action === WorkflowActions.EMAIL_ATTENDEE
|
2023-03-10 14:28:42 +00:00
|
|
|
? reminder.booking.attendees[0].name
|
|
|
|
: reminder.booking.user?.name;
|
2022-12-02 13:43:07 +00:00
|
|
|
|
|
|
|
const attendeeName =
|
|
|
|
reminder.workflowStep.action === WorkflowActions.EMAIL_ATTENDEE
|
2023-03-10 14:28:42 +00:00
|
|
|
? reminder.booking.user?.name
|
|
|
|
: reminder.booking.attendees[0].name;
|
2022-12-02 13:43:07 +00:00
|
|
|
|
|
|
|
const timeZone =
|
|
|
|
reminder.workflowStep.action === WorkflowActions.EMAIL_ATTENDEE
|
2023-03-10 14:28:42 +00:00
|
|
|
? reminder.booking.attendees[0].timeZone
|
|
|
|
: reminder.booking.user?.timeZone;
|
2022-12-02 13:43:07 +00:00
|
|
|
|
2023-02-08 17:06:25 +00:00
|
|
|
const locale =
|
|
|
|
reminder.workflowStep.action === WorkflowActions.EMAIL_ATTENDEE ||
|
|
|
|
reminder.workflowStep.action === WorkflowActions.SMS_ATTENDEE
|
2023-03-10 14:28:42 +00:00
|
|
|
? reminder.booking.attendees[0].locale
|
|
|
|
: reminder.booking.user?.locale;
|
2023-02-08 17:06:25 +00:00
|
|
|
|
2022-12-02 13:43:07 +00:00
|
|
|
let emailContent = {
|
|
|
|
emailSubject: reminder.workflowStep.emailSubject || "",
|
2023-04-18 10:08:09 +00:00
|
|
|
emailBody: `<body style="white-space: pre-wrap;">${reminder.workflowStep.reminderBody || ""}</body>`,
|
2022-12-02 13:43:07 +00:00
|
|
|
};
|
|
|
|
|
2023-04-18 10:08:09 +00:00
|
|
|
let emailBodyEmpty = false;
|
|
|
|
|
|
|
|
if (reminder.workflowStep.reminderBody) {
|
|
|
|
const { responses } = getCalEventResponses({
|
|
|
|
bookingFields: reminder.booking.eventType?.bookingFields ?? null,
|
|
|
|
booking: reminder.booking,
|
|
|
|
});
|
|
|
|
|
|
|
|
const variables: VariablesType = {
|
|
|
|
eventName: reminder.booking.eventType?.title || "",
|
|
|
|
organizerName: reminder.booking.user?.name || "",
|
|
|
|
attendeeName: reminder.booking.attendees[0].name,
|
|
|
|
attendeeEmail: reminder.booking.attendees[0].email,
|
|
|
|
eventDate: dayjs(reminder.booking.startTime).tz(timeZone),
|
|
|
|
eventEndTime: dayjs(reminder.booking?.endTime).tz(timeZone),
|
|
|
|
timeZone: timeZone,
|
|
|
|
location: reminder.booking.location || "",
|
|
|
|
additionalNotes: reminder.booking.description,
|
|
|
|
responses: responses,
|
|
|
|
meetingUrl: bookingMetadataSchema.parse(reminder.booking.metadata || {})?.videoCallUrl,
|
|
|
|
cancelLink: `/booking/${reminder.booking.uid}?cancel=true`,
|
|
|
|
rescheduleLink: `/${reminder.booking.user?.username}/${reminder.booking.eventType?.slug}?rescheduleUid=${reminder.booking.uid}`,
|
|
|
|
};
|
2023-07-17 23:38:37 +00:00
|
|
|
const emailLocale = locale || "en";
|
2023-04-18 10:08:09 +00:00
|
|
|
const emailSubject = customTemplate(
|
|
|
|
reminder.workflowStep.emailSubject || "",
|
|
|
|
variables,
|
2023-07-17 23:38:37 +00:00
|
|
|
emailLocale,
|
2023-07-19 14:30:37 +00:00
|
|
|
getTimeFormatStringFromUserTimeFormat(reminder.booking.user?.timeFormat),
|
2023-04-18 10:08:09 +00:00
|
|
|
!!reminder.booking.user?.hideBranding
|
|
|
|
).text;
|
|
|
|
emailContent.emailSubject = emailSubject;
|
|
|
|
emailContent.emailBody = customTemplate(
|
|
|
|
reminder.workflowStep.reminderBody || "",
|
|
|
|
variables,
|
2023-07-17 23:38:37 +00:00
|
|
|
emailLocale,
|
2023-07-19 14:30:37 +00:00
|
|
|
getTimeFormatStringFromUserTimeFormat(reminder.booking.user?.timeFormat),
|
2023-04-18 10:08:09 +00:00
|
|
|
!!reminder.booking.user?.hideBranding
|
|
|
|
).html;
|
|
|
|
|
|
|
|
emailBodyEmpty =
|
2023-07-19 14:30:37 +00:00
|
|
|
customTemplate(
|
|
|
|
reminder.workflowStep.reminderBody || "",
|
|
|
|
variables,
|
|
|
|
emailLocale,
|
|
|
|
getTimeFormatStringFromUserTimeFormat(reminder.booking.user?.timeFormat)
|
|
|
|
).text.length === 0;
|
2023-04-18 10:08:09 +00:00
|
|
|
} else if (reminder.workflowStep.template === WorkflowTemplates.REMINDER) {
|
|
|
|
emailContent = emailReminderTemplate(
|
|
|
|
false,
|
|
|
|
reminder.workflowStep.action,
|
2023-07-19 14:30:37 +00:00
|
|
|
getTimeFormatStringFromUserTimeFormat(reminder.booking.user?.timeFormat),
|
2023-04-18 10:08:09 +00:00
|
|
|
reminder.booking.startTime.toISOString() || "",
|
|
|
|
reminder.booking.endTime.toISOString() || "",
|
|
|
|
reminder.booking.eventType?.title || "",
|
|
|
|
timeZone || "",
|
|
|
|
attendeeName || "",
|
|
|
|
name || "",
|
|
|
|
!!reminder.booking.user?.hideBranding
|
|
|
|
);
|
2022-12-02 13:43:07 +00:00
|
|
|
}
|
2023-04-18 10:08:09 +00:00
|
|
|
|
|
|
|
if (emailContent.emailSubject.length > 0 && !emailBodyEmpty && sendTo) {
|
2022-12-02 13:43:07 +00:00
|
|
|
const batchIdResponse = await client.request({
|
|
|
|
url: "/v3/mail/batch",
|
|
|
|
method: "POST",
|
|
|
|
});
|
|
|
|
|
|
|
|
const batchId = batchIdResponse[1].batch_id;
|
|
|
|
|
2022-12-16 22:58:43 +00:00
|
|
|
if (reminder.workflowStep.action !== WorkflowActions.EMAIL_ADDRESS) {
|
|
|
|
await sgMail.send({
|
|
|
|
to: sendTo,
|
2023-01-18 14:32:39 +00:00
|
|
|
from: {
|
|
|
|
email: senderEmail,
|
|
|
|
name: reminder.workflowStep.sender || "Cal.com",
|
|
|
|
},
|
2022-12-16 22:58:43 +00:00
|
|
|
subject: emailContent.emailSubject,
|
2023-04-18 10:08:09 +00:00
|
|
|
html: emailContent.emailBody,
|
2022-12-16 22:58:43 +00:00
|
|
|
batchId: batchId,
|
|
|
|
sendAt: dayjs(reminder.scheduledDate).unix(),
|
2023-03-10 14:28:42 +00:00
|
|
|
replyTo: reminder.booking.user?.email || senderEmail,
|
2023-07-21 15:36:39 +00:00
|
|
|
mailSettings: {
|
|
|
|
sandboxMode: {
|
|
|
|
enable: sandboxMode,
|
|
|
|
},
|
|
|
|
},
|
2023-08-29 11:56:26 +00:00
|
|
|
attachments: reminder.workflowStep.includeCalendarEvent
|
|
|
|
? [
|
|
|
|
{
|
|
|
|
content: Buffer.from(getiCalEventAsString(reminder.booking) || "").toString("base64"),
|
|
|
|
filename: "event.ics",
|
|
|
|
type: "text/calendar; method=REQUEST",
|
|
|
|
disposition: "attachment",
|
|
|
|
contentId: uuidv4(),
|
|
|
|
},
|
|
|
|
]
|
|
|
|
: undefined,
|
2022-12-16 22:58:43 +00:00
|
|
|
});
|
|
|
|
}
|
2022-12-02 13:43:07 +00:00
|
|
|
|
|
|
|
await prisma.workflowReminder.update({
|
|
|
|
where: {
|
|
|
|
id: reminder.id,
|
|
|
|
},
|
|
|
|
data: {
|
|
|
|
scheduled: true,
|
|
|
|
referenceId: batchId,
|
2022-07-21 18:56:20 +00:00
|
|
|
},
|
2022-12-02 13:43:07 +00:00
|
|
|
});
|
2022-07-14 00:10:45 +00:00
|
|
|
}
|
2022-12-02 13:43:07 +00:00
|
|
|
} catch (error) {
|
|
|
|
console.log(`Error scheduling Email with error ${error}`);
|
2022-07-14 00:10:45 +00:00
|
|
|
}
|
2022-08-01 10:20:21 +00:00
|
|
|
}
|
|
|
|
res.status(200).json({ message: "Emails scheduled" });
|
2022-07-14 00:10:45 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
export default defaultHandler({
|
|
|
|
POST: Promise.resolve({ default: handler }),
|
|
|
|
});
|