diff --git a/.env.example b/.env.example index 2906b2f69d..383623bd4d 100644 --- a/.env.example +++ b/.env.example @@ -188,6 +188,10 @@ TWILIO_TOKEN= TWILIO_MESSAGING_SID= TWILIO_PHONE_NUMBER= TWILIO_WHATSAPP_PHONE_NUMBER= +TWILIO_WHATSAPP_REMINDER_CONTENT_SID= +TWILIO_WHATSAPP_CANCELLED_CONTENT_SID= +TWILIO_WHATSAPP_RESCHEDULED_CONTENT_SID= +TWILIO_WHATSAPP_COMPLETED_CONTENT_SID= # For NEXT_PUBLIC_SENDER_ID only letters, numbers and spaces are allowed (max. 11 characters) NEXT_PUBLIC_SENDER_ID= TWILIO_VERIFY_SID= diff --git a/packages/features/ee/workflows/api/scheduleWhatsappReminders.ts b/packages/features/ee/workflows/api/scheduleWhatsappReminders.ts index b6baa24067..7ff20e842b 100644 --- a/packages/features/ee/workflows/api/scheduleWhatsappReminders.ts +++ b/packages/features/ee/workflows/api/scheduleWhatsappReminders.ts @@ -12,6 +12,10 @@ import { getWhatsappTemplateFunction, isAttendeeAction } from "../lib/actionHelp import type { PartialWorkflowReminder } from "../lib/getWorkflowReminders"; import { select } from "../lib/getWorkflowReminders"; import { scheduleSmsOrFallbackEmail } from "../lib/reminders/messageDispatcher"; +import { + getContentSidForTemplate, + getContentVariablesForTemplate, +} from "../lib/reminders/templates/whatsapp/ContentSidMapping"; export async function handler(req: NextRequest) { const apiKey = req.headers.get("authorization") || req.nextUrl.searchParams.get("apiKey"); @@ -74,13 +78,29 @@ export async function handler(req: NextRequest) { ? reminder.booking?.attendees[0].timeZone : reminder.booking?.user?.timeZone; + const startTime = reminder.booking?.startTime.toISOString(); + const locale = reminder.booking.user?.locale || "en"; + const timeFormat = getTimeFormatStringFromUserTimeFormat(reminder.booking.user?.timeFormat); + const templateFunction = getWhatsappTemplateFunction(reminder.workflowStep.template); + const contentSid = getContentSidForTemplate(reminder.workflowStep.template); + const contentVariables = getContentVariablesForTemplate({ + name: userName, + attendeeName: attendeeName || "", + eventName: reminder.booking?.eventType?.title, + eventDate: dayjs(startTime).tz(timeZone).locale(locale).format("YYYY MMM D"), + startTime: dayjs(startTime) + .tz(timeZone) + .locale(locale) + .format(timeFormat || "h:mma"), + timeZone, + }); const message = templateFunction( false, reminder.booking.user?.locale || "en", reminder.workflowStep.action, - getTimeFormatStringFromUserTimeFormat(reminder.booking.user?.timeFormat), - reminder.booking?.startTime.toISOString() || "", + timeFormat, + startTime || "", reminder.booking?.eventType?.title || "", timeZone || "", attendeeName || "", @@ -98,6 +118,8 @@ export async function handler(req: NextRequest) { userId, teamId, isWhatsapp: true, + contentSid, + contentVariables, }, fallbackData: reminder.workflowStep.action && isAttendeeAction(reminder.workflowStep.action) diff --git a/packages/features/ee/workflows/lib/reminders/messageDispatcher.ts b/packages/features/ee/workflows/lib/reminders/messageDispatcher.ts index 9181412ac8..f9b03d20f1 100644 --- a/packages/features/ee/workflows/lib/reminders/messageDispatcher.ts +++ b/packages/features/ee/workflows/lib/reminders/messageDispatcher.ts @@ -18,6 +18,8 @@ export async function sendSmsOrFallbackEmail(props: { userId?: number | null; teamId?: number | null; isWhatsapp?: boolean; + contentSid?: string; + contentVariables?: Record; }; fallbackData?: { email: string; @@ -62,6 +64,8 @@ export async function scheduleSmsOrFallbackEmail(props: { userId?: number | null; teamId?: number | null; isWhatsapp?: boolean; + contentSid?: string; + contentVariables?: Record; }; fallbackData?: { email: string; diff --git a/packages/features/ee/workflows/lib/reminders/providers/twilioProvider.ts b/packages/features/ee/workflows/lib/reminders/providers/twilioProvider.ts index 671d81d5a7..e82fe74eba 100644 --- a/packages/features/ee/workflows/lib/reminders/providers/twilioProvider.ts +++ b/packages/features/ee/workflows/lib/reminders/providers/twilioProvider.ts @@ -40,6 +40,8 @@ export const sendSMS = async ({ userId, teamId, isWhatsapp = false, + contentSid, + contentVariables, }: { phoneNumber: string; body: string; @@ -48,8 +50,10 @@ export const sendSMS = async ({ userId?: number | null; teamId?: number | null; isWhatsapp?: boolean; + contentSid?: string; + contentVariables?: Record; }) => { - log.silly("sendSMS", JSON.stringify({ phoneNumber, body, sender, userId, teamId })); + log.silly("sendSMS", JSON.stringify({ phoneNumber, body, sender, userId, teamId, contentSid })); const isSMSSendingLocked = await isLockedForSMSSending(userId, teamId); @@ -80,15 +84,32 @@ export const sendSMS = async ({ }); } - const response = await twilio.messages.create({ - body: body, - messagingServiceSid: process.env.TWILIO_MESSAGING_SID, - to: getSMSNumber(phoneNumber, isWhatsapp), - from: isWhatsapp ? getDefaultSender(isWhatsapp) : sender || getDefaultSender(), - statusCallback: getStatusCallbackUrl(userId, teamId, bookingUid), - }); + if (isWhatsapp) { + const messageOptions: any = { + contentSid: contentSid, + to: getSMSNumber(phoneNumber, isWhatsapp), + from: getDefaultSender(isWhatsapp), + statusCallback: getStatusCallbackUrl(userId, teamId, bookingUid), + messagingServiceSid: process.env.TWILIO_MESSAGING_SID, + }; - return response; + if (contentVariables) { + messageOptions.contentVariables = JSON.stringify(contentVariables); + } + + const response = await twilio.messages.create(messageOptions); + return response; + } else { + const response = await twilio.messages.create({ + body: body, + messagingServiceSid: process.env.TWILIO_MESSAGING_SID, + to: getSMSNumber(phoneNumber), + from: sender || getDefaultSender(), + statusCallback: getStatusCallbackUrl(userId, teamId, bookingUid), + }); + + return response; + } }; const getStatusCallbackUrl = (userId?: number | null, teamId?: number | null, bookingUid?: string | null) => { @@ -108,6 +129,8 @@ export const scheduleSMS = async ({ userId, teamId, isWhatsapp = false, + contentSid, + contentVariables, }: { phoneNumber: string; body: string; @@ -117,6 +140,8 @@ export const scheduleSMS = async ({ userId?: number | null; teamId?: number | null; isWhatsapp?: boolean; + contentSid?: string; + contentVariables?: Record; }) => { const isSMSSendingLocked = await isLockedForSMSSending(userId, teamId); @@ -145,17 +170,37 @@ export const scheduleSMS = async ({ rateLimitingType: "smsMonth", }); } - const response = await twilio.messages.create({ - body, - messagingServiceSid: process.env.TWILIO_MESSAGING_SID, - to: getSMSNumber(phoneNumber, isWhatsapp), - scheduleType: "fixed", - sendAt: scheduledDate, - from: isWhatsapp ? getDefaultSender(isWhatsapp) : sender || getDefaultSender(), - statusCallback: getStatusCallbackUrl(userId, teamId, bookingUid), - }); - return response; + if (isWhatsapp) { + const messageOptions: any = { + contentSid: contentSid, + to: getSMSNumber(phoneNumber, isWhatsapp), + scheduleType: "fixed", + sendAt: scheduledDate, + from: getDefaultSender(isWhatsapp), + statusCallback: getStatusCallbackUrl(userId, teamId, bookingUid), + messagingServiceSid: process.env.TWILIO_MESSAGING_SID, + }; + + if (contentVariables) { + messageOptions.contentVariables = JSON.stringify(contentVariables); + } + + const response = await twilio.messages.create(messageOptions); + return response; + } else { + const response = await twilio.messages.create({ + body, + messagingServiceSid: process.env.TWILIO_MESSAGING_SID, + to: getSMSNumber(phoneNumber), + scheduleType: "fixed", + sendAt: scheduledDate, + from: sender || getDefaultSender(), + statusCallback: getStatusCallbackUrl(userId, teamId, bookingUid), + }); + + return response; + } }; export const cancelSMS = async (referenceId: string) => { diff --git a/packages/features/ee/workflows/lib/reminders/templates/whatsapp/ContentSidMapping.ts b/packages/features/ee/workflows/lib/reminders/templates/whatsapp/ContentSidMapping.ts new file mode 100644 index 0000000000..be3319cfc0 --- /dev/null +++ b/packages/features/ee/workflows/lib/reminders/templates/whatsapp/ContentSidMapping.ts @@ -0,0 +1,40 @@ +import { WorkflowTemplates } from "@calcom/prisma/enums"; + +export const getContentSidForTemplate = (template?: WorkflowTemplates): string | undefined => { + if (!template) { + return process.env.TWILIO_WHATSAPP_REMINDER_CONTENT_SID; + } + switch (template) { + case WorkflowTemplates.REMINDER: + return process.env.TWILIO_WHATSAPP_REMINDER_CONTENT_SID; + case WorkflowTemplates.CANCELLED: + return process.env.TWILIO_WHATSAPP_CANCELLED_CONTENT_SID; + case WorkflowTemplates.RESCHEDULED: + return process.env.TWILIO_WHATSAPP_RESCHEDULED_CONTENT_SID; + case WorkflowTemplates.COMPLETED: + return process.env.TWILIO_WHATSAPP_COMPLETED_CONTENT_SID; + default: + return process.env.TWILIO_WHATSAPP_REMINDER_CONTENT_SID; + } +}; + +export const getContentVariablesForTemplate = (variables: { + name?: string; + attendeeName?: string; + eventName?: string; + eventDate?: string; + startTime?: string; + timeZone?: string; +}): Record => { + const { name, attendeeName, eventName, eventDate, startTime, timeZone } = variables; + + const commonVariables: Record = { + "1": name || "", + "2": attendeeName || "", + "3": eventName || "", + "4": eventDate || "", + "5": `${startTime ?? ""} ${timeZone ?? ""}`, + }; + + return commonVariables; +}; diff --git a/packages/features/ee/workflows/lib/reminders/whatsappReminderManager.ts b/packages/features/ee/workflows/lib/reminders/whatsappReminderManager.ts index a9da906b3c..9ce9d4203e 100644 --- a/packages/features/ee/workflows/lib/reminders/whatsappReminderManager.ts +++ b/packages/features/ee/workflows/lib/reminders/whatsappReminderManager.ts @@ -10,6 +10,10 @@ import { } from "@calcom/prisma/enums"; import { isAttendeeAction } from "../actionHelperFunctions"; +import { + getContentSidForTemplate, + getContentVariablesForTemplate, +} from "../reminders/templates/whatsapp/ContentSidMapping"; import { scheduleSmsOrFallbackEmail, sendSmsOrFallbackEmail } from "./messageDispatcher"; import type { ScheduleTextReminderArgs, timeUnitLowerCase } from "./smsReminderManager"; import { deleteScheduledSMSReminder } from "./smsReminderManager"; @@ -76,7 +80,21 @@ export const scheduleWhatsappReminder = async (args: ScheduleTextReminderArgs) = action === WorkflowActions.WHATSAPP_ATTENDEE ? evt.organizer.name : evt.attendees[0].name; const timeZone = action === WorkflowActions.WHATSAPP_ATTENDEE ? evt.attendees[0].timeZone : evt.organizer.timeZone; + const locale = evt.organizer.language.locale; + const timeFormat = evt.organizer.timeFormat; + const contentSid = getContentSidForTemplate(template); + const contentVariables = getContentVariablesForTemplate({ + name, + attendeeName, + eventName: evt.title, + eventDate: dayjs(startTime).tz(timeZone).locale(locale).format("YYYY MMM D"), + startTime: dayjs(startTime) + .tz(timeZone) + .locale(locale) + .format(timeFormat || "h:mma"), + timeZone, + }); let textMessage = message; switch (template) { @@ -84,9 +102,9 @@ export const scheduleWhatsappReminder = async (args: ScheduleTextReminderArgs) = textMessage = whatsappReminderTemplate( false, - evt.organizer.language.locale, + locale, action, - evt.organizer.timeFormat, + timeFormat, evt.startTime, evt.title, timeZone, @@ -98,9 +116,9 @@ export const scheduleWhatsappReminder = async (args: ScheduleTextReminderArgs) = textMessage = whatsappEventCancelledTemplate( false, - evt.organizer.language.locale, + locale, action, - evt.organizer.timeFormat, + timeFormat, evt.startTime, evt.title, timeZone, @@ -112,9 +130,9 @@ export const scheduleWhatsappReminder = async (args: ScheduleTextReminderArgs) = textMessage = whatsappEventRescheduledTemplate( false, - evt.organizer.language.locale, + locale, action, - evt.organizer.timeFormat, + timeFormat, evt.startTime, evt.title, timeZone, @@ -126,9 +144,9 @@ export const scheduleWhatsappReminder = async (args: ScheduleTextReminderArgs) = textMessage = whatsappEventCompletedTemplate( false, - evt.organizer.language.locale, + locale, action, - evt.organizer.timeFormat, + timeFormat, evt.startTime, evt.title, timeZone, @@ -140,9 +158,9 @@ export const scheduleWhatsappReminder = async (args: ScheduleTextReminderArgs) = textMessage = whatsappReminderTemplate( false, - evt.organizer.language.locale, + locale, action, - evt.organizer.timeFormat, + timeFormat, evt.startTime, evt.title, timeZone, @@ -170,6 +188,8 @@ export const scheduleWhatsappReminder = async (args: ScheduleTextReminderArgs) = userId, teamId, isWhatsapp: true, + contentSid, + contentVariables, }, fallbackData: isAttendeeAction(action) ? { @@ -203,6 +223,8 @@ export const scheduleWhatsappReminder = async (args: ScheduleTextReminderArgs) = userId, teamId, isWhatsapp: true, + contentSid, + contentVariables, }, fallbackData: isAttendeeAction(action) ? { diff --git a/turbo.json b/turbo.json index 3bb7798a38..a85d493246 100644 --- a/turbo.json +++ b/turbo.json @@ -418,6 +418,10 @@ "TWILIO_SID", "TWILIO_TOKEN", "TWILIO_VERIFY_SID", + "TWILIO_WHATSAPP_REMINDER_CONTENT_SID", + "TWILIO_WHATSAPP_CANCELLED_CONTENT_SID", + "TWILIO_WHATSAPP_RESCHEDULED_CONTENT_SID", + "TWILIO_WHATSAPP_COMPLETED_CONTENT_SID", "UPSTASH_REDIS_REST_TOKEN", "UPSTASH_REDIS_REST_URL", "UNKEY_ROOT_KEY",