feat(notifications): native support for pushover

This commit is contained in:
Nicolas Meienberger
2025-11-22 17:48:19 +01:00
parent a622b5e689
commit 8c4939af4e
6 changed files with 130 additions and 1 deletions

View File

@@ -64,6 +64,12 @@ const defaultValuesForType = {
topic: "",
priority: "default" as const,
},
pushover: {
type: "pushover" as const,
userKey: "",
apiToken: "",
priority: 0,
},
custom: {
type: "custom" as const,
shoutrrrUrl: "",
@@ -141,6 +147,7 @@ export const CreateNotificationForm = ({ onSubmit, mode = "create", initialValue
<SelectItem value="discord">Discord</SelectItem>
<SelectItem value="gotify">Gotify</SelectItem>
<SelectItem value="ntfy">Ntfy</SelectItem>
<SelectItem value="pushover">Pushover</SelectItem>
<SelectItem value="custom">Custom (Shoutrrr URL)</SelectItem>
</SelectContent>
</Select>
@@ -490,6 +497,80 @@ export const CreateNotificationForm = ({ onSubmit, mode = "create", initialValue
</>
)}
{watchedType === "pushover" && (
<>
<FormField
control={form.control}
name="userKey"
render={({ field }) => (
<FormItem>
<FormLabel>User Key</FormLabel>
<FormControl>
<Input {...field} placeholder="uQiRzpo4DXghDmr9QzzfQu27cmVRsG" />
</FormControl>
<FormDescription>Your Pushover user key from the dashboard.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="apiToken"
render={({ field }) => (
<FormItem>
<FormLabel>API Token</FormLabel>
<FormControl>
<Input {...field} type="password" placeholder="••••••••" />
</FormControl>
<FormDescription>Application API token from your Pushover application.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="devices"
render={({ field }) => (
<FormItem>
<FormLabel>Devices (Optional)</FormLabel>
<FormControl>
<Input {...field} placeholder="iphone,android" />
</FormControl>
<FormDescription>Comma-separated list of device names. Leave empty for all devices.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="priority"
render={({ field }) => (
<FormItem>
<FormLabel>Priority</FormLabel>
<Select
onValueChange={(value) => field.onChange(Number(value))}
defaultValue={String(field.value)}
value={String(field.value)}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select priority" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="-1">Low (-1)</SelectItem>
<SelectItem value="0">Normal (0)</SelectItem>
<SelectItem value="1">High (1)</SelectItem>
</SelectContent>
</Select>
<FormDescription>Message priority level.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</>
)}
{watchedType === "custom" && (
<FormField
control={form.control}
@@ -513,7 +594,7 @@ export const CreateNotificationForm = ({ onSubmit, mode = "create", initialValue
>
Shoutrrr documentation
</a>
&nbsp; for supported services and URL formats.
&nbsp;for supported services and URL formats.
</FormDescription>
<FormMessage />
</FormItem>

View File

@@ -101,6 +101,7 @@ export default function Notifications({ loaderData }: Route.ComponentProps) {
<SelectItem value="discord">Discord</SelectItem>
<SelectItem value="gotify">Gotify</SelectItem>
<SelectItem value="ntfy">Ntfy</SelectItem>
<SelectItem value="pushover">Pushover</SelectItem>
<SelectItem value="custom">Custom</SelectItem>
</SelectContent>
</Select>

View File

@@ -6,6 +6,7 @@ export const NOTIFICATION_TYPES = {
discord: "discord",
gotify: "gotify",
ntfy: "ntfy",
pushover: "pushover",
custom: "custom",
} as const;
@@ -52,6 +53,14 @@ export const ntfyNotificationConfigSchema = type({
priority: "'max' | 'high' | 'default' | 'low' | 'min'",
});
export const pushoverNotificationConfigSchema = type({
type: "'pushover'",
userKey: "string",
apiToken: "string",
devices: "string?",
priority: "-1 | 0 | 1",
});
export const customNotificationConfigSchema = type({
type: "'custom'",
shoutrrrUrl: "string",
@@ -62,6 +71,7 @@ export const notificationConfigSchema = emailNotificationConfigSchema
.or(discordNotificationConfigSchema)
.or(gotifyNotificationConfigSchema)
.or(ntfyNotificationConfigSchema)
.or(pushoverNotificationConfigSchema)
.or(customNotificationConfigSchema);
export type NotificationConfig = typeof notificationConfigSchema.infer;

View File

@@ -4,6 +4,7 @@ import { buildSlackShoutrrrUrl } from "./slack";
import { buildDiscordShoutrrrUrl } from "./discord";
import { buildGotifyShoutrrrUrl } from "./gotify";
import { buildNtfyShoutrrrUrl } from "./ntfy";
import { buildPushoverShoutrrrUrl } from "./pushover";
import { buildCustomShoutrrrUrl } from "./custom";
export function buildShoutrrrUrl(config: NotificationConfig): string {
@@ -18,6 +19,8 @@ export function buildShoutrrrUrl(config: NotificationConfig): string {
return buildGotifyShoutrrrUrl(config);
case "ntfy":
return buildNtfyShoutrrrUrl(config);
case "pushover":
return buildPushoverShoutrrrUrl(config);
case "custom":
return buildCustomShoutrrrUrl(config);
default: {

View File

@@ -0,0 +1,24 @@
import type { NotificationConfig } from "~/schemas/notifications";
export function buildPushoverShoutrrrUrl(
config: Extract<NotificationConfig, { type: "pushover" }>,
): string {
const params = new URLSearchParams();
if (config.devices) {
params.append("devices", config.devices);
}
if (config.priority !== undefined) {
params.append("priority", config.priority.toString());
}
const queryString = params.toString();
let shoutrrrUrl = `pushover://shoutrrr:${config.apiToken}@${config.userKey}/`;
if (queryString) {
shoutrrrUrl += `?${queryString}`;
}
return shoutrrrUrl;
}

View File

@@ -60,6 +60,11 @@ async function encryptSensitiveFields(config: NotificationConfig): Promise<Notif
...config,
token: config.token ? await cryptoUtils.encrypt(config.token) : undefined,
};
case "pushover":
return {
...config,
apiToken: await cryptoUtils.encrypt(config.apiToken),
};
case "custom":
return {
...config,
@@ -97,6 +102,11 @@ async function decryptSensitiveFields(config: NotificationConfig): Promise<Notif
...config,
token: config.token ? await cryptoUtils.decrypt(config.token) : undefined,
};
case "pushover":
return {
...config,
apiToken: await cryptoUtils.decrypt(config.apiToken),
};
case "custom":
return {
...config,