* 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>
199 lines
5.5 KiB
TypeScript
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;
|
|
}
|
|
}
|