Skip to content

Commit e9a0914

Browse files
authored
Merge pull request bluewave-labs#3530 from egeoztass/feat/pushover-notification
feat: add Pushover notification channel
2 parents f392bee + 4281e87 commit e9a0914

14 files changed

Lines changed: 343 additions & 26 deletions

File tree

client/src/Hooks/useNotificationForm.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,14 @@ function buildDefaults(data: Notification | null): NotificationFormData {
6060
address: data.address || "",
6161
};
6262
}
63+
if (data?.type === "pushover") {
64+
return {
65+
type: "pushover",
66+
notificationName: data.notificationName || "",
67+
address: data.address || "",
68+
accessToken: data.accessToken || "",
69+
};
70+
}
6371
// Default: email (covers both data === null and data.type === "email")
6472
return {
6573
type: "email",

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

Lines changed: 72 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -147,30 +147,32 @@ const NotificationsCreatePage = () => {
147147
/>
148148
}
149149
/>
150-
{watchedType !== "matrix" && watchedType !== "telegram" && (
151-
<ConfigBox
152-
title={addressConfig.title}
153-
subtitle={addressConfig.description}
154-
rightContent={
155-
<Controller
156-
name="address"
157-
control={control}
158-
defaultValue={"address" in defaults ? defaults.address : ""}
159-
render={({ field, fieldState }) => (
160-
<TextField
161-
{...field}
162-
type="text"
163-
fieldLabel={addressConfig.fieldLabel}
164-
placeholder={addressConfig.placeholder}
165-
fullWidth
166-
error={!!fieldState.error}
167-
helperText={fieldState.error?.message ?? ""}
168-
/>
169-
)}
170-
/>
171-
}
172-
/>
173-
)}
150+
{watchedType !== "matrix" &&
151+
watchedType !== "telegram" &&
152+
watchedType !== "pushover" && (
153+
<ConfigBox
154+
title={addressConfig.title}
155+
subtitle={addressConfig.description}
156+
rightContent={
157+
<Controller
158+
name="address"
159+
control={control}
160+
defaultValue={"address" in defaults ? defaults.address : ""}
161+
render={({ field, fieldState }) => (
162+
<TextField
163+
{...field}
164+
type="text"
165+
fieldLabel={addressConfig.fieldLabel}
166+
placeholder={addressConfig.placeholder}
167+
fullWidth
168+
error={!!fieldState.error}
169+
helperText={fieldState.error?.message ?? ""}
170+
/>
171+
)}
172+
/>
173+
}
174+
/>
175+
)}
174176
{watchedType === "telegram" && (
175177
<ConfigBox
176178
title={t("pages.notifications.form.telegram.title")}
@@ -214,6 +216,52 @@ const NotificationsCreatePage = () => {
214216
}
215217
/>
216218
)}
219+
{watchedType === "pushover" && (
220+
<ConfigBox
221+
title={t("pages.notifications.form.pushover.title")}
222+
subtitle={t("pages.notifications.form.pushover.description")}
223+
rightContent={
224+
<Stack spacing={theme.spacing(8)}>
225+
<Controller
226+
name="accessToken"
227+
control={control}
228+
defaultValue={"accessToken" in defaults ? defaults.accessToken : ""}
229+
render={({ field, fieldState }) => (
230+
<TextField
231+
{...field}
232+
type="text"
233+
fieldLabel={t("pages.notifications.form.pushover.optionAppToken")}
234+
placeholder={t(
235+
"pages.notifications.form.pushover.placeholderAppToken"
236+
)}
237+
fullWidth
238+
error={!!fieldState.error}
239+
helperText={fieldState.error?.message ?? ""}
240+
/>
241+
)}
242+
/>
243+
<Controller
244+
name="address"
245+
control={control}
246+
defaultValue={"address" in defaults ? defaults.address : ""}
247+
render={({ field, fieldState }) => (
248+
<TextField
249+
{...field}
250+
type="text"
251+
fieldLabel={t("pages.notifications.form.pushover.optionUserKey")}
252+
placeholder={t(
253+
"pages.notifications.form.pushover.placeholderUserKey"
254+
)}
255+
fullWidth
256+
error={!!fieldState.error}
257+
helperText={fieldState.error?.message ?? ""}
258+
/>
259+
)}
260+
/>
261+
</Stack>
262+
}
263+
/>
264+
)}
217265
{watchedType === "matrix" && (
218266
<ConfigBox
219267
title={t("pages.notifications.form.matrix.title")}

client/src/Types/Notification.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export const NotificationChannels = [
77
"matrix",
88
"teams",
99
"telegram",
10+
"pushover",
1011
] as const;
1112
export type NotificationChannel = (typeof NotificationChannels)[number];
1213

client/src/Validation/notifications.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,12 @@ const telegramSchema = baseSchema.extend({
5656
accessToken: z.string().min(1, "Bot token is required"),
5757
});
5858

59+
const pushoverSchema = baseSchema.extend({
60+
type: z.literal("pushover"),
61+
address: z.string().min(1, "User key is required"),
62+
accessToken: z.string().min(1, "App token is required"),
63+
});
64+
5965
export const notificationSchema = z.discriminatedUnion("type", [
6066
emailSchema,
6167
slackSchema,
@@ -65,6 +71,7 @@ export const notificationSchema = z.discriminatedUnion("type", [
6571
matrixSchema,
6672
teamsSchema,
6773
telegramSchema,
74+
pushoverSchema,
6875
]);
6976

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

client/src/locales/en.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -959,6 +959,14 @@
959959
"placeholderBotToken": "123456789:ABCdefGhIJKlmNoPQRsTUVwxyZ",
960960
"optionChatId": "Chat ID",
961961
"placeholderChatId": "-1001234567890"
962+
},
963+
"pushover": {
964+
"title": "Pushover configuration",
965+
"description": "Configure Pushover to receive push notifications on your devices.",
966+
"optionAppToken": "Application API token",
967+
"placeholderAppToken": "azGDORePK8gMaC0QOYAMyEEuzJnyUi",
968+
"optionUserKey": "User key",
969+
"placeholderUserKey": "uQiRzpo4DXghDmr9QzzfQu27cmVRsG"
962970
}
963971
},
964972
"table": {

server/src/config/services.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {
2929
MatrixProvider,
3030
TeamsProvider,
3131
TelegramProvider,
32+
PushoverProvider,
3233
// Interfaces
3334
INetworkService,
3435
IEmailService,
@@ -298,6 +299,7 @@ export const initializeServices = async ({
298299
const matrixProvider = new MatrixProvider(logger);
299300
const teamsProvider = new TeamsProvider(logger);
300301
const telegramProvider = new TelegramProvider(logger);
302+
const pushoverProvider = new PushoverProvider(logger);
301303

302304
const notificationsService = new NotificationsService(
303305
notificationsRepository,
@@ -310,6 +312,7 @@ export const initializeServices = async ({
310312
matrixProvider,
311313
teamsProvider,
312314
telegramProvider,
315+
pushoverProvider,
313316
settingsService,
314317
logger,
315318
notificationMessageBuilder

server/src/db/models/Notification.ts

Lines changed: 1 addition & 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"] as NotificationChannel[],
28+
enum: ["email", "slack", "discord", "webhook", "pager_duty", "matrix", "teams", "telegram", "pushover"] as NotificationChannel[],
2929
required: true,
3030
},
3131
notificationName: {

server/src/service/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export * from "@/service/infrastructure/notificationProviders/slack.js";
3030
export * from "@/service/infrastructure/notificationProviders/teams.js";
3131
export * from "@/service/infrastructure/notificationProviders/webhook.js";
3232
export * from "@/service/infrastructure/notificationProviders/telegram.js";
33+
export * from "@/service/infrastructure/notificationProviders/pushover.js";
3334

3435
// System services
3536
export * from "@/service/system/settingsService.js";
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
const SERVICE_NAME = "PushoverProvider";
2+
import type { Notification } from "@/types/index.js";
3+
import { NotificationProvider } from "@/service/infrastructure/notificationProviders/INotificationProvider.js";
4+
import type { NotificationMessage } from "@/types/notificationMessage.js";
5+
import { getTestMessage } from "@/service/infrastructure/notificationProviders/utils.js";
6+
import got from "got";
7+
8+
export class PushoverProvider extends NotificationProvider {
9+
async sendTestAlert(notification: Partial<Notification>): Promise<boolean> {
10+
if (!notification.address || !notification.accessToken) {
11+
return false;
12+
}
13+
14+
try {
15+
await got.post("https://api.pushover.net/1/messages.json", {
16+
form: {
17+
token: notification.accessToken,
18+
user: notification.address,
19+
message: getTestMessage(),
20+
title: "Checkmate Test Notification",
21+
},
22+
...this.gotRequestOptions(),
23+
});
24+
return true;
25+
} catch (error) {
26+
const errMsg = error instanceof Error ? error.message : "unknown error";
27+
const errStack = error instanceof Error ? error.stack : undefined;
28+
this.logger.warn({
29+
message: "Pushover test alert failed",
30+
service: SERVICE_NAME,
31+
method: "sendTestAlert",
32+
stack: errStack,
33+
details: { error: errMsg },
34+
});
35+
return false;
36+
}
37+
}
38+
39+
async sendMessage(notification: Notification, message: NotificationMessage): Promise<boolean> {
40+
if (!notification.address || !notification.accessToken) {
41+
return false;
42+
}
43+
44+
const text = this.buildPushoverText(message);
45+
46+
try {
47+
await got.post("https://api.pushover.net/1/messages.json", {
48+
form: {
49+
token: notification.accessToken,
50+
user: notification.address,
51+
message: text,
52+
title: message.content.title,
53+
},
54+
...this.gotRequestOptions(),
55+
});
56+
57+
this.logger.info({
58+
message: "Pushover notification sent",
59+
service: SERVICE_NAME,
60+
method: "sendMessage",
61+
});
62+
return true;
63+
} catch (error) {
64+
const errMsg = error instanceof Error ? error.message : "unknown error";
65+
const errStack = error instanceof Error ? error.stack : undefined;
66+
this.logger.warn({
67+
message: "Pushover alert failed",
68+
service: SERVICE_NAME,
69+
method: "sendMessage",
70+
stack: errStack,
71+
details: { error: errMsg },
72+
});
73+
return false;
74+
}
75+
}
76+
77+
private buildPushoverText(message: NotificationMessage): string {
78+
const lines: string[] = [];
79+
80+
lines.push(message.content.summary);
81+
lines.push("");
82+
lines.push("Monitor Details:");
83+
lines.push(`• Name: ${message.monitor.name}`);
84+
lines.push(`• URL: ${message.monitor.url}`);
85+
lines.push(`• Type: ${message.monitor.type}`);
86+
lines.push(`• Status: ${message.monitor.status}`);
87+
88+
if (message.content.details && message.content.details.length > 0) {
89+
lines.push("");
90+
lines.push("Additional Information:");
91+
message.content.details.forEach((detail) => lines.push(`• ${detail}`));
92+
}
93+
94+
if (message.content.thresholds && message.content.thresholds.length > 0) {
95+
lines.push("");
96+
lines.push("Threshold Breaches:");
97+
message.content.thresholds.forEach((breach) => {
98+
lines.push(`• ${breach.metric.toUpperCase()}: ${breach.formattedValue} (threshold: ${breach.threshold}${breach.unit})`);
99+
});
100+
}
101+
102+
if (message.content.incident) {
103+
lines.push("");
104+
lines.push(`View Incident: ${message.clientHost}/incidents/${message.monitor.id}`);
105+
}
106+
107+
return lines.join("\n");
108+
}
109+
}

server/src/service/infrastructure/notificationsService.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ export class NotificationsService implements INotificationsService {
3434
private matrixProvider: INotificationProvider;
3535
private teamsProvider: INotificationProvider;
3636
private telegramProvider: INotificationProvider;
37+
private pushoverProvider: INotificationProvider;
3738
private logger: ILogger;
3839
private settingsService: ISettingsService;
3940
private notificationMessageBuilder: INotificationMessageBuilder;
@@ -49,6 +50,7 @@ export class NotificationsService implements INotificationsService {
4950
matrixProvider: INotificationProvider,
5051
teamsProvider: INotificationProvider,
5152
telegramProvider: INotificationProvider,
53+
pushoverProvider: INotificationProvider,
5254
settingsService: ISettingsService,
5355
logger: ILogger,
5456
notificationMessageBuilder: INotificationMessageBuilder
@@ -63,6 +65,7 @@ export class NotificationsService implements INotificationsService {
6365
this.matrixProvider = matrixProvider;
6466
this.teamsProvider = teamsProvider;
6567
this.telegramProvider = telegramProvider;
68+
this.pushoverProvider = pushoverProvider;
6669
this.settingsService = settingsService;
6770
this.logger = logger;
6871
this.notificationMessageBuilder = notificationMessageBuilder;
@@ -102,6 +105,8 @@ export class NotificationsService implements INotificationsService {
102105
return await this.teamsProvider.sendMessage!(notification, notificationMessage);
103106
case "telegram":
104107
return await this.telegramProvider.sendMessage!(notification, notificationMessage);
108+
case "pushover":
109+
return await this.pushoverProvider.sendMessage!(notification, notificationMessage);
105110
default:
106111
this.logger.warn({
107112
message: `Unknown notification type: ${notification.type}`,
@@ -164,6 +169,8 @@ export class NotificationsService implements INotificationsService {
164169
return await this.teamsProvider.sendTestAlert(notification);
165170
case "telegram":
166171
return await this.telegramProvider.sendTestAlert(notification);
172+
case "pushover":
173+
return await this.pushoverProvider.sendTestAlert(notification);
167174
default:
168175
return false;
169176
}

0 commit comments

Comments
 (0)