Files
calendar/packages/features/ee/workflows/lib/reminders/providers/twilioProvider.ts
T
c9c5493de6 feat: support phone based booking api (#17635)
* chore: save progress

* feat: support phone based booking using API v2

* chore: change name of the variable

* fix: update event types and booking fields for improved validation and new properties

- Updated imports in input and output event types services to use the correct library version.
- Added new `avatarUrl` and `successRedirectUrl` properties to the Swagger documentation for user and booking schemas.
- Enhanced booking fields validation by changing the `required` property to a boolean type.
- Improved descriptions and examples in the OpenAPI specifications for better clarity.
- Refactored booking fields input and output classes to include new properties and ensure proper validation.

* fix: booking with phone number

* test: add event type update test

* test: add test for creating booking

* chore: update unit test

* fix: tests

* fix: add turbo

* chore: remove emial

* refactor: make email optional and move it to service

* fix: test

* chore: bump platform-libraries

* fix: e2e test

---------

Co-authored-by: supalarry <laurisskraucis@gmail.com>
2025-01-08 21:16:10 +05:30

199 lines
5.5 KiB
TypeScript

import TwilioClient from "twilio";
import { v4 as uuidv4 } from "uuid";
import { checkSMSRateLimit } from "@calcom/lib/checkRateLimitAndThrowError";
import logger from "@calcom/lib/logger";
import { setTestSMS } from "@calcom/lib/testSMS";
import prisma from "@calcom/prisma";
import { SMSLockState } from "@calcom/prisma/enums";
const log = logger.getSubLogger({ prefix: ["[twilioProvider]"] });
const testMode = process.env.NEXT_PUBLIC_IS_E2E || process.env.INTEGRATION_TEST_MODE || process.env.IS_E2E;
function createTwilioClient() {
if (process.env.TWILIO_SID && process.env.TWILIO_TOKEN && process.env.TWILIO_MESSAGING_SID) {
return TwilioClient(process.env.TWILIO_SID, process.env.TWILIO_TOKEN);
}
throw new Error("Twilio credentials are missing from the .env file");
}
function getDefaultSender(whatsapp = false) {
let defaultSender = process.env.TWILIO_PHONE_NUMBER;
if (whatsapp) {
defaultSender = `whatsapp:+${process.env.TWILIO_WHATSAPP_PHONE_NUMBER}`;
}
return defaultSender || "";
}
function getSMSNumber(phone: string, whatsapp = false) {
return whatsapp ? `whatsapp:${phone}` : phone;
}
export const sendSMS = async (
phoneNumber: string,
body: string,
sender: string,
userId?: number | null,
teamId?: number | null,
whatsapp = false
) => {
log.silly("sendSMS", JSON.stringify({ phoneNumber, body, sender, userId, teamId }));
const isSMSSendingLocked = await isLockedForSMSSending(userId, teamId);
if (isSMSSendingLocked) {
log.debug(`${teamId ? `Team id ${teamId} ` : `User id ${userId} `} is locked for SMS sending `);
return;
}
if (testMode) {
setTestSMS({
to: getSMSNumber(phoneNumber, whatsapp),
from: whatsapp ? getDefaultSender(whatsapp) : sender ? sender : getDefaultSender(),
message: body,
});
console.log(
"Skipped sending SMS because process.env.NEXT_PUBLIC_IS_E2E or process.env.INTEGRATION_TEST_MODE is set. SMS are available in globalThis.testSMS"
);
return;
}
const twilio = createTwilioClient();
if (!teamId && userId) {
await checkSMSRateLimit({
identifier: `sms:user:${userId}`,
rateLimitingType: "smsMonth",
});
}
const response = await twilio.messages.create({
body: body,
messagingServiceSid: process.env.TWILIO_MESSAGING_SID,
to: getSMSNumber(phoneNumber, whatsapp),
from: whatsapp ? getDefaultSender(whatsapp) : sender ? sender : getDefaultSender(),
});
return response;
};
export const scheduleSMS = async (
phoneNumber: string,
body: string,
scheduledDate: Date,
sender: string,
userId?: number | null,
teamId?: number | null,
whatsapp = false
) => {
const isSMSSendingLocked = await isLockedForSMSSending(userId, teamId);
if (isSMSSendingLocked) {
log.debug(`${teamId ? `Team id ${teamId} ` : `User id ${userId} `} is locked for SMS sending `);
return;
}
if (testMode) {
setTestSMS({
to: getSMSNumber(phoneNumber, whatsapp),
from: whatsapp ? getDefaultSender(whatsapp) : sender ? sender : getDefaultSender(),
message: body,
});
console.log(
"Skipped sending SMS because process.env.NEXT_PUBLIC_IS_E2E or process.env.INTEGRATION_TEST_MODE is set. SMS are available in globalThis.testSMS"
);
return { sid: uuidv4() };
}
const twilio = createTwilioClient();
if (!teamId && userId) {
await checkSMSRateLimit({
identifier: `sms:user:${userId}`,
rateLimitingType: "smsMonth",
});
}
const response = await twilio.messages.create({
body: body,
messagingServiceSid: process.env.TWILIO_MESSAGING_SID,
to: getSMSNumber(phoneNumber, whatsapp),
scheduleType: "fixed",
sendAt: scheduledDate,
from: whatsapp ? getDefaultSender(whatsapp) : sender ? sender : getDefaultSender(),
});
return response;
};
export const cancelSMS = async (referenceId: string) => {
const twilio = createTwilioClient();
await twilio.messages(referenceId).update({ status: "canceled" });
};
export const sendVerificationCode = async (phoneNumber: string) => {
const twilio = createTwilioClient();
if (process.env.TWILIO_VERIFY_SID) {
await twilio.verify
.services(process.env.TWILIO_VERIFY_SID)
.verifications.create({ to: phoneNumber, channel: "sms" });
}
};
export const verifyNumber = async (phoneNumber: string, code: string) => {
const twilio = createTwilioClient();
if (process.env.TWILIO_VERIFY_SID) {
try {
const verification_check = await twilio.verify.v2
.services(process.env.TWILIO_VERIFY_SID)
.verificationChecks.create({ to: phoneNumber, code: code });
return verification_check.status;
} catch (e) {
return "failed";
}
}
};
async function isLockedForSMSSending(userId?: number | null, teamId?: number | null) {
if (teamId) {
const team = await prisma.team.findFirst({
where: {
id: teamId,
},
});
return team?.smsLockState === SMSLockState.LOCKED;
}
if (userId) {
const memberships = await prisma.membership.findMany({
where: {
userId: userId,
},
select: {
team: {
select: {
smsLockState: true,
},
},
},
});
const memberOfLockedTeam = memberships.find(
(membership) => membership.team.smsLockState === SMSLockState.LOCKED
);
if (!!memberOfLockedTeam) {
return true;
}
const user = await prisma.user.findFirst({
where: {
id: userId,
},
});
return user?.smsLockState === SMSLockState.LOCKED;
}
}