Skip to content

Commit

Permalink
feat: New workflow action to send Whatsapp message (calcom#8818)
Browse files Browse the repository at this point in the history
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
  • Loading branch information
3 people committed Jul 11, 2023
1 parent fdef157 commit d58924e
Show file tree
Hide file tree
Showing 33 changed files with 862 additions and 148 deletions.
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ TWILIO_SID=
TWILIO_TOKEN=
TWILIO_MESSAGING_SID=
TWILIO_PHONE_NUMBER=
TWILIO_WHATSAPP_PHONE_NUMBER=
# For NEXT_PUBLIC_SENDER_ID only letters, numbers and spaces are allowed (max. 11 characters)
NEXT_PUBLIC_SENDER_ID=
TWILIO_VERIFY_SID=
Expand Down
23 changes: 23 additions & 0 deletions .github/workflows/cron-scheduleWhatsappReminders.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
name: Cron - scheduleWhatsappReminders

on:
# "Scheduled workflows run on the latest commit on the default or base branch."
# — https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#schedule
schedule:
# Runs “At minute 0, 15, 30, and 45.” (see https://crontab.guru)
- cron: "0,15,30,45 * * * *"
jobs:
cron-scheduleWhatsappReminders:
env:
APP_URL: ${{ secrets.APP_URL }}
CRON_API_KEY: ${{ secrets.CRON_API_KEY }}
runs-on: ubuntu-latest
steps:
- name: cURL request
if: ${{ env.APP_URL && env.CRON_API_KEY }}
run: |
curl ${{ secrets.APP_URL }}/api/cron/workflows/scheduleWhatsappReminders \
-X POST \
-H 'content-type: application/json' \
-H 'authorization: ${{ secrets.CRON_API_KEY }}' \
--fail
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from "@calcom/features/ee/workflows/api/scheduleWhatsappReminders";
8 changes: 6 additions & 2 deletions apps/web/public/static/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -1086,6 +1086,8 @@
"email_attendee_action": "send email to attendees",
"sms_attendee_action": "Send SMS to attendee",
"sms_number_action": "send SMS to a specific number",
"whatsapp_number_action": "send Whatsapp to a specific number",
"whatsapp_attendee_action": "send Whatsapp to attendee",
"workflows": "Workflows",
"new_workflow_btn": "New Workflow",
"add_new_workflow": "Add a new workflow",
Expand Down Expand Up @@ -1147,8 +1149,10 @@
"specific_issue": "Have a specific issue?",
"browse_our_docs": "browse our docs",
"choose_template": "Choose a template",
"reminder": "Reminder",
"custom": "Custom",
"reminder": "Reminder",
"rescheduled": "Rescheduled",
"completed": "Completed",
"reminder_email": "Reminder: {{eventType}} with {{name}} at {{date}}",
"not_triggering_existing_bookings": "Won't trigger for already existing bookings as user will be asked for phone number when booking the event.",
"minute_one": "{{count}} minute",
Expand Down Expand Up @@ -1491,7 +1495,7 @@
"team_url_required": "Must enter a team URL",
"url_taken": "This URL is already taken",
"team_publish": "Publish team",
"number_sms_notifications": "Phone number (SMS notifications)",
"number_text_notifications": "Phone number (Text notifications)",
"attendee_email_variable": "Attendee email",
"attendee_email_info": "The person booking's email",
"kbar_search_placeholder": "Type a command or search...",
Expand Down
1 change: 0 additions & 1 deletion apps/web/public/static/locales/fr/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -1490,7 +1490,6 @@
"team_url_required": "Vous devez saisir un lien d'équipe",
"url_taken": "Ce lien est déjà pris",
"team_publish": "Publier l'équipe",
"number_sms_notifications": "Numéro de téléphone (notifications par SMS)",
"attendee_email_variable": "Adresse e-mail du participant",
"attendee_email_info": "Adresse e-mail du participant",
"kbar_search_placeholder": "Saisissez une commande ou une recherche...",
Expand Down
4 changes: 2 additions & 2 deletions packages/features/bookings/lib/getBookingFields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export const getSmsReminderNumberField = () =>
({
name: SMS_REMINDER_NUMBER_FIELD,
type: "phone",
defaultLabel: "number_sms_notifications",
defaultLabel: "number_text_notifications",
defaultPlaceholder: "enter_phone_number",
editable: "system",
} as const);
Expand Down Expand Up @@ -136,7 +136,7 @@ export const ensureBookingInputsHaveSystemFields = ({
const smsNumberSources = [] as NonNullable<(typeof bookingFields)[number]["sources"]>;
workflows.forEach((workflow) => {
workflow.workflow.steps.forEach((step) => {
if (step.action === "SMS_ATTENDEE") {
if (step.action === "SMS_ATTENDEE" || step.action === "WHATSAPP_ATTENDEE") {
const workflowId = workflow.workflow.id;
smsNumberSources.push(
getSmsReminderNumberSource({
Expand Down
3 changes: 3 additions & 0 deletions packages/features/bookings/lib/handleCancelBooking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventR
import { deleteScheduledEmailReminder } from "@calcom/features/ee/workflows/lib/reminders/emailReminderManager";
import { sendCancelledReminders } from "@calcom/features/ee/workflows/lib/reminders/reminderScheduler";
import { deleteScheduledSMSReminder } from "@calcom/features/ee/workflows/lib/reminders/smsReminderManager";
import { deleteScheduledWhatsappReminder } from "@calcom/features/ee/workflows/lib/reminders/whatsappReminderManager";
import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks";
import type { EventTypeInfo } from "@calcom/features/webhooks/lib/sendPayload";
import sendPayload from "@calcom/features/webhooks/lib/sendPayload";
Expand Down Expand Up @@ -655,6 +656,8 @@ async function handler(req: CustomRequest) {
deleteScheduledEmailReminder(reminder.id, reminder.referenceId);
} else if (reminder.method === WorkflowMethods.SMS) {
deleteScheduledSMSReminder(reminder.id, reminder.referenceId);
} else if (reminder.method === WorkflowMethods.WHATSAPP) {
deleteScheduledWhatsappReminder(reminder.id, reminder.referenceId);
}
});
});
Expand Down
4 changes: 4 additions & 0 deletions packages/features/bookings/lib/handleNewBooking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ import {
import { deleteScheduledEmailReminder } from "@calcom/features/ee/workflows/lib/reminders/emailReminderManager";
import { scheduleWorkflowReminders } from "@calcom/features/ee/workflows/lib/reminders/reminderScheduler";
import { deleteScheduledSMSReminder } from "@calcom/features/ee/workflows/lib/reminders/smsReminderManager";
import { deleteScheduledWhatsappReminder } from "@calcom/features/ee/workflows/lib/reminders/whatsappReminderManager";

import type { GetSubscriberOptions } from "@calcom/features/webhooks/lib/getWebhooks";
import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks";
import { isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib";
Expand Down Expand Up @@ -1976,6 +1978,8 @@ async function handler(
deleteScheduledEmailReminder(reminder.id, reminder.referenceId);
} else if (reminder.method === WorkflowMethods.SMS) {
deleteScheduledSMSReminder(reminder.id, reminder.referenceId);
} else if (reminder.method === WorkflowMethods.WHATSAPP) {
deleteScheduledWhatsappReminder(reminder.id, reminder.referenceId);
}
});
} catch (error) {
Expand Down
111 changes: 111 additions & 0 deletions packages/features/ee/workflows/api/scheduleWhatsappReminders.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/* Schedule any workflow reminder that falls within 7 days for WHATSAPP */
import { WorkflowActions, WorkflowMethods, WorkflowTemplates } from "@prisma/client";
import type { NextApiRequest, NextApiResponse } from "next";

import dayjs from "@calcom/dayjs";
import { defaultHandler } from "@calcom/lib/server";
import prisma from "@calcom/prisma";

import * as twilio from "../lib/reminders/smsProviders/twilioProvider";
import { getWhatsappTemplateFunction } from "../lib/actionHelperFunctions";

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;
}

//delete all scheduled whatsapp reminders where scheduled date is past current date
await prisma.workflowReminder.deleteMany({
where: {
method: WorkflowMethods.WHATSAPP,
scheduledDate: {
lte: dayjs().toISOString(),
},
},
});

//find all unscheduled WHATSAPP reminders
const unscheduledReminders = await prisma.workflowReminder.findMany({
where: {
method: WorkflowMethods.WHATSAPP,
scheduled: false,
scheduledDate: {
lte: dayjs().add(7, "day").toISOString(),
},
},
include: {
workflowStep: true,
booking: {
include: {
eventType: true,
user: true,
attendees: true,
},
},
},
});

if (!unscheduledReminders.length) res.json({ ok: true });

for (const reminder of unscheduledReminders) {
if (!reminder.workflowStep || !reminder.booking) {
continue;
}
try {
const sendTo =
reminder.workflowStep.action === WorkflowActions.WHATSAPP_NUMBER
? reminder.workflowStep.sendTo
: reminder.booking?.smsReminderNumber;

const userName =
reminder.workflowStep.action === WorkflowActions.WHATSAPP_ATTENDEE
? reminder.booking?.attendees[0].name
: "";

const attendeeName =
reminder.workflowStep.action === WorkflowActions.WHATSAPP_ATTENDEE
? reminder.booking?.user?.name
: reminder.booking?.attendees[0].name;

const timeZone =
reminder.workflowStep.action === WorkflowActions.WHATSAPP_ATTENDEE
? reminder.booking?.attendees[0].timeZone
: reminder.booking?.user?.timeZone;

const templateFunction = getWhatsappTemplateFunction(reminder.workflowStep.template)
const message = templateFunction(
false,
reminder.workflowStep.action,
reminder.booking?.startTime.toISOString() || "",
reminder.booking?.eventType?.title || "",
timeZone || "",
attendeeName || "",
userName
);

if (message?.length && message?.length > 0 && sendTo) {
const scheduledSMS = await twilio.scheduleSMS(sendTo, message, reminder.scheduledDate, "", true);

await prisma.workflowReminder.update({
where: {
id: reminder.id,
},
data: {
scheduled: true,
referenceId: scheduledSMS.sid,
},
});
}
} catch (error) {
console.log(`Error scheduling WHATSAPP with error ${error}`);
}
}

res.status(200).json({ message: "WHATSAPP scheduled" });
}

export default defaultHandler({
POST: Promise.resolve({ default: handler }),
});
32 changes: 26 additions & 6 deletions packages/features/ee/workflows/components/AddActionDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@ import { useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { z } from "zod";

import { SENDER_ID } from "@calcom/lib/constants";
import { SENDER_NAME } from "@calcom/lib/constants";
import { SENDER_ID, SENDER_NAME } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { WorkflowActions } from "@calcom/prisma/enums";
import { trpc } from "@calcom/trpc/react";
Expand Down Expand Up @@ -94,6 +93,7 @@ export const AddActionDialog = (props: IAddActionDialog) => {
setIsPhoneNumberNeeded(true);
setIsSenderIdNeeded(true);
setIsEmailAddressNeeded(false);
form.resetField("senderId", { defaultValue: SENDER_ID })
} else if (newValue.value === WorkflowActions.EMAIL_ADDRESS) {
setIsEmailAddressNeeded(true);
setIsSenderIdNeeded(false);
Expand All @@ -102,6 +102,11 @@ export const AddActionDialog = (props: IAddActionDialog) => {
setIsSenderIdNeeded(true);
setIsEmailAddressNeeded(false);
setIsPhoneNumberNeeded(false);
form.resetField("senderId", { defaultValue: SENDER_ID })
} else if (newValue.value === WorkflowActions.WHATSAPP_NUMBER) {
setIsSenderIdNeeded(false);
setIsPhoneNumberNeeded(true);
setIsEmailAddressNeeded(false);
} else {
setIsSenderIdNeeded(false);
setIsEmailAddressNeeded(false);
Expand All @@ -116,6 +121,20 @@ export const AddActionDialog = (props: IAddActionDialog) => {

if (!actionOptions) return null;

const canRequirePhoneNumber = (workflowStep: string) => {
return (
WorkflowActions.SMS_ATTENDEE === workflowStep ||
WorkflowActions.WHATSAPP_ATTENDEE === workflowStep
)
}

const showSender = (action: string) => {
return !isSenderIdNeeded && !(
WorkflowActions.WHATSAPP_NUMBER === action ||
WorkflowActions.WHATSAPP_ATTENDEE === action
)
}

return (
<Dialog open={isOpenDialog} onOpenChange={setIsOpenDialog}>
<DialogContent type="creation" title={t("add_action")}>
Expand Down Expand Up @@ -167,7 +186,7 @@ export const AddActionDialog = (props: IAddActionDialog) => {
{isPhoneNumberNeeded && (
<div className="mt-5 space-y-1">
<Label htmlFor="sendTo">{t("phone_number")}</Label>
<div className="mb-5 mt-1">
<div className="mt-1 mb-5">
<Controller
control={form.control}
name="sendTo"
Expand All @@ -193,7 +212,7 @@ export const AddActionDialog = (props: IAddActionDialog) => {
<EmailField required label={t("email_address")} {...form.register("sendTo")} />
</div>
)}
{isSenderIdNeeded ? (
{isSenderIdNeeded && (
<>
<div className="mt-5">
<div className="flex">
Expand All @@ -208,13 +227,14 @@ export const AddActionDialog = (props: IAddActionDialog) => {
<p className="mt-1 text-xs text-red-500">{t("sender_id_error_message")}</p>
)}
</>
) : (
)}
{showSender(form.getValues('action')) && (
<div className="mt-5">
<Label>{t("sender_name")}</Label>
<Input type="text" placeholder={SENDER_NAME} {...form.register(`senderName`)} />
</div>
)}
{form.getValues("action") === WorkflowActions.SMS_ATTENDEE && (
{canRequirePhoneNumber(form.getValues("action")) && (
<div className="mt-5">
<Controller
name="numberRequired"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,14 +89,12 @@ const WorkflowListItem = (props: ItemProps) => {
sendTo.add(t("organizer"));
break;
case WorkflowActions.EMAIL_ATTENDEE:
sendTo.add(t("attendee_name_variable"));
break;
case WorkflowActions.SMS_ATTENDEE:
case WorkflowActions.WHATSAPP_ATTENDEE:
sendTo.add(t("attendee_name_variable"));
break;
case WorkflowActions.SMS_NUMBER:
sendTo.add(step.sendTo || "");
break;
case WorkflowActions.WHATSAPP_NUMBER:
case WorkflowActions.EMAIL_ADDRESS:
sendTo.add(step.sendTo || "");
break;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import type { MultiSelectCheckboxesOptionType as Option } from "@calcom/ui";
import { Button, Label, MultiSelectCheckboxes, TextField } from "@calcom/ui";
import { ArrowDown, Trash2 } from "@calcom/ui/components/icon";

import { isSMSAction } from "../lib/actionHelperFunctions";
import { isSMSAction, isWhatsappAction } from "../lib/actionHelperFunctions";
import type { FormValues } from "../pages/workflow";
import { AddActionDialog } from "./AddActionDialog";
import { DeleteDialog } from "./DeleteDialog";
Expand Down Expand Up @@ -98,7 +98,7 @@ export default function WorkflowDetailsPage(props: Props) {
workflowId: workflowId,
reminderBody: null,
emailSubject: null,
template: WorkflowTemplates.CUSTOM,
template: isWhatsappAction(action) ? WorkflowTemplates.REMINDER : WorkflowTemplates.CUSTOM,
numberRequired: numberRequired || false,
sender: isSMSAction(action) ? sender || SENDER_ID : SENDER_ID,
senderName: !isSMSAction(action) ? senderName || SENDER_NAME : SENDER_NAME,
Expand Down
Loading

0 comments on commit d58924e

Please sign in to comment.