Merge pull request #3447 from pluisol/feature/pushover-notifications

feat: add Pushover notification provider
This commit is contained in:
Mauricio Siu
2026-01-27 21:16:36 -06:00
committed by GitHub
16 changed files with 7708 additions and 13 deletions

View File

@@ -19,6 +19,7 @@ export const notificationType = pgEnum("notificationType", [
"email",
"gotify",
"ntfy",
"pushover",
"custom",
"lark",
]);
@@ -64,6 +65,9 @@ export const notifications = pgTable("notification", {
larkId: text("larkId").references(() => lark.larkId, {
onDelete: "cascade",
}),
pushoverId: text("pushoverId").references(() => pushover.pushoverId, {
onDelete: "cascade",
}),
organizationId: text("organizationId")
.notNull()
.references(() => organization.id, { onDelete: "cascade" }),
@@ -149,6 +153,18 @@ export const lark = pgTable("lark", {
webhookUrl: text("webhookUrl").notNull(),
});
export const pushover = pgTable("pushover", {
pushoverId: text("pushoverId")
.notNull()
.primaryKey()
.$defaultFn(() => nanoid()),
userKey: text("userKey").notNull(),
apiToken: text("apiToken").notNull(),
priority: integer("priority").notNull().default(0),
retry: integer("retry"),
expire: integer("expire"),
});
export const notificationsRelations = relations(notifications, ({ one }) => ({
slack: one(slack, {
fields: [notifications.slackId],
@@ -182,6 +198,10 @@ export const notificationsRelations = relations(notifications, ({ one }) => ({
fields: [notifications.larkId],
references: [lark.larkId],
}),
pushover: one(pushover, {
fields: [notifications.pushoverId],
references: [pushover.pushoverId],
}),
organization: one(organization, {
fields: [notifications.organizationId],
references: [organization.id],
@@ -439,6 +459,69 @@ export const apiTestLarkConnection = apiCreateLark.pick({
webhookUrl: true,
});
export const apiCreatePushover = notificationsSchema
.pick({
appBuildError: true,
databaseBackup: true,
volumeBackup: true,
dokployRestart: true,
name: true,
appDeploy: true,
dockerCleanup: true,
serverThreshold: true,
})
.extend({
userKey: z.string().min(1),
apiToken: z.string().min(1),
priority: z.number().min(-2).max(2).default(0),
retry: z.number().min(30).nullish(),
expire: z.number().min(1).max(10800).nullish(),
})
.refine(
(data) =>
data.priority !== 2 || (data.retry != null && data.expire != null),
{
message: "Retry and expire are required for emergency priority (2)",
path: ["retry"],
},
);
export const apiUpdatePushover = z.object({
notificationId: z.string().min(1),
pushoverId: z.string().min(1),
organizationId: z.string().optional(),
userKey: z.string().min(1).optional(),
apiToken: z.string().min(1).optional(),
priority: z.number().min(-2).max(2).optional(),
retry: z.number().min(30).nullish(),
expire: z.number().min(1).max(10800).nullish(),
appBuildError: z.boolean().optional(),
databaseBackup: z.boolean().optional(),
volumeBackup: z.boolean().optional(),
dokployRestart: z.boolean().optional(),
name: z.string().optional(),
appDeploy: z.boolean().optional(),
dockerCleanup: z.boolean().optional(),
serverThreshold: z.boolean().optional(),
});
export const apiTestPushoverConnection = z
.object({
userKey: z.string().min(1),
apiToken: z.string().min(1),
priority: z.number().min(-2).max(2),
retry: z.number().min(30).nullish(),
expire: z.number().min(1).max(10800).nullish(),
})
.refine(
(data) =>
data.priority !== 2 || (data.retry != null && data.expire != null),
{
message: "Retry and expire are required for emergency priority (2)",
path: ["retry"],
},
);
export const apiSendTest = notificationsSchema
.extend({
botToken: z.string(),

View File

@@ -6,6 +6,7 @@ import {
type apiCreateGotify,
type apiCreateLark,
type apiCreateNtfy,
type apiCreatePushover,
type apiCreateSlack,
type apiCreateTelegram,
type apiUpdateCustom,
@@ -14,6 +15,7 @@ import {
type apiUpdateGotify,
type apiUpdateLark,
type apiUpdateNtfy,
type apiUpdatePushover,
type apiUpdateSlack,
type apiUpdateTelegram,
custom,
@@ -23,6 +25,7 @@ import {
lark,
notifications,
ntfy,
pushover,
slack,
telegram,
} from "@dokploy/server/db/schema";
@@ -694,6 +697,7 @@ export const findNotificationById = async (notificationId: string) => {
ntfy: true,
custom: true,
lark: true,
pushover: true,
},
});
if (!notification) {
@@ -817,3 +821,99 @@ export const updateNotificationById = async (
return result[0];
};
export const createPushoverNotification = async (
input: typeof apiCreatePushover._type,
organizationId: string,
) => {
await db.transaction(async (tx) => {
const newPushover = await tx
.insert(pushover)
.values({
userKey: input.userKey,
apiToken: input.apiToken,
priority: input.priority,
retry: input.retry,
expire: input.expire,
})
.returning()
.then((value) => value[0]);
if (!newPushover) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error input: Inserting pushover",
});
}
const newDestination = await tx
.insert(notifications)
.values({
pushoverId: newPushover.pushoverId,
name: input.name,
appDeploy: input.appDeploy,
appBuildError: input.appBuildError,
databaseBackup: input.databaseBackup,
volumeBackup: input.volumeBackup,
dokployRestart: input.dokployRestart,
dockerCleanup: input.dockerCleanup,
serverThreshold: input.serverThreshold,
notificationType: "pushover",
organizationId: organizationId,
})
.returning()
.then((value) => value[0]);
if (!newDestination) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error input: Inserting notification",
});
}
return newDestination;
});
};
export const updatePushoverNotification = async (
input: typeof apiUpdatePushover._type,
) => {
await db.transaction(async (tx) => {
const newDestination = await tx
.update(notifications)
.set({
name: input.name,
appDeploy: input.appDeploy,
appBuildError: input.appBuildError,
databaseBackup: input.databaseBackup,
volumeBackup: input.volumeBackup,
dokployRestart: input.dokployRestart,
dockerCleanup: input.dockerCleanup,
organizationId: input.organizationId,
serverThreshold: input.serverThreshold,
})
.where(eq(notifications.notificationId, input.notificationId))
.returning()
.then((value) => value[0]);
if (!newDestination) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error Updating notification",
});
}
await tx
.update(pushover)
.set({
userKey: input.userKey,
apiToken: input.apiToken,
priority: input.priority,
retry: input.retry,
expire: input.expire,
})
.where(eq(pushover.pushoverId, input.pushoverId));
return newDestination;
});
};

View File

@@ -11,6 +11,7 @@ import {
sendGotifyNotification,
sendLarkNotification,
sendNtfyNotification,
sendPushoverNotification,
sendSlackNotification,
sendTelegramNotification,
} from "./utils";
@@ -48,12 +49,22 @@ export const sendBuildErrorNotifications = async ({
ntfy: true,
custom: true,
lark: true,
pushover: true,
},
});
for (const notification of notificationList) {
const { email, discord, telegram, slack, gotify, ntfy, custom, lark } =
notification;
const {
email,
discord,
telegram,
slack,
gotify,
ntfy,
custom,
lark,
pushover,
} = notification;
try {
if (email) {
const template = await renderAsync(
@@ -349,6 +360,14 @@ export const sendBuildErrorNotifications = async ({
},
});
}
if (pushover) {
await sendPushoverNotification(
pushover,
"Build Failed",
`Project: ${projectName}\nApplication: ${applicationName}\nType: ${applicationType}\nDate: ${date.toLocaleString()}\nError: ${errorMessage}`,
);
}
} catch (error) {
console.log(error);
}

View File

@@ -12,6 +12,7 @@ import {
sendGotifyNotification,
sendLarkNotification,
sendNtfyNotification,
sendPushoverNotification,
sendSlackNotification,
sendTelegramNotification,
} from "./utils";
@@ -51,12 +52,22 @@ export const sendBuildSuccessNotifications = async ({
ntfy: true,
custom: true,
lark: true,
pushover: true,
},
});
for (const notification of notificationList) {
const { email, discord, telegram, slack, gotify, ntfy, custom, lark } =
notification;
const {
email,
discord,
telegram,
slack,
gotify,
ntfy,
custom,
lark,
pushover,
} = notification;
try {
if (email) {
const template = await renderAsync(
@@ -363,6 +374,14 @@ export const sendBuildSuccessNotifications = async ({
},
});
}
if (pushover) {
await sendPushoverNotification(
pushover,
"Build Success",
`Project: ${projectName}\nApplication: ${applicationName}\nEnvironment: ${environmentName}\nType: ${applicationType}\nDate: ${date.toLocaleString()}`,
);
}
} catch (error) {
console.log(error);
}

View File

@@ -11,6 +11,7 @@ import {
sendGotifyNotification,
sendLarkNotification,
sendNtfyNotification,
sendPushoverNotification,
sendSlackNotification,
sendTelegramNotification,
} from "./utils";
@@ -48,12 +49,22 @@ export const sendDatabaseBackupNotifications = async ({
ntfy: true,
custom: true,
lark: true,
pushover: true,
},
});
for (const notification of notificationList) {
const { email, discord, telegram, slack, gotify, ntfy, custom, lark } =
notification;
const {
email,
discord,
telegram,
slack,
gotify,
ntfy,
custom,
lark,
pushover,
} = notification;
try {
if (email) {
const template = await renderAsync(
@@ -377,6 +388,14 @@ export const sendDatabaseBackupNotifications = async ({
},
});
}
if (pushover) {
await sendPushoverNotification(
pushover,
`Database Backup ${type === "success" ? "Successful" : "Failed"}`,
`Project: ${projectName}\nApplication: ${applicationName}\nDatabase: ${databaseType}\nDatabase Name: ${databaseName}\nDate: ${date.toLocaleString()}${type === "error" && errorMessage ? `\nError: ${errorMessage}` : ""}`,
);
}
} catch (error) {
console.log(error);
}

View File

@@ -11,6 +11,7 @@ import {
sendGotifyNotification,
sendLarkNotification,
sendNtfyNotification,
sendPushoverNotification,
sendSlackNotification,
sendTelegramNotification,
} from "./utils";
@@ -35,12 +36,22 @@ export const sendDockerCleanupNotifications = async (
ntfy: true,
custom: true,
lark: true,
pushover: true,
},
});
for (const notification of notificationList) {
const { email, discord, telegram, slack, gotify, ntfy, custom, lark } =
notification;
const {
email,
discord,
telegram,
slack,
gotify,
ntfy,
custom,
lark,
pushover,
} = notification;
try {
if (email) {
const template = await renderAsync(
@@ -230,6 +241,14 @@ export const sendDockerCleanupNotifications = async (
},
});
}
if (pushover) {
await sendPushoverNotification(
pushover,
"Docker Cleanup",
`Date: ${date.toLocaleString()}\nMessage: ${message}`,
);
}
} catch (error) {
console.log(error);
}

View File

@@ -11,6 +11,7 @@ import {
sendGotifyNotification,
sendLarkNotification,
sendNtfyNotification,
sendPushoverNotification,
sendSlackNotification,
sendTelegramNotification,
} from "./utils";
@@ -29,12 +30,22 @@ export const sendDokployRestartNotifications = async () => {
ntfy: true,
custom: true,
lark: true,
pushover: true,
},
});
for (const notification of notificationList) {
const { email, discord, telegram, slack, gotify, ntfy, custom, lark } =
notification;
const {
email,
discord,
telegram,
slack,
gotify,
ntfy,
custom,
lark,
pushover,
} = notification;
try {
if (email) {
@@ -219,6 +230,14 @@ export const sendDokployRestartNotifications = async () => {
},
});
}
if (pushover) {
await sendPushoverNotification(
pushover,
"Dokploy Server Restarted",
`Date: ${date.toLocaleString()}`,
);
}
} catch (error) {
console.log(error);
}

View File

@@ -5,6 +5,7 @@ import {
sendCustomNotification,
sendDiscordNotification,
sendLarkNotification,
sendPushoverNotification,
sendSlackNotification,
sendTelegramNotification,
} from "./utils";
@@ -38,6 +39,7 @@ export const sendServerThresholdNotifications = async (
slack: true,
custom: true,
lark: true,
pushover: true,
},
});
@@ -45,7 +47,7 @@ export const sendServerThresholdNotifications = async (
const typeColor = 0xff0000; // Rojo para indicar alerta
for (const notification of notificationList) {
const { discord, telegram, slack, custom, lark } = notification;
const { discord, telegram, slack, custom, lark, pushover } = notification;
if (discord) {
const decorate = (decoration: string, text: string) =>
@@ -266,5 +268,13 @@ export const sendServerThresholdNotifications = async (
},
});
}
if (pushover) {
await sendPushoverNotification(
pushover,
`Server ${payload.Type} Alert`,
`Server: ${payload.ServerName}\nType: ${payload.Type}\nCurrent: ${payload.Value.toFixed(2)}%\nThreshold: ${payload.Threshold.toFixed(2)}%\nMessage: ${payload.Message}\nTime: ${date.toLocaleString()}`,
);
}
}
};

View File

@@ -5,6 +5,7 @@ import type {
gotify,
lark,
ntfy,
pushover,
slack,
telegram,
} from "@dokploy/server/db/schema";
@@ -223,3 +224,33 @@ export const sendLarkNotification = async (
console.log(err);
}
};
export const sendPushoverNotification = async (
connection: typeof pushover.$inferInsert,
title: string,
message: string,
) => {
const formData = new URLSearchParams();
formData.append("token", connection.apiToken);
formData.append("user", connection.userKey);
formData.append("title", title);
formData.append("message", message);
formData.append("priority", connection.priority?.toString() || "0");
// For emergency priority (2), retry and expire are required
if (connection.priority === 2) {
formData.append("retry", connection.retry?.toString() || "30");
formData.append("expire", connection.expire?.toString() || "3600");
}
const response = await fetch("https://api.pushover.net/1/messages.json", {
method: "POST",
body: formData,
});
if (!response.ok) {
throw new Error(
`Failed to send Pushover notification: ${response.statusText}`,
);
}
};

View File

@@ -9,6 +9,7 @@ import {
sendEmailNotification,
sendGotifyNotification,
sendNtfyNotification,
sendPushoverNotification,
sendSlackNotification,
sendTelegramNotification,
} from "./utils";
@@ -53,11 +54,13 @@ export const sendVolumeBackupNotifications = async ({
slack: true,
gotify: true,
ntfy: true,
pushover: true,
},
});
for (const notification of notificationList) {
const { email, discord, telegram, slack, gotify, ntfy } = notification;
const { email, discord, telegram, slack, gotify, ntfy, pushover } =
notification;
if (email) {
const subject = `Volume Backup ${type === "success" ? "Successful" : "Failed"} - ${applicationName}`;
@@ -270,5 +273,13 @@ export const sendVolumeBackupNotifications = async ({
],
});
}
if (pushover) {
await sendPushoverNotification(
pushover,
`Volume Backup ${type === "success" ? "Successful" : "Failed"}`,
`Project: ${projectName}\nApplication: ${applicationName}\nVolume: ${volumeName}\nService Type: ${serviceType}${backupSize ? `\nBackup Size: ${backupSize}` : ""}\nDate: ${date.toLocaleString()}${type === "error" && errorMessage ? `\nError: ${errorMessage}` : ""}`,
);
}
}
};