Compare commits

...

3 Commits

Author SHA1 Message Date
Nicolas Meienberger
e99487eed9 fix(notifications): multiple providers using the wrong params 2025-11-23 21:09:23 +01:00
Nicolas Meienberger
8d4e5d2d4e fix(ntfy): wrong usage of token 2025-11-23 20:49:44 +01:00
Nicolas Meienberger
daea3e64e4 fix(smtp-notification): always use smtp:// 2025-11-23 20:37:21 +01:00
9 changed files with 107 additions and 25 deletions

View File

@@ -1859,13 +1859,15 @@ export type GetScheduleNotificationsResponses = {
priority: 'default' | 'high' | 'low' | 'max' | 'min';
topic: string;
type: 'ntfy';
password?: string;
serverUrl?: string;
token?: string;
username?: string;
} | {
priority: number;
serverUrl: string;
token: string;
type: 'gotify';
path?: string;
} | {
shoutrrrUrl: string;
type: 'custom';
@@ -1873,6 +1875,7 @@ export type GetScheduleNotificationsResponses = {
type: 'discord';
webhookUrl: string;
avatarUrl?: string;
threadId?: string;
username?: string;
} | {
type: 'slack';
@@ -1940,13 +1943,15 @@ export type UpdateScheduleNotificationsResponses = {
priority: 'default' | 'high' | 'low' | 'max' | 'min';
topic: string;
type: 'ntfy';
password?: string;
serverUrl?: string;
token?: string;
username?: string;
} | {
priority: number;
serverUrl: string;
token: string;
type: 'gotify';
path?: string;
} | {
shoutrrrUrl: string;
type: 'custom';
@@ -1954,6 +1959,7 @@ export type UpdateScheduleNotificationsResponses = {
type: 'discord';
webhookUrl: string;
avatarUrl?: string;
threadId?: string;
username?: string;
} | {
type: 'slack';
@@ -2010,13 +2016,15 @@ export type ListNotificationDestinationsResponses = {
priority: 'default' | 'high' | 'low' | 'max' | 'min';
topic: string;
type: 'ntfy';
password?: string;
serverUrl?: string;
token?: string;
username?: string;
} | {
priority: number;
serverUrl: string;
token: string;
type: 'gotify';
path?: string;
} | {
shoutrrrUrl: string;
type: 'custom';
@@ -2024,6 +2032,7 @@ export type ListNotificationDestinationsResponses = {
type: 'discord';
webhookUrl: string;
avatarUrl?: string;
threadId?: string;
username?: string;
} | {
type: 'slack';
@@ -2064,13 +2073,15 @@ export type CreateNotificationDestinationData = {
priority: 'default' | 'high' | 'low' | 'max' | 'min';
topic: string;
type: 'ntfy';
password?: string;
serverUrl?: string;
token?: string;
username?: string;
} | {
priority: number;
serverUrl: string;
token: string;
type: 'gotify';
path?: string;
} | {
shoutrrrUrl: string;
type: 'custom';
@@ -2078,6 +2089,7 @@ export type CreateNotificationDestinationData = {
type: 'discord';
webhookUrl: string;
avatarUrl?: string;
threadId?: string;
username?: string;
} | {
type: 'slack';
@@ -2117,13 +2129,15 @@ export type CreateNotificationDestinationResponses = {
priority: 'default' | 'high' | 'low' | 'max' | 'min';
topic: string;
type: 'ntfy';
password?: string;
serverUrl?: string;
token?: string;
username?: string;
} | {
priority: number;
serverUrl: string;
token: string;
type: 'gotify';
path?: string;
} | {
shoutrrrUrl: string;
type: 'custom';
@@ -2131,6 +2145,7 @@ export type CreateNotificationDestinationResponses = {
type: 'discord';
webhookUrl: string;
avatarUrl?: string;
threadId?: string;
username?: string;
} | {
type: 'slack';
@@ -2217,13 +2232,15 @@ export type GetNotificationDestinationResponses = {
priority: 'default' | 'high' | 'low' | 'max' | 'min';
topic: string;
type: 'ntfy';
password?: string;
serverUrl?: string;
token?: string;
username?: string;
} | {
priority: number;
serverUrl: string;
token: string;
type: 'gotify';
path?: string;
} | {
shoutrrrUrl: string;
type: 'custom';
@@ -2231,6 +2248,7 @@ export type GetNotificationDestinationResponses = {
type: 'discord';
webhookUrl: string;
avatarUrl?: string;
threadId?: string;
username?: string;
} | {
type: 'slack';
@@ -2271,13 +2289,15 @@ export type UpdateNotificationDestinationData = {
priority: 'default' | 'high' | 'low' | 'max' | 'min';
topic: string;
type: 'ntfy';
password?: string;
serverUrl?: string;
token?: string;
username?: string;
} | {
priority: number;
serverUrl: string;
token: string;
type: 'gotify';
path?: string;
} | {
shoutrrrUrl: string;
type: 'custom';
@@ -2285,6 +2305,7 @@ export type UpdateNotificationDestinationData = {
type: 'discord';
webhookUrl: string;
avatarUrl?: string;
threadId?: string;
username?: string;
} | {
type: 'slack';
@@ -2334,13 +2355,15 @@ export type UpdateNotificationDestinationResponses = {
priority: 'default' | 'high' | 'low' | 'max' | 'min';
topic: string;
type: 'ntfy';
password?: string;
serverUrl?: string;
token?: string;
username?: string;
} | {
priority: number;
serverUrl: string;
token: string;
type: 'gotify';
path?: string;
} | {
shoutrrrUrl: string;
type: 'custom';
@@ -2348,6 +2371,7 @@ export type UpdateNotificationDestinationResponses = {
type: 'discord';
webhookUrl: string;
avatarUrl?: string;
threadId?: string;
username?: string;
} | {
type: 'slack';

View File

@@ -370,6 +370,22 @@ export const CreateNotificationForm = ({ onSubmit, mode = "create", initialValue
</FormItem>
)}
/>
<FormField
control={form.control}
name="threadId"
render={({ field }) => (
<FormItem>
<FormLabel>Thread ID (Optional)</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>
ID of the thread to post messages in. Leave empty to post in the main channel.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</>
)}
@@ -423,6 +439,20 @@ export const CreateNotificationForm = ({ onSubmit, mode = "create", initialValue
</FormItem>
)}
/>
<FormField
control={form.control}
name="path"
render={({ field }) => (
<FormItem>
<FormLabel>Path (Optional)</FormLabel>
<FormControl>
<Input {...field} placeholder="/custom/path" />
</FormControl>
<FormDescription>Custom path on the Gotify server, if applicable.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</>
)}
@@ -458,14 +488,28 @@ export const CreateNotificationForm = ({ onSubmit, mode = "create", initialValue
/>
<FormField
control={form.control}
name="token"
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Access Token (Optional)</FormLabel>
<FormLabel>Username (Optional)</FormLabel>
<FormControl>
<Input {...field} placeholder="username" />
</FormControl>
<FormDescription>Username for server authentication, if required.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password (Optional)</FormLabel>
<FormControl>
<Input {...field} type="password" placeholder="••••••••" />
</FormControl>
<FormDescription>Required if the topic is protected.</FormDescription>
<FormDescription>Password for server authentication, if required.</FormDescription>
<FormMessage />
</FormItem>
)}

View File

@@ -36,12 +36,14 @@ export const discordNotificationConfigSchema = type({
webhookUrl: "string",
username: "string?",
avatarUrl: "string?",
threadId: "string?",
});
export const gotifyNotificationConfigSchema = type({
type: "'gotify'",
serverUrl: "string",
token: "string",
path: "string?",
priority: "0 <= number <= 10",
});
@@ -49,8 +51,9 @@ export const ntfyNotificationConfigSchema = type({
type: "'ntfy'",
serverUrl: "string?",
topic: "string",
token: "string?",
priority: "'max' | 'high' | 'default' | 'low' | 'min'",
username: "string?",
password: "string?",
});
export const pushoverNotificationConfigSchema = type({

View File

@@ -17,7 +17,10 @@ export function buildDiscordShoutrrrUrl(config: Extract<NotificationConfig, { ty
params.append("username", config.username);
}
if (config.avatarUrl) {
params.append("avatar_url", config.avatarUrl);
params.append("avatarurl", config.avatarUrl);
}
if (config.threadId) {
params.append("thread_id", config.threadId);
}
if (params.toString()) {

View File

@@ -1,10 +1,10 @@
import type { NotificationConfig } from "~/schemas/notifications";
export function buildEmailShoutrrrUrl(config: Extract<NotificationConfig, { type: "email" }>): string {
const protocol = config.useTLS ? "smtps" : "smtp";
const auth = `${encodeURIComponent(config.username)}:${encodeURIComponent(config.password)}`;
const host = `${config.smtpHost}:${config.smtpPort}`;
const toRecipients = config.to.map((email) => encodeURIComponent(email)).join(",");
const useStartTLS = config.useTLS ? "yes" : "no";
return `${protocol}://${auth}@${host}/?from=${encodeURIComponent(config.from)}&to=${toRecipients}`;
return `smtp://${auth}@${host}/?from=${encodeURIComponent(config.from)}&to=${toRecipients}&starttls=${useStartTLS}`;
}

View File

@@ -4,8 +4,9 @@ export function buildGotifyShoutrrrUrl(config: Extract<NotificationConfig, { typ
const url = new URL(config.serverUrl);
const hostname = url.hostname;
const port = url.port ? `:${url.port}` : "";
const path = config.path ? `/${config.path.replace(/^\/+|\/+$/g, "")}` : "";
let shoutrrrUrl = `gotify://${hostname}${port}/${config.token}`;
let shoutrrrUrl = `gotify://${hostname}${port}${path}/${config.token}`;
if (config.priority !== undefined) {
shoutrrrUrl += `?priority=${config.priority}`;

View File

@@ -3,19 +3,26 @@ import type { NotificationConfig } from "~/schemas/notifications";
export function buildNtfyShoutrrrUrl(config: Extract<NotificationConfig, { type: "ntfy" }>): string {
let shoutrrrUrl: string;
const params = new URLSearchParams();
const auth =
config.username && config.password
? `${encodeURIComponent(config.username)}:${encodeURIComponent(config.password)}@`
: "";
if (config.serverUrl) {
const url = new URL(config.serverUrl);
const hostname = url.hostname;
const port = url.port ? `:${url.port}` : "";
shoutrrrUrl = `ntfy://${hostname}${port}/${config.topic}`;
const scheme = url.protocol === "https:" ? "https" : "http";
params.append("scheme", scheme);
shoutrrrUrl = `ntfy://${auth}${hostname}${port}/${config.topic}`;
} else {
shoutrrrUrl = `ntfy://ntfy.sh/${config.topic}`;
shoutrrrUrl = `ntfy://${auth}ntfy.sh/${config.topic}`;
}
const params = new URLSearchParams();
if (config.token) {
params.append("token", config.token);
}
if (config.priority) {
params.append("priority", config.priority);
}

View File

@@ -20,7 +20,7 @@ export function buildSlackShoutrrrUrl(config: Extract<NotificationConfig, { type
params.append("username", config.username);
}
if (config.iconEmoji) {
params.append("icon", config.iconEmoji);
params.append("icon_emoji", config.iconEmoji);
}
if (params.toString()) {

View File

@@ -58,7 +58,7 @@ async function encryptSensitiveFields(config: NotificationConfig): Promise<Notif
case "ntfy":
return {
...config,
token: config.token ? await cryptoUtils.encrypt(config.token) : undefined,
password: config.password ? await cryptoUtils.encrypt(config.password) : undefined,
};
case "pushover":
return {
@@ -100,7 +100,7 @@ async function decryptSensitiveFields(config: NotificationConfig): Promise<Notif
case "ntfy":
return {
...config,
token: config.token ? await cryptoUtils.decrypt(config.token) : undefined,
password: config.password ? await cryptoUtils.decrypt(config.password) : undefined,
};
case "pushover":
return {