-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
feat: add Twilio SMS notification channel #3534
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,96 @@ | ||
| const SERVICE_NAME = "TwilioProvider"; | ||
| import type { Notification } from "@/types/index.js"; | ||
| import { NotificationProvider } from "@/service/infrastructure/notificationProviders/INotificationProvider.js"; | ||
| import type { NotificationMessage } from "@/types/notificationMessage.js"; | ||
| import { getTestMessage } from "@/service/infrastructure/notificationProviders/utils.js"; | ||
| import got from "got"; | ||
|
|
||
| export class TwilioProvider extends NotificationProvider { | ||
| async sendTestAlert(notification: Partial<Notification>): Promise<boolean> { | ||
| if (!notification.address || !notification.accessToken || !notification.phone || !notification.homeserverUrl) { | ||
| return false; | ||
| } | ||
|
|
||
| try { | ||
| await got.post(`https://api.twilio.com/2010-04-01/Accounts/${notification.address}/Messages.json`, { | ||
| form: { | ||
| To: notification.phone, | ||
| From: notification.homeserverUrl, | ||
| Body: getTestMessage(), | ||
| }, | ||
| username: notification.address, | ||
| password: notification.accessToken, | ||
| ...this.gotRequestOptions(), | ||
| }); | ||
| return true; | ||
| } catch (error) { | ||
| const errMsg = error instanceof Error ? error.message : "unknown error"; | ||
| const errStack = error instanceof Error ? error.stack : undefined; | ||
| this.logger.warn({ | ||
| message: "Twilio test alert failed", | ||
| service: SERVICE_NAME, | ||
| method: "sendTestAlert", | ||
| stack: errStack, | ||
| details: { error: errMsg }, | ||
| }); | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| async sendMessage(notification: Notification, message: NotificationMessage): Promise<boolean> { | ||
| if (!notification.address || !notification.accessToken || !notification.phone || !notification.homeserverUrl) { | ||
| return false; | ||
| } | ||
|
|
||
| const text = this.buildSmsText(message); | ||
|
|
||
| try { | ||
| await got.post(`https://api.twilio.com/2010-04-01/Accounts/${notification.address}/Messages.json`, { | ||
| form: { | ||
| To: notification.phone, | ||
| From: notification.homeserverUrl, | ||
| Body: text, | ||
| }, | ||
| username: notification.address, | ||
| password: notification.accessToken, | ||
| ...this.gotRequestOptions(), | ||
| }); | ||
|
|
||
| this.logger.info({ | ||
| message: "Twilio SMS notification sent", | ||
| service: SERVICE_NAME, | ||
| method: "sendMessage", | ||
| }); | ||
| return true; | ||
| } catch (error) { | ||
| const errMsg = error instanceof Error ? error.message : "unknown error"; | ||
| const errStack = error instanceof Error ? error.stack : undefined; | ||
| this.logger.warn({ | ||
| message: "Twilio SMS alert failed", | ||
| service: SERVICE_NAME, | ||
| method: "sendMessage", | ||
| stack: errStack, | ||
| details: { error: errMsg }, | ||
| }); | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| private buildSmsText(message: NotificationMessage): string { | ||
| const lines: string[] = []; | ||
|
|
||
| lines.push(message.content.title); | ||
| lines.push(message.content.summary); | ||
| lines.push(""); | ||
| lines.push(`URL: ${message.monitor.url}`); | ||
| lines.push(`Status: ${message.monitor.status}`); | ||
|
|
||
| if (message.content.thresholds && message.content.thresholds.length > 0) { | ||
| message.content.thresholds.forEach((breach) => { | ||
| lines.push(`${breach.metric.toUpperCase()}: ${breach.formattedValue}`); | ||
| }); | ||
| } | ||
|
|
||
| return lines.join("\n"); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -35,6 +35,7 @@ export class NotificationsService implements INotificationsService { | |
| private teamsProvider: INotificationProvider; | ||
| private telegramProvider: INotificationProvider; | ||
| private pushoverProvider: INotificationProvider; | ||
| private twilioProvider: INotificationProvider; | ||
| private logger: ILogger; | ||
| private settingsService: ISettingsService; | ||
| private notificationMessageBuilder: INotificationMessageBuilder; | ||
|
|
@@ -51,6 +52,7 @@ export class NotificationsService implements INotificationsService { | |
| teamsProvider: INotificationProvider, | ||
| telegramProvider: INotificationProvider, | ||
| pushoverProvider: INotificationProvider, | ||
| twilioProvider: INotificationProvider, | ||
| settingsService: ISettingsService, | ||
| logger: ILogger, | ||
| notificationMessageBuilder: INotificationMessageBuilder | ||
|
|
@@ -66,6 +68,7 @@ export class NotificationsService implements INotificationsService { | |
| this.teamsProvider = teamsProvider; | ||
| this.telegramProvider = telegramProvider; | ||
| this.pushoverProvider = pushoverProvider; | ||
| this.twilioProvider = twilioProvider; | ||
| this.settingsService = settingsService; | ||
| this.logger = logger; | ||
| this.notificationMessageBuilder = notificationMessageBuilder; | ||
|
|
@@ -107,6 +110,8 @@ export class NotificationsService implements INotificationsService { | |
| return await this.telegramProvider.sendMessage!(notification, notificationMessage); | ||
| case "pushover": | ||
| return await this.pushoverProvider.sendMessage!(notification, notificationMessage); | ||
| case "twilio": | ||
| return await this.twilioProvider.sendMessage!(notification, notificationMessage); | ||
| default: | ||
| this.logger.warn({ | ||
| message: `Unknown notification type: ${notification.type}`, | ||
|
|
@@ -171,6 +176,8 @@ export class NotificationsService implements INotificationsService { | |
| return await this.telegramProvider.sendTestAlert(notification); | ||
| case "pushover": | ||
| return await this.pushoverProvider.sendTestAlert(notification); | ||
| case "twilio": | ||
| return await this.twilioProvider.sendTestAlert(notification); | ||
|
Comment on lines
+179
to
+180
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also uncovered |
||
| default: | ||
| return false; | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -79,6 +79,15 @@ export const createNotificationBodyValidation = z.discriminatedUnion("type", [ | |
| address: z.string().min(1, "User key is required"), | ||
| accessToken: z.string().min(1, "App token is required"), | ||
| }), | ||
| // Twilio SMS notification | ||
| z.object({ | ||
| notificationName: z.string().min(1, "Notification name is required"), | ||
| type: z.literal("twilio"), | ||
| address: z.string().min(1, "Account SID is required"), | ||
| accessToken: z.string().min(1, "Auth token is required"), | ||
| phone: z.string().min(1, "Recipient phone number is required"), | ||
| homeserverUrl: z.string().min(1, "Twilio phone number is required"), | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Rather than repurpose the We've already got notification type specific fields like Please remember to update repositories/models after making this change. Thanks! |
||
| }), | ||
| ]); | ||
|
|
||
| export const testNotificationBodyValidation = createNotificationBodyValidation; | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These two cases (Pushover and Twiliio) are untested, please add the appropriate tests and check coverage by running
npm run test