mirror of
https://github.com/OneUptime/oneuptime.git
synced 2026-01-11 19:56:44 +00:00
302 lines
10 KiB
TypeScript
302 lines
10 KiB
TypeScript
import {
|
|
SMSDefaultCostInCents,
|
|
SMSHighRiskCostInCents,
|
|
getTwilioConfig,
|
|
} from "../Config";
|
|
import { isHighRiskPhoneNumber } from "Common/Types/Call/CallRequest";
|
|
import TwilioConfig from "Common/Types/CallAndSMS/TwilioConfig";
|
|
import BadDataException from "Common/Types/Exception/BadDataException";
|
|
import ObjectID from "Common/Types/ObjectID";
|
|
import Phone from "Common/Types/Phone";
|
|
import SmsStatus from "Common/Types/SmsStatus";
|
|
import Text from "Common/Types/Text";
|
|
import UserNotificationStatus from "Common/Types/UserNotification/UserNotificationStatus";
|
|
import { IsBillingEnabled } from "CommonServer/EnvironmentConfig";
|
|
import NotificationService from "CommonServer/Services/NotificationService";
|
|
import ProjectService from "CommonServer/Services/ProjectService";
|
|
import SmsLogService from "CommonServer/Services/SmsLogService";
|
|
import UserOnCallLogTimelineService from "CommonServer/Services/UserOnCallLogTimelineService";
|
|
import logger from "CommonServer/Utils/Logger";
|
|
import Project from "Model/Models/Project";
|
|
import SmsLog from "Model/Models/SmsLog";
|
|
import Twilio from "twilio";
|
|
import { MessageInstance } from "twilio/lib/rest/api/v2010/account/message";
|
|
|
|
export default class SmsService {
|
|
public static async sendSms(
|
|
to: Phone,
|
|
message: string,
|
|
options: {
|
|
projectId?: ObjectID | undefined; // project id for sms log
|
|
customTwilioConfig?: TwilioConfig | undefined;
|
|
isSensitive?: boolean; // if true, message will not be logged
|
|
userOnCallLogTimelineId?: ObjectID | undefined;
|
|
},
|
|
): Promise<void> {
|
|
let smsError: Error | null = null;
|
|
const smsLog: SmsLog = new SmsLog();
|
|
|
|
try {
|
|
// check number of sms to send for this entire messages to send. Each sms can have 160 characters.
|
|
const smsSegments: number = Math.ceil(message.length / 160);
|
|
|
|
message = Text.trimLines(message);
|
|
|
|
let smsCost: number = 0;
|
|
|
|
const shouldChargeForSMS: boolean =
|
|
IsBillingEnabled && !options.customTwilioConfig;
|
|
|
|
if (shouldChargeForSMS) {
|
|
smsCost = SMSDefaultCostInCents / 100;
|
|
|
|
if (isHighRiskPhoneNumber(to)) {
|
|
smsCost = SMSHighRiskCostInCents / 100;
|
|
}
|
|
}
|
|
|
|
if (smsSegments > 1) {
|
|
smsCost = smsCost * smsSegments;
|
|
}
|
|
|
|
smsLog.toNumber = to;
|
|
|
|
smsLog.smsText =
|
|
options && options.isSensitive
|
|
? "This message is sensitive and is not logged"
|
|
: message;
|
|
smsLog.smsCostInUSDCents = 0;
|
|
|
|
if (options.projectId) {
|
|
smsLog.projectId = options.projectId;
|
|
}
|
|
|
|
const twilioConfig: TwilioConfig | null =
|
|
options.customTwilioConfig || (await getTwilioConfig());
|
|
|
|
if (!twilioConfig) {
|
|
throw new BadDataException("Twilio Config not found");
|
|
}
|
|
|
|
const client: Twilio.Twilio = Twilio(
|
|
twilioConfig.accountSid,
|
|
twilioConfig.authToken,
|
|
);
|
|
|
|
smsLog.fromNumber = twilioConfig.phoneNumber;
|
|
|
|
let project: Project | null = null;
|
|
|
|
// make sure project has enough balance.
|
|
|
|
if (options.projectId) {
|
|
project = await ProjectService.findOneById({
|
|
id: options.projectId,
|
|
select: {
|
|
smsOrCallCurrentBalanceInUSDCents: true,
|
|
enableSmsNotifications: true,
|
|
lowCallAndSMSBalanceNotificationSentToOwners: true,
|
|
name: true,
|
|
notEnabledSmsOrCallNotificationSentToOwners: true,
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
if (!project) {
|
|
smsLog.status = SmsStatus.Error;
|
|
smsLog.statusMessage = `Project ${options.projectId.toString()} not found.`;
|
|
logger.error(smsLog.statusMessage);
|
|
await SmsLogService.create({
|
|
data: smsLog,
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (!project.enableSmsNotifications) {
|
|
smsLog.status = SmsStatus.Error;
|
|
smsLog.statusMessage = `SMS notifications are not enabled for this project. Please enable SMS notifications in Project Settings.`;
|
|
logger.error(smsLog.statusMessage);
|
|
await SmsLogService.create({
|
|
data: smsLog,
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
if (!project.notEnabledSmsOrCallNotificationSentToOwners) {
|
|
await ProjectService.updateOneById({
|
|
data: {
|
|
notEnabledSmsOrCallNotificationSentToOwners: true,
|
|
},
|
|
id: project.id!,
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
await ProjectService.sendEmailToProjectOwners(
|
|
project.id!,
|
|
"SMS notifications not enabled for " + (project.name || ""),
|
|
`We tried to send an SMS to ${to.toString()} with message: <br/> <br/> ${message} <br/> <br/> This SMS was not sent because SMS notifications are not enabled for this project. Please enable SMS notifications in Project Settings.`,
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (shouldChargeForSMS) {
|
|
// check if auto recharge is enabled and current balance is low.
|
|
let updatedBalance: number =
|
|
project.smsOrCallCurrentBalanceInUSDCents!;
|
|
try {
|
|
updatedBalance = await NotificationService.rechargeIfBalanceIsLow(
|
|
project.id!,
|
|
);
|
|
} catch (err) {
|
|
logger.error(err);
|
|
}
|
|
|
|
project.smsOrCallCurrentBalanceInUSDCents = updatedBalance;
|
|
|
|
if (!project.smsOrCallCurrentBalanceInUSDCents) {
|
|
smsLog.status = SmsStatus.LowBalance;
|
|
smsLog.statusMessage = `Project ${options.projectId.toString()} does not have enough SMS balance.`;
|
|
logger.error(smsLog.statusMessage);
|
|
await SmsLogService.create({
|
|
data: smsLog,
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
if (!project.lowCallAndSMSBalanceNotificationSentToOwners) {
|
|
await ProjectService.updateOneById({
|
|
data: {
|
|
lowCallAndSMSBalanceNotificationSentToOwners: true,
|
|
},
|
|
id: project.id!,
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
await ProjectService.sendEmailToProjectOwners(
|
|
project.id!,
|
|
"Low SMS and Call Balance for " + (project.name || ""),
|
|
`We tried to send an SMS to ${to.toString()} with message: <br/> <br/> ${message} <br/>This SMS was not sent because project does not have enough balance to send SMS. Current balance is ${
|
|
(project.smsOrCallCurrentBalanceInUSDCents || 0) / 100
|
|
} USD cents. Required balance to send this SMS should is ${smsCost} USD. Please enable auto recharge or recharge manually.`,
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (project.smsOrCallCurrentBalanceInUSDCents < smsCost * 100) {
|
|
smsLog.status = SmsStatus.LowBalance;
|
|
smsLog.statusMessage = `Project does not have enough balance to send SMS. Current balance is ${
|
|
project.smsOrCallCurrentBalanceInUSDCents / 100
|
|
} USD. Required balance is ${smsCost} USD to send this SMS.`;
|
|
logger.error(smsLog.statusMessage);
|
|
await SmsLogService.create({
|
|
data: smsLog,
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
if (!project.lowCallAndSMSBalanceNotificationSentToOwners) {
|
|
await ProjectService.updateOneById({
|
|
data: {
|
|
lowCallAndSMSBalanceNotificationSentToOwners: true,
|
|
},
|
|
id: project.id!,
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
await ProjectService.sendEmailToProjectOwners(
|
|
project.id!,
|
|
"Low SMS and Call Balance for " + (project.name || ""),
|
|
`We tried to send an SMS to ${to.toString()} with message: <br/> <br/> ${message} <br/> <br/> This SMS was not sent because project does not have enough balance to send SMS. Current balance is ${
|
|
project.smsOrCallCurrentBalanceInUSDCents / 100
|
|
} USD. Required balance is ${smsCost} USD to send this SMS. Please enable auto recharge or recharge manually.`,
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
const twillioMessage: MessageInstance = await client.messages.create({
|
|
body: message,
|
|
to: to.toString(),
|
|
from: twilioConfig.phoneNumber.toString(), // From a valid Twilio number
|
|
});
|
|
|
|
smsLog.status = SmsStatus.Success;
|
|
smsLog.statusMessage = "Message ID: " + twillioMessage.sid;
|
|
|
|
logger.debug("SMS message sent successfully.");
|
|
logger.debug(smsLog.statusMessage);
|
|
|
|
if (shouldChargeForSMS && project) {
|
|
smsLog.smsCostInUSDCents = smsCost * 100;
|
|
|
|
project.smsOrCallCurrentBalanceInUSDCents = Math.floor(
|
|
project.smsOrCallCurrentBalanceInUSDCents! - smsCost * 100,
|
|
);
|
|
|
|
await ProjectService.updateOneById({
|
|
data: {
|
|
smsOrCallCurrentBalanceInUSDCents:
|
|
project.smsOrCallCurrentBalanceInUSDCents,
|
|
notEnabledSmsOrCallNotificationSentToOwners: false, // reset this flag
|
|
},
|
|
id: project.id!,
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
}
|
|
} catch (e: any) {
|
|
smsLog.smsCostInUSDCents = 0;
|
|
smsLog.status = SmsStatus.Error;
|
|
smsLog.statusMessage =
|
|
e && e.message ? e.message.toString() : e.toString();
|
|
|
|
logger.error("SMS message failed to send.");
|
|
logger.error(smsLog.statusMessage);
|
|
|
|
smsError = e;
|
|
}
|
|
|
|
if (options.projectId) {
|
|
await SmsLogService.create({
|
|
data: smsLog,
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
}
|
|
|
|
if (options.userOnCallLogTimelineId) {
|
|
await UserOnCallLogTimelineService.updateOneById({
|
|
data: {
|
|
status:
|
|
smsLog.status === SmsStatus.Success
|
|
? UserNotificationStatus.Sent
|
|
: UserNotificationStatus.Error,
|
|
statusMessage: smsLog.statusMessage!,
|
|
},
|
|
id: options.userOnCallLogTimelineId,
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
}
|
|
|
|
if (smsError) {
|
|
throw smsError;
|
|
}
|
|
}
|
|
}
|