Skip to content

Commit 4e8cdfb

Browse files
authored
Merge pull request #3447 from pluisol/feature/pushover-notifications
feat: add Pushover notification provider
2 parents d0ea8b5 + 7db1f3a commit 4e8cdfb

16 files changed

Lines changed: 7708 additions & 13 deletions

File tree

apps/dokploy/components/dashboard/settings/notifications/handle-notifications.tsx

Lines changed: 220 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
GotifyIcon,
1616
LarkIcon,
1717
NtfyIcon,
18+
PushoverIcon,
1819
SlackIcon,
1920
TelegramIcon,
2021
} from "@/components/icons/notification-icons";
@@ -114,6 +115,16 @@ export const notificationSchema = z.discriminatedUnion("type", [
114115
priority: z.number().min(1).max(5).default(3),
115116
})
116117
.merge(notificationBaseSchema),
118+
z
119+
.object({
120+
type: z.literal("pushover"),
121+
userKey: z.string().min(1, { message: "User Key is required" }),
122+
apiToken: z.string().min(1, { message: "API Token is required" }),
123+
priority: z.number().min(-2).max(2).default(0),
124+
retry: z.number().min(30).nullish(),
125+
expire: z.number().min(1).max(10800).nullish(),
126+
})
127+
.merge(notificationBaseSchema),
117128
z
118129
.object({
119130
type: z.literal("custom"),
@@ -166,6 +177,10 @@ export const notificationsMap = {
166177
icon: <NtfyIcon />,
167178
label: "ntfy",
168179
},
180+
pushover: {
181+
icon: <PushoverIcon />,
182+
label: "Pushover",
183+
},
169184
custom: {
170185
icon: <PenBoxIcon size={29} className="text-muted-foreground" />,
171186
label: "Custom",
@@ -209,6 +224,9 @@ export const HandleNotifications = ({ notificationId }: Props) => {
209224
const { mutateAsync: testCustomConnection, isLoading: isLoadingCustom } =
210225
api.notification.testCustomConnection.useMutation();
211226

227+
const { mutateAsync: testPushoverConnection, isLoading: isLoadingPushover } =
228+
api.notification.testPushoverConnection.useMutation();
229+
212230
const customMutation = notificationId
213231
? api.notification.updateCustom.useMutation()
214232
: api.notification.createCustom.useMutation();
@@ -233,6 +251,9 @@ export const HandleNotifications = ({ notificationId }: Props) => {
233251
const larkMutation = notificationId
234252
? api.notification.updateLark.useMutation()
235253
: api.notification.createLark.useMutation();
254+
const pushoverMutation = notificationId
255+
? api.notification.updatePushover.useMutation()
256+
: api.notification.createPushover.useMutation();
236257

237258
const form = useForm<NotificationSchema>({
238259
defaultValues: {
@@ -393,6 +414,23 @@ export const HandleNotifications = ({ notificationId }: Props) => {
393414
dockerCleanup: notification.dockerCleanup,
394415
serverThreshold: notification.serverThreshold,
395416
});
417+
} else if (notification.notificationType === "pushover") {
418+
form.reset({
419+
appBuildError: notification.appBuildError,
420+
appDeploy: notification.appDeploy,
421+
dokployRestart: notification.dokployRestart,
422+
databaseBackup: notification.databaseBackup,
423+
volumeBackup: notification.volumeBackup,
424+
type: notification.notificationType,
425+
userKey: notification.pushover?.userKey,
426+
apiToken: notification.pushover?.apiToken,
427+
priority: notification.pushover?.priority,
428+
retry: notification.pushover?.retry ?? undefined,
429+
expire: notification.pushover?.expire ?? undefined,
430+
name: notification.name,
431+
dockerCleanup: notification.dockerCleanup,
432+
serverThreshold: notification.serverThreshold,
433+
});
396434
}
397435
} else {
398436
form.reset();
@@ -408,6 +446,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
408446
ntfy: ntfyMutation,
409447
lark: larkMutation,
410448
custom: customMutation,
449+
pushover: pushoverMutation,
411450
};
412451

413452
const onSubmit = async (data: NotificationSchema) => {
@@ -559,6 +598,28 @@ export const HandleNotifications = ({ notificationId }: Props) => {
559598
notificationId: notificationId || "",
560599
customId: notification?.customId || "",
561600
});
601+
} else if (data.type === "pushover") {
602+
if (data.priority === 2 && (data.retry == null || data.expire == null)) {
603+
toast.error("Retry and expire are required for emergency priority (2)");
604+
return;
605+
}
606+
promise = pushoverMutation.mutateAsync({
607+
appBuildError: appBuildError,
608+
appDeploy: appDeploy,
609+
dokployRestart: dokployRestart,
610+
databaseBackup: databaseBackup,
611+
volumeBackup: volumeBackup,
612+
userKey: data.userKey,
613+
apiToken: data.apiToken,
614+
priority: data.priority,
615+
retry: data.priority === 2 ? data.retry : undefined,
616+
expire: data.priority === 2 ? data.expire : undefined,
617+
name: data.name,
618+
dockerCleanup: dockerCleanup,
619+
serverThreshold: serverThreshold,
620+
notificationId: notificationId || "",
621+
pushoverId: notification?.pushoverId || "",
622+
});
562623
}
563624

564625
if (promise) {
@@ -1255,6 +1316,147 @@ export const HandleNotifications = ({ notificationId }: Props) => {
12551316
/>
12561317
</>
12571318
)}
1319+
{type === "pushover" && (
1320+
<>
1321+
<FormField
1322+
control={form.control}
1323+
name="userKey"
1324+
render={({ field }) => (
1325+
<FormItem>
1326+
<FormLabel>User Key</FormLabel>
1327+
<FormControl>
1328+
<Input placeholder="ub3de9kl2q..." {...field} />
1329+
</FormControl>
1330+
<FormMessage />
1331+
</FormItem>
1332+
)}
1333+
/>
1334+
<FormField
1335+
control={form.control}
1336+
name="apiToken"
1337+
render={({ field }) => (
1338+
<FormItem>
1339+
<FormLabel>API Token</FormLabel>
1340+
<FormControl>
1341+
<Input placeholder="a3d9k2q7m4..." {...field} />
1342+
</FormControl>
1343+
<FormMessage />
1344+
</FormItem>
1345+
)}
1346+
/>
1347+
<FormField
1348+
control={form.control}
1349+
name="priority"
1350+
defaultValue={0}
1351+
render={({ field }) => (
1352+
<FormItem className="w-full">
1353+
<FormLabel>Priority</FormLabel>
1354+
<FormControl>
1355+
<Input
1356+
placeholder="0"
1357+
value={field.value ?? 0}
1358+
onChange={(e) => {
1359+
const value = e.target.value;
1360+
if (value === "" || value === "-") {
1361+
field.onChange(0);
1362+
} else {
1363+
const priority = Number.parseInt(value);
1364+
if (
1365+
!Number.isNaN(priority) &&
1366+
priority >= -2 &&
1367+
priority <= 2
1368+
) {
1369+
field.onChange(priority);
1370+
}
1371+
}
1372+
}}
1373+
type="number"
1374+
min={-2}
1375+
max={2}
1376+
/>
1377+
</FormControl>
1378+
<FormDescription>
1379+
Message priority (-2 to 2, default: 0, emergency: 2)
1380+
</FormDescription>
1381+
<FormMessage />
1382+
</FormItem>
1383+
)}
1384+
/>
1385+
{form.watch("priority") === 2 && (
1386+
<>
1387+
<FormField
1388+
control={form.control}
1389+
name="retry"
1390+
render={({ field }) => (
1391+
<FormItem className="w-full">
1392+
<FormLabel>Retry (seconds)</FormLabel>
1393+
<FormControl>
1394+
<Input
1395+
placeholder="30"
1396+
{...field}
1397+
value={field.value ?? ""}
1398+
onChange={(e) => {
1399+
const value = e.target.value;
1400+
if (value === "") {
1401+
field.onChange(undefined);
1402+
} else {
1403+
const retry = Number.parseInt(value);
1404+
if (!Number.isNaN(retry)) {
1405+
field.onChange(retry);
1406+
}
1407+
}
1408+
}}
1409+
type="number"
1410+
min={30}
1411+
/>
1412+
</FormControl>
1413+
<FormDescription>
1414+
How often (in seconds) to retry. Minimum 30
1415+
seconds.
1416+
</FormDescription>
1417+
<FormMessage />
1418+
</FormItem>
1419+
)}
1420+
/>
1421+
<FormField
1422+
control={form.control}
1423+
name="expire"
1424+
render={({ field }) => (
1425+
<FormItem className="w-full">
1426+
<FormLabel>Expire (seconds)</FormLabel>
1427+
<FormControl>
1428+
<Input
1429+
placeholder="3600"
1430+
{...field}
1431+
value={field.value ?? ""}
1432+
onChange={(e) => {
1433+
const value = e.target.value;
1434+
if (value === "") {
1435+
field.onChange(undefined);
1436+
} else {
1437+
const expire = Number.parseInt(value);
1438+
if (!Number.isNaN(expire)) {
1439+
field.onChange(expire);
1440+
}
1441+
}
1442+
}}
1443+
type="number"
1444+
min={1}
1445+
max={10800}
1446+
/>
1447+
</FormControl>
1448+
<FormDescription>
1449+
How long to keep retrying (max 10800 seconds / 3
1450+
hours).
1451+
</FormDescription>
1452+
<FormMessage />
1453+
</FormItem>
1454+
)}
1455+
/>
1456+
</>
1457+
)}
1458+
</>
1459+
)}
12581460
</div>
12591461
</div>
12601462
<div className="flex flex-col gap-4">
@@ -1428,7 +1630,8 @@ export const HandleNotifications = ({ notificationId }: Props) => {
14281630
isLoadingGotify ||
14291631
isLoadingNtfy ||
14301632
isLoadingLark ||
1431-
isLoadingCustom
1633+
isLoadingCustom ||
1634+
isLoadingPushover
14321635
}
14331636
variant="secondary"
14341637
type="button"
@@ -1497,6 +1700,22 @@ export const HandleNotifications = ({ notificationId }: Props) => {
14971700
endpoint: data.endpoint,
14981701
headers: headersRecord,
14991702
});
1703+
} else if (data.type === "pushover") {
1704+
if (
1705+
data.priority === 2 &&
1706+
(data.retry == null || data.expire == null)
1707+
) {
1708+
throw new Error(
1709+
"Retry and expire are required for emergency priority (2)",
1710+
);
1711+
}
1712+
await testPushoverConnection({
1713+
userKey: data.userKey,
1714+
apiToken: data.apiToken,
1715+
priority: data.priority,
1716+
retry: data.priority === 2 ? data.retry : undefined,
1717+
expire: data.priority === 2 ? data.expire : undefined,
1718+
});
15001719
}
15011720
toast.success("Connection Success");
15021721
} catch (error) {

apps/dokploy/components/icons/notification-icons.tsx

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,3 +231,29 @@ export const NtfyIcon = ({ className }: Props) => {
231231
</svg>
232232
);
233233
};
234+
235+
export const PushoverIcon = ({ className }: Props) => {
236+
return (
237+
<svg
238+
viewBox="0 0 600 600"
239+
className={cn("size-8", className)}
240+
xmlns="http://www.w3.org/2000/svg"
241+
>
242+
<g stroke="none" strokeWidth="1">
243+
<ellipse
244+
style={{ fillRule: "evenodd" }}
245+
fill="#249DF1"
246+
transform="matrix(-0.674571, 0.73821, -0.73821, -0.674571, 556.833239, 241.613465)"
247+
cx="216.308"
248+
cy="152.076"
249+
rx="296.855"
250+
ry="296.855"
251+
/>
252+
<path
253+
fill="#FFFFFF"
254+
d="M 280.949 172.514 L 355.429 162.714 L 282.909 326.374 L 282.909 326.374 C 295.649 325.394 308.142 321.067 320.389 313.394 L 320.389 313.394 L 320.389 313.394 C 332.642 305.714 343.916 296.077 354.209 284.484 L 354.209 284.484 L 354.209 284.484 C 364.496 272.884 373.396 259.981 380.909 245.774 L 380.909 245.774 L 380.909 245.774 C 388.422 231.561 393.812 217.594 397.079 203.874 L 397.079 203.874 L 397.079 203.874 C 399.039 195.381 399.939 187.214 399.779 179.374 L 399.779 179.374 L 399.779 179.374 C 399.612 171.534 397.569 164.674 393.649 158.794 L 393.649 158.794 L 393.649 158.794 C 389.729 152.914 383.766 148.177 375.759 144.584 L 375.759 144.584 L 375.759 144.584 C 367.759 140.991 356.899 139.194 343.179 139.194 L 343.179 139.194 L 343.179 139.194 C 327.172 139.194 311.409 141.807 295.889 147.034 L 295.889 147.034 L 295.889 147.034 C 280.376 152.261 266.002 159.857 252.769 169.824 L 252.769 169.824 L 252.769 169.824 C 239.542 179.784 228.029 192.197 218.229 207.064 L 218.229 207.064 L 218.229 207.064 C 208.429 221.924 201.406 238.827 197.159 257.774 L 197.159 257.774 L 197.159 257.774 C 195.526 263.981 194.546 268.961 194.219 272.714 L 194.219 272.714 L 194.219 272.714 C 193.892 276.474 193.812 279.577 193.979 282.024 L 193.979 282.024 L 193.979 282.024 C 194.139 284.477 194.462 286.357 194.949 287.664 L 194.949 287.664 L 194.949 287.664 C 195.442 288.971 195.852 290.277 196.179 291.584 L 196.179 291.584 L 196.179 291.584 C 179.519 291.584 167.349 288.234 159.669 281.534 L 159.669 281.534 L 159.669 281.534 C 151.996 274.841 150.119 263.164 154.039 246.504 L 154.039 246.504 L 154.039 246.504 C 157.959 229.191 166.862 212.694 180.749 197.014 L 180.749 197.014 L 180.749 197.014 C 194.629 181.334 211.122 167.531 230.229 155.604 L 230.229 155.604 L 230.229 155.604 C 249.342 143.684 270.249 134.214 292.949 127.194 L 292.949 127.194 L 292.949 127.194 C 315.656 120.167 337.789 116.654 359.349 116.654 L 359.349 116.654 L 359.349 116.654 C 378.296 116.654 394.219 119.347 407.119 124.734 L 407.119 124.734 L 407.119 124.734 C 420.026 130.127 430.072 137.234 437.259 146.054 L 437.259 146.054 L 437.259 146.054 C 444.446 154.874 448.936 165.164 450.729 176.924 L 450.729 176.924 L 450.729 176.924 C 452.529 188.684 451.959 200.934 449.019 213.674 L 449.019 213.674 L 449.019 213.674 C 445.426 229.027 438.646 244.464 428.679 259.984 L 428.679 259.984 L 428.679 259.984 C 418.719 275.497 406.226 289.544 391.199 302.124 L 391.199 302.124 L 391.199 302.124 C 376.172 314.697 358.939 324.904 339.499 332.744 L 339.499 332.744 L 339.499 332.744 C 320.066 340.584 299.406 344.504 277.519 344.504 L 277.519 344.504 L 275.069 344.504 L 212.839 484.154 L 142.279 484.154 L 280.949 172.514 Z"
255+
/>
256+
</g>
257+
</svg>
258+
);
259+
};
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
ALTER TYPE "public"."notificationType" ADD VALUE 'pushover' BEFORE 'custom';--> statement-breakpoint
2+
CREATE TABLE "pushover" (
3+
"pushoverId" text PRIMARY KEY NOT NULL,
4+
"userKey" text NOT NULL,
5+
"apiToken" text NOT NULL,
6+
"priority" integer DEFAULT 0 NOT NULL,
7+
"retry" integer,
8+
"expire" integer
9+
);
10+
--> statement-breakpoint
11+
ALTER TABLE "notification" ADD COLUMN "pushoverId" text;--> statement-breakpoint
12+
ALTER TABLE "notification" ADD CONSTRAINT "notification_pushoverId_pushover_pushoverId_fk" FOREIGN KEY ("pushoverId") REFERENCES "public"."pushover"("pushoverId") ON DELETE cascade ON UPDATE no action;

0 commit comments

Comments
 (0)