feat: add Twilio SMS notification channel
Adds Twilio SMS as a new notification channel, allowing users to receive SMS alerts via the Twilio API. Backend: provider using HTTP Basic Auth, type registration, validation, service wiring. Uses existing fields: address (Account SID), accessToken (Auth Token), homeserverUrl (From number), phone (To number). Frontend: form UI with 4 fields, validation, i18n keys. Tests: 18 provider tests + updated service test constructor. Closes #3095 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
e9a0914fad
commit
9210259511
14 files changed
+381
-3
No files matched your search
@@ -60,6 +60,16 @@ function buildDefaults(data: Notification | null): NotificationFormData {
|
||||
address: data.address || "",
|
||||
};
|
||||
}
|
||||
if (data?.type === "twilio") {
|
||||
return {
|
||||
type: "twilio",
|
||||
notificationName: data.notificationName || "",
|
||||
address: data.address || "",
|
||||
accessToken: data.accessToken || "",
|
||||
phone: data.phone || "",
|
||||
homeserverUrl: data.homeserverUrl || "",
|
||||
};
|
||||
}
|
||||
if (data?.type === "pushover") {
|
||||
return {
|
||||
type: "pushover",
|
||||
|
||||
@@ -149,7 +149,8 @@ const NotificationsCreatePage = () => {
|
||||
/>
|
||||
{watchedType !== "matrix" &&
|
||||
watchedType !== "telegram" &&
|
||||
watchedType !== "pushover" && (
|
||||
watchedType !== "pushover" &&
|
||||
watchedType !== "twilio" && (
|
||||
<ConfigBox
|
||||
title={addressConfig.title}
|
||||
subtitle={addressConfig.description}
|
||||
@@ -262,6 +263,86 @@ const NotificationsCreatePage = () => {
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{watchedType === "twilio" && (
|
||||
<ConfigBox
|
||||
title={t("pages.notifications.form.twilio.title")}
|
||||
subtitle={t("pages.notifications.form.twilio.description")}
|
||||
rightContent={
|
||||
<Stack spacing={theme.spacing(8)}>
|
||||
<Controller
|
||||
name="address"
|
||||
control={control}
|
||||
defaultValue={"address" in defaults ? defaults.address : ""}
|
||||
render={({ field, fieldState }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
type="text"
|
||||
fieldLabel={t("pages.notifications.form.twilio.optionAccountSid")}
|
||||
placeholder={t(
|
||||
"pages.notifications.form.twilio.placeholderAccountSid"
|
||||
)}
|
||||
fullWidth
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message ?? ""}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="accessToken"
|
||||
control={control}
|
||||
defaultValue={"accessToken" in defaults ? defaults.accessToken : ""}
|
||||
render={({ field, fieldState }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
type="text"
|
||||
fieldLabel={t("pages.notifications.form.twilio.optionAuthToken")}
|
||||
placeholder={t(
|
||||
"pages.notifications.form.twilio.placeholderAuthToken"
|
||||
)}
|
||||
fullWidth
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message ?? ""}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="homeserverUrl"
|
||||
control={control}
|
||||
defaultValue={"homeserverUrl" in defaults ? defaults.homeserverUrl : ""}
|
||||
render={({ field, fieldState }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
type="text"
|
||||
fieldLabel={t("pages.notifications.form.twilio.optionFromNumber")}
|
||||
placeholder={t(
|
||||
"pages.notifications.form.twilio.placeholderFromNumber"
|
||||
)}
|
||||
fullWidth
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message ?? ""}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="phone"
|
||||
control={control}
|
||||
defaultValue={"phone" in defaults ? defaults.phone : ""}
|
||||
render={({ field, fieldState }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
type="text"
|
||||
fieldLabel={t("pages.notifications.form.twilio.optionToNumber")}
|
||||
placeholder={t("pages.notifications.form.twilio.placeholderToNumber")}
|
||||
fullWidth
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message ?? ""}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Stack>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{watchedType === "matrix" && (
|
||||
<ConfigBox
|
||||
title={t("pages.notifications.form.matrix.title")}
|
||||
|
||||
@@ -8,6 +8,7 @@ export const NotificationChannels = [
|
||||
"teams",
|
||||
"telegram",
|
||||
"pushover",
|
||||
"twilio",
|
||||
] as const;
|
||||
export type NotificationChannel = (typeof NotificationChannels)[number];
|
||||
|
||||
|
||||
@@ -62,6 +62,14 @@ const pushoverSchema = baseSchema.extend({
|
||||
accessToken: z.string().min(1, "App token is required"),
|
||||
});
|
||||
|
||||
const twilioSchema = baseSchema.extend({
|
||||
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"),
|
||||
});
|
||||
|
||||
export const notificationSchema = z.discriminatedUnion("type", [
|
||||
emailSchema,
|
||||
slackSchema,
|
||||
@@ -72,6 +80,7 @@ export const notificationSchema = z.discriminatedUnion("type", [
|
||||
teamsSchema,
|
||||
telegramSchema,
|
||||
pushoverSchema,
|
||||
twilioSchema,
|
||||
]);
|
||||
|
||||
export type NotificationFormData = z.infer<typeof notificationSchema>;
|
||||
@@ -967,6 +967,18 @@
|
||||
"placeholderAppToken": "azGDORePK8gMaC0QOYAMyEEuzJnyUi",
|
||||
"optionUserKey": "User key",
|
||||
"placeholderUserKey": "uQiRzpo4DXghDmr9QzzfQu27cmVRsG"
|
||||
},
|
||||
"twilio": {
|
||||
"title": "Twilio SMS configuration",
|
||||
"description": "Configure Twilio to send SMS notifications to a phone number.",
|
||||
"optionAccountSid": "Account SID",
|
||||
"placeholderAccountSid": "ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
|
||||
"optionAuthToken": "Auth token",
|
||||
"placeholderAuthToken": "your_auth_token",
|
||||
"optionFromNumber": "From number (Twilio)",
|
||||
"placeholderFromNumber": "+15551234567",
|
||||
"optionToNumber": "To number (recipient)",
|
||||
"placeholderToNumber": "+15559876543"
|
||||
}
|
||||
},
|
||||
"table": {
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
TeamsProvider,
|
||||
TelegramProvider,
|
||||
PushoverProvider,
|
||||
TwilioProvider,
|
||||
// Interfaces
|
||||
INetworkService,
|
||||
IEmailService,
|
||||
@@ -300,6 +301,7 @@ export const initializeServices = async ({
|
||||
const teamsProvider = new TeamsProvider(logger);
|
||||
const telegramProvider = new TelegramProvider(logger);
|
||||
const pushoverProvider = new PushoverProvider(logger);
|
||||
const twilioProvider = new TwilioProvider(logger);
|
||||
|
||||
const notificationsService = new NotificationsService(
|
||||
notificationsRepository,
|
||||
@@ -313,6 +315,7 @@ export const initializeServices = async ({
|
||||
teamsProvider,
|
||||
telegramProvider,
|
||||
pushoverProvider,
|
||||
twilioProvider,
|
||||
settingsService,
|
||||
logger,
|
||||
notificationMessageBuilder
|
||||
|
||||
@@ -25,7 +25,7 @@ const NotificationSchema = new Schema<NotificationDocument>(
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
enum: ["email", "slack", "discord", "webhook", "pager_duty", "matrix", "teams", "telegram", "pushover"] as NotificationChannel[],
|
||||
enum: ["email", "slack", "discord", "webhook", "pager_duty", "matrix", "teams", "telegram", "pushover", "twilio"] as NotificationChannel[],
|
||||
required: true,
|
||||
},
|
||||
notificationName: {
|
||||
|
||||
@@ -31,6 +31,7 @@ export * from "@/service/infrastructure/notificationProviders/teams.js";
|
||||
export * from "@/service/infrastructure/notificationProviders/webhook.js";
|
||||
export * from "@/service/infrastructure/notificationProviders/telegram.js";
|
||||
export * from "@/service/infrastructure/notificationProviders/pushover.js";
|
||||
export * from "@/service/infrastructure/notificationProviders/twilio.js";
|
||||
|
||||
// System services
|
||||
export * from "@/service/system/settingsService.js";
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,15 @@
|
||||
export const NotificationChannels = ["email", "slack", "discord", "webhook", "pager_duty", "matrix", "teams", "telegram", "pushover"] as const;
|
||||
export const NotificationChannels = [
|
||||
"email",
|
||||
"slack",
|
||||
"discord",
|
||||
"webhook",
|
||||
"pager_duty",
|
||||
"matrix",
|
||||
"teams",
|
||||
"telegram",
|
||||
"pushover",
|
||||
"twilio",
|
||||
] as const;
|
||||
export type NotificationChannel = (typeof NotificationChannels)[number];
|
||||
|
||||
export interface Notification {
|
||||
|
||||
@@ -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"),
|
||||
}),
|
||||
]);
|
||||
|
||||
export const testNotificationBodyValidation = createNotificationBodyValidation;
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
import { describe, expect, it, jest, beforeEach } from "@jest/globals";
|
||||
import { createMockLogger } from "../../../helpers/createMockLogger.ts";
|
||||
import { makeNotification, makeMessage, makeMessageWithThresholds, makeMessageWithIncident } from "../../../helpers/notificationMessage.ts";
|
||||
import { testNotificationProviderContract } from "../../../helpers/notificationProviderContract.ts";
|
||||
|
||||
const mockGotPost = jest.fn().mockResolvedValue({});
|
||||
jest.unstable_mockModule("got", () => ({ default: { post: mockGotPost } }));
|
||||
|
||||
const { TwilioProvider } = await import("../../../../src/service/infrastructure/notificationProviders/twilio.ts");
|
||||
|
||||
const createProvider = () => {
|
||||
const logger = createMockLogger();
|
||||
return { provider: new TwilioProvider(logger as any), logger };
|
||||
};
|
||||
|
||||
const makeTwilioNotification = (overrides?: Record<string, unknown>) =>
|
||||
makeNotification({
|
||||
phone: "+15559876543",
|
||||
homeserverUrl: "+15551234567",
|
||||
...overrides,
|
||||
});
|
||||
|
||||
testNotificationProviderContract("TwilioProvider", {
|
||||
create: () => {
|
||||
mockGotPost.mockResolvedValue({});
|
||||
return createProvider().provider;
|
||||
},
|
||||
makeNotification: () => makeTwilioNotification(),
|
||||
});
|
||||
|
||||
describe("TwilioProvider", () => {
|
||||
beforeEach(() => mockGotPost.mockReset().mockResolvedValue({}));
|
||||
|
||||
describe("sendTestAlert", () => {
|
||||
it("sends to Twilio API and returns true", async () => {
|
||||
expect(await createProvider().provider.sendTestAlert(makeTwilioNotification())).toBe(true);
|
||||
expect(mockGotPost).toHaveBeenCalledWith(
|
||||
expect.stringContaining("api.twilio.com/2010-04-01/Accounts/"),
|
||||
expect.objectContaining({
|
||||
form: expect.objectContaining({ To: "+15559876543", From: "+15551234567" }),
|
||||
username: expect.any(String),
|
||||
password: expect.any(String),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("returns false when address is missing", async () => {
|
||||
expect(await createProvider().provider.sendTestAlert(makeTwilioNotification({ address: "" }))).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false when accessToken is missing", async () => {
|
||||
expect(await createProvider().provider.sendTestAlert(makeTwilioNotification({ accessToken: undefined }))).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false when phone is missing", async () => {
|
||||
expect(await createProvider().provider.sendTestAlert(makeTwilioNotification({ phone: "" }))).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false when homeserverUrl (from number) is missing", async () => {
|
||||
expect(await createProvider().provider.sendTestAlert(makeTwilioNotification({ homeserverUrl: "" }))).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false and logs on error", async () => {
|
||||
mockGotPost.mockRejectedValue(new Error("fail"));
|
||||
const { provider, logger } = createProvider();
|
||||
expect(await provider.sendTestAlert(makeTwilioNotification())).toBe(false);
|
||||
expect(logger.warn).toHaveBeenCalledWith(expect.objectContaining({ method: "sendTestAlert", details: { error: "fail" } }));
|
||||
});
|
||||
|
||||
it("handles non-Error thrown values in sendTestAlert", async () => {
|
||||
mockGotPost.mockRejectedValue("string error");
|
||||
const { provider, logger } = createProvider();
|
||||
expect(await provider.sendTestAlert(makeTwilioNotification())).toBe(false);
|
||||
expect(logger.warn).toHaveBeenCalledWith(expect.objectContaining({ stack: undefined, details: { error: "unknown error" } }));
|
||||
});
|
||||
});
|
||||
|
||||
describe("sendMessage", () => {
|
||||
it("sends SMS and returns true", async () => {
|
||||
const { provider } = createProvider();
|
||||
expect(await provider.sendMessage(makeTwilioNotification() as any, makeMessage())).toBe(true);
|
||||
const form = mockGotPost.mock.calls[0][1].form;
|
||||
expect(form.Body).toContain("Monitor Down");
|
||||
expect(form.To).toBe("+15559876543");
|
||||
expect(form.From).toBe("+15551234567");
|
||||
});
|
||||
|
||||
it("returns false when address is missing", async () => {
|
||||
expect(await createProvider().provider.sendMessage(makeTwilioNotification({ address: "" }) as any, makeMessage())).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false when accessToken is missing", async () => {
|
||||
expect(await createProvider().provider.sendMessage(makeTwilioNotification({ accessToken: undefined }) as any, makeMessage())).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false when phone is missing", async () => {
|
||||
expect(await createProvider().provider.sendMessage(makeTwilioNotification({ phone: "" }) as any, makeMessage())).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false and logs on error", async () => {
|
||||
mockGotPost.mockRejectedValue(new Error("fail"));
|
||||
const { provider, logger } = createProvider();
|
||||
expect(await provider.sendMessage(makeTwilioNotification() as any, makeMessage())).toBe(false);
|
||||
expect(logger.warn).toHaveBeenCalledWith(expect.objectContaining({ method: "sendMessage" }));
|
||||
});
|
||||
|
||||
it("handles non-Error thrown values in sendMessage", async () => {
|
||||
mockGotPost.mockRejectedValue(42);
|
||||
const { provider, logger } = createProvider();
|
||||
expect(await provider.sendMessage(makeTwilioNotification() as any, makeMessage())).toBe(false);
|
||||
expect(logger.warn).toHaveBeenCalledWith(expect.objectContaining({ details: { error: "unknown error" } }));
|
||||
});
|
||||
|
||||
it("includes thresholds in text", async () => {
|
||||
const { provider } = createProvider();
|
||||
await provider.sendMessage(makeTwilioNotification() as any, makeMessageWithThresholds());
|
||||
expect(mockGotPost.mock.calls[0][1].form.Body).toContain("CPU");
|
||||
});
|
||||
|
||||
it("includes monitor URL in text", async () => {
|
||||
const { provider } = createProvider();
|
||||
await provider.sendMessage(makeTwilioNotification() as any, makeMessage());
|
||||
expect(mockGotPost.mock.calls[0][1].form.Body).toContain("https://example.com");
|
||||
});
|
||||
|
||||
it("omits thresholds when not present", async () => {
|
||||
const { provider } = createProvider();
|
||||
const msg = makeMessage();
|
||||
msg.content.thresholds = undefined;
|
||||
await provider.sendMessage(makeTwilioNotification() as any, msg);
|
||||
const text = mockGotPost.mock.calls[0][1].form.Body;
|
||||
expect(text).not.toContain("CPU");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -46,6 +46,7 @@ const createService = (overrides?: Record<string, unknown>) => {
|
||||
const teamsProvider = createProvider();
|
||||
const telegramProvider = createProvider();
|
||||
const pushoverProvider = createProvider();
|
||||
const twilioProvider = createProvider();
|
||||
const settingsService = createSettingsService();
|
||||
const notificationMessageBuilder = createMessageBuilder();
|
||||
|
||||
@@ -62,6 +63,7 @@ const createService = (overrides?: Record<string, unknown>) => {
|
||||
teamsProvider,
|
||||
telegramProvider,
|
||||
pushoverProvider,
|
||||
twilioProvider,
|
||||
settingsService,
|
||||
notificationMessageBuilder,
|
||||
...overrides,
|
||||
@@ -79,6 +81,7 @@ const createService = (overrides?: Record<string, unknown>) => {
|
||||
defaults.teamsProvider as any,
|
||||
defaults.telegramProvider as any,
|
||||
defaults.pushoverProvider as any,
|
||||
defaults.twilioProvider as any,
|
||||
defaults.settingsService as any,
|
||||
defaults.logger as any,
|
||||
defaults.notificationMessageBuilder as any
|
||||
|
||||
Reference in new issue
Block a user