Skip to content

Commit 984a611

Browse files
authored
Merge pull request #3534 from egeoztass/feat/twilio-sms-notification
feat: add Twilio SMS notification channel
2 parents 247308b + c57614e commit 984a611

16 files changed

Lines changed: 409 additions & 8 deletions

File tree

client/src/Hooks/useNotificationForm.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,16 @@ function buildDefaults(data: Notification | null): NotificationFormData {
6060
address: data.address || "",
6161
};
6262
}
63+
if (data?.type === "twilio") {
64+
return {
65+
type: "twilio",
66+
notificationName: data.notificationName || "",
67+
accountSid: data.accountSid || "",
68+
accessToken: data.accessToken || "",
69+
phone: data.phone || "",
70+
twilioPhoneNumber: data.twilioPhoneNumber || "",
71+
};
72+
}
6373
if (data?.type === "pushover") {
6474
return {
6575
type: "pushover",

client/src/Pages/Notifications/create/index.tsx

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,8 @@ const NotificationsCreatePage = () => {
149149
/>
150150
{watchedType !== "matrix" &&
151151
watchedType !== "telegram" &&
152-
watchedType !== "pushover" && (
152+
watchedType !== "pushover" &&
153+
watchedType !== "twilio" && (
153154
<ConfigBox
154155
title={addressConfig.title}
155156
subtitle={addressConfig.description}
@@ -262,6 +263,88 @@ const NotificationsCreatePage = () => {
262263
}
263264
/>
264265
)}
266+
{watchedType === "twilio" && (
267+
<ConfigBox
268+
title={t("pages.notifications.form.twilio.title")}
269+
subtitle={t("pages.notifications.form.twilio.description")}
270+
rightContent={
271+
<Stack spacing={theme.spacing(8)}>
272+
<Controller
273+
name="accountSid"
274+
control={control}
275+
defaultValue={"accountSid" in defaults ? defaults.accountSid : ""}
276+
render={({ field, fieldState }) => (
277+
<TextField
278+
{...field}
279+
type="text"
280+
fieldLabel={t("pages.notifications.form.twilio.optionAccountSid")}
281+
placeholder={t(
282+
"pages.notifications.form.twilio.placeholderAccountSid"
283+
)}
284+
fullWidth
285+
error={!!fieldState.error}
286+
helperText={fieldState.error?.message ?? ""}
287+
/>
288+
)}
289+
/>
290+
<Controller
291+
name="accessToken"
292+
control={control}
293+
defaultValue={"accessToken" in defaults ? defaults.accessToken : ""}
294+
render={({ field, fieldState }) => (
295+
<TextField
296+
{...field}
297+
type="text"
298+
fieldLabel={t("pages.notifications.form.twilio.optionAuthToken")}
299+
placeholder={t(
300+
"pages.notifications.form.twilio.placeholderAuthToken"
301+
)}
302+
fullWidth
303+
error={!!fieldState.error}
304+
helperText={fieldState.error?.message ?? ""}
305+
/>
306+
)}
307+
/>
308+
<Controller
309+
name="twilioPhoneNumber"
310+
control={control}
311+
defaultValue={
312+
"twilioPhoneNumber" in defaults ? defaults.twilioPhoneNumber : ""
313+
}
314+
render={({ field, fieldState }) => (
315+
<TextField
316+
{...field}
317+
type="text"
318+
fieldLabel={t("pages.notifications.form.twilio.optionFromNumber")}
319+
placeholder={t(
320+
"pages.notifications.form.twilio.placeholderFromNumber"
321+
)}
322+
fullWidth
323+
error={!!fieldState.error}
324+
helperText={fieldState.error?.message ?? ""}
325+
/>
326+
)}
327+
/>
328+
<Controller
329+
name="phone"
330+
control={control}
331+
defaultValue={"phone" in defaults ? defaults.phone : ""}
332+
render={({ field, fieldState }) => (
333+
<TextField
334+
{...field}
335+
type="text"
336+
fieldLabel={t("pages.notifications.form.twilio.optionToNumber")}
337+
placeholder={t("pages.notifications.form.twilio.placeholderToNumber")}
338+
fullWidth
339+
error={!!fieldState.error}
340+
helperText={fieldState.error?.message ?? ""}
341+
/>
342+
)}
343+
/>
344+
</Stack>
345+
}
346+
/>
347+
)}
265348
{watchedType === "matrix" && (
266349
<ConfigBox
267350
title={t("pages.notifications.form.matrix.title")}

client/src/Types/Notification.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export const NotificationChannels = [
88
"teams",
99
"telegram",
1010
"pushover",
11+
"twilio",
1112
] as const;
1213
export type NotificationChannel = (typeof NotificationChannels)[number];
1314

@@ -22,6 +23,8 @@ export interface Notification {
2223
homeserverUrl?: string;
2324
roomId?: string;
2425
accessToken?: string;
26+
accountSid?: string;
27+
twilioPhoneNumber?: string;
2528
createdAt: string;
2629
updatedAt: string;
2730
}

client/src/Validation/notifications.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,14 @@ const pushoverSchema = baseSchema.extend({
6262
accessToken: z.string().min(1, "App token is required"),
6363
});
6464

65+
const twilioSchema = baseSchema.extend({
66+
type: z.literal("twilio"),
67+
accountSid: z.string().min(1, "Account SID is required"),
68+
accessToken: z.string().min(1, "Auth token is required"),
69+
phone: z.string().min(1, "Recipient phone number is required"),
70+
twilioPhoneNumber: z.string().min(1, "Twilio phone number is required"),
71+
});
72+
6573
export const notificationSchema = z.discriminatedUnion("type", [
6674
emailSchema,
6775
slackSchema,
@@ -72,6 +80,7 @@ export const notificationSchema = z.discriminatedUnion("type", [
7280
teamsSchema,
7381
telegramSchema,
7482
pushoverSchema,
83+
twilioSchema,
7584
]);
7685

7786
export type NotificationFormData = z.infer<typeof notificationSchema>;

client/src/locales/en.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -968,6 +968,18 @@
968968
"placeholderAppToken": "azGDORePK8gMaC0QOYAMyEEuzJnyUi",
969969
"optionUserKey": "User key",
970970
"placeholderUserKey": "uQiRzpo4DXghDmr9QzzfQu27cmVRsG"
971+
},
972+
"twilio": {
973+
"title": "Twilio SMS configuration",
974+
"description": "Configure Twilio to send SMS notifications to a phone number.",
975+
"optionAccountSid": "Account SID",
976+
"placeholderAccountSid": "ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
977+
"optionAuthToken": "Auth token",
978+
"placeholderAuthToken": "your_auth_token",
979+
"optionFromNumber": "From number (Twilio)",
980+
"placeholderFromNumber": "+15551234567",
981+
"optionToNumber": "To number (recipient)",
982+
"placeholderToNumber": "+15559876543"
971983
}
972984
},
973985
"table": {

server/src/config/services.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import {
3030
TeamsProvider,
3131
TelegramProvider,
3232
PushoverProvider,
33+
TwilioProvider,
3334
// Interfaces
3435
INetworkService,
3536
IEmailService,
@@ -300,6 +301,7 @@ export const initializeServices = async ({
300301
const teamsProvider = new TeamsProvider(logger);
301302
const telegramProvider = new TelegramProvider(logger);
302303
const pushoverProvider = new PushoverProvider(logger);
304+
const twilioProvider = new TwilioProvider(logger);
303305

304306
const notificationsService = new NotificationsService(
305307
notificationsRepository,
@@ -313,6 +315,7 @@ export const initializeServices = async ({
313315
teamsProvider,
314316
telegramProvider,
315317
pushoverProvider,
318+
twilioProvider,
316319
settingsService,
317320
logger,
318321
notificationMessageBuilder

server/src/db/models/Notification.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ const NotificationSchema = new Schema<NotificationDocument>(
2525
},
2626
type: {
2727
type: String,
28-
enum: ["email", "slack", "discord", "webhook", "pager_duty", "matrix", "teams", "telegram", "pushover"] as NotificationChannel[],
28+
enum: ["email", "slack", "discord", "webhook", "pager_duty", "matrix", "teams", "telegram", "pushover", "twilio"] as NotificationChannel[],
2929
required: true,
3030
},
3131
notificationName: {
@@ -37,6 +37,8 @@ const NotificationSchema = new Schema<NotificationDocument>(
3737
homeserverUrl: { type: String },
3838
roomId: { type: String },
3939
accessToken: { type: String },
40+
accountSid: { type: String },
41+
twilioPhoneNumber: { type: String },
4042
},
4143
{
4244
timestamps: true,

server/src/repositories/notifications/MongoNotificationsRepository.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ class MongoNotificationsRepository implements INotificationsRepository {
3232
homeserverUrl: doc.homeserverUrl ?? undefined,
3333
roomId: doc.roomId ?? undefined,
3434
accessToken: doc.accessToken ?? undefined,
35+
accountSid: doc.accountSid ?? undefined,
36+
twilioPhoneNumber: doc.twilioPhoneNumber ?? undefined,
3537
createdAt: toDateString(doc.createdAt),
3638
updatedAt: toDateString(doc.updatedAt),
3739
};

server/src/repositories/notifications/TimescaleNotificationsRepository.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,20 +14,22 @@ interface NotificationRow {
1414
homeserver_url: string | null;
1515
room_id: string | null;
1616
access_token: string | null;
17+
account_sid: string | null;
18+
twilio_phone_number: string | null;
1719
created_at: Date;
1820
updated_at: Date;
1921
}
2022

2123
const COLUMNS = `id, user_id, team_id, type, notification_name, address, phone,
22-
homeserver_url, room_id, access_token, created_at, updated_at`;
24+
homeserver_url, room_id, access_token, account_sid, twilio_phone_number, created_at, updated_at`;
2325

2426
export class TimescaleNotificationsRepository implements INotificationsRepository {
2527
constructor(private pool: Pool) {}
2628

2729
create = async (data: Partial<Notification>): Promise<Notification> => {
2830
const result = await this.pool.query<NotificationRow>(
29-
`INSERT INTO notifications (user_id, team_id, type, notification_name, address, phone, homeserver_url, room_id, access_token)
30-
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
31+
`INSERT INTO notifications (user_id, team_id, type, notification_name, address, phone, homeserver_url, room_id, access_token, account_sid, twilio_phone_number)
32+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
3133
RETURNING ${COLUMNS}`,
3234
[
3335
data.userId,
@@ -39,6 +41,8 @@ export class TimescaleNotificationsRepository implements INotificationsRepositor
3941
data.homeserverUrl ?? null,
4042
data.roomId ?? null,
4143
data.accessToken ?? null,
44+
data.accountSid ?? null,
45+
data.twilioPhoneNumber ?? null,
4246
]
4347
);
4448
const row = result.rows[0];
@@ -83,6 +87,8 @@ export class TimescaleNotificationsRepository implements INotificationsRepositor
8387
["homeserverUrl", "homeserver_url"],
8488
["roomId", "room_id"],
8589
["accessToken", "access_token"],
90+
["accountSid", "account_sid"],
91+
["twilioPhoneNumber", "twilio_phone_number"],
8692
];
8793

8894
for (const [key, column] of fieldMap) {
@@ -134,6 +140,8 @@ export class TimescaleNotificationsRepository implements INotificationsRepositor
134140
homeserverUrl: row.homeserver_url ?? undefined,
135141
roomId: row.room_id ?? undefined,
136142
accessToken: row.access_token ?? undefined,
143+
accountSid: row.account_sid ?? undefined,
144+
twilioPhoneNumber: row.twilio_phone_number ?? undefined,
137145
createdAt: row.created_at.toISOString(),
138146
updatedAt: row.updated_at.toISOString(),
139147
});

server/src/service/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export * from "@/service/infrastructure/notificationProviders/teams.js";
3131
export * from "@/service/infrastructure/notificationProviders/webhook.js";
3232
export * from "@/service/infrastructure/notificationProviders/telegram.js";
3333
export * from "@/service/infrastructure/notificationProviders/pushover.js";
34+
export * from "@/service/infrastructure/notificationProviders/twilio.js";
3435

3536
// System services
3637
export * from "@/service/system/settingsService.js";

0 commit comments

Comments
 (0)