feat: add generic webhook form & refactor each type in its own form (#239)
* feat: add generic webhook form & refactor each type in its own form * chore: pr feedbacks * fix(email-form): filter out empty emails
This commit is contained in:
parent
b0c397ab87
commit
658c42a53b
24 changed files with 1019 additions and 568 deletions
|
|
@ -2151,6 +2151,15 @@ export type GetScheduleNotificationsResponses = {
|
|||
useTLS: boolean;
|
||||
password?: string;
|
||||
username?: string;
|
||||
} | {
|
||||
method: 'GET' | 'POST';
|
||||
type: 'generic';
|
||||
url: string;
|
||||
contentType?: string;
|
||||
headers?: Array<string>;
|
||||
messageKey?: string;
|
||||
titleKey?: string;
|
||||
useJson?: boolean;
|
||||
} | {
|
||||
priority: 'default' | 'high' | 'low' | 'max' | 'min';
|
||||
topic: string;
|
||||
|
|
@ -2184,7 +2193,7 @@ export type GetScheduleNotificationsResponses = {
|
|||
enabled: boolean;
|
||||
id: number;
|
||||
name: string;
|
||||
type: 'custom' | 'discord' | 'email' | 'gotify' | 'ntfy' | 'pushover' | 'slack' | 'telegram';
|
||||
type: 'custom' | 'discord' | 'email' | 'generic' | 'gotify' | 'ntfy' | 'pushover' | 'slack' | 'telegram';
|
||||
updatedAt: number;
|
||||
};
|
||||
destinationId: number;
|
||||
|
|
@ -2241,6 +2250,15 @@ export type UpdateScheduleNotificationsResponses = {
|
|||
useTLS: boolean;
|
||||
password?: string;
|
||||
username?: string;
|
||||
} | {
|
||||
method: 'GET' | 'POST';
|
||||
type: 'generic';
|
||||
url: string;
|
||||
contentType?: string;
|
||||
headers?: Array<string>;
|
||||
messageKey?: string;
|
||||
titleKey?: string;
|
||||
useJson?: boolean;
|
||||
} | {
|
||||
priority: 'default' | 'high' | 'low' | 'max' | 'min';
|
||||
topic: string;
|
||||
|
|
@ -2274,7 +2292,7 @@ export type UpdateScheduleNotificationsResponses = {
|
|||
enabled: boolean;
|
||||
id: number;
|
||||
name: string;
|
||||
type: 'custom' | 'discord' | 'email' | 'gotify' | 'ntfy' | 'pushover' | 'slack' | 'telegram';
|
||||
type: 'custom' | 'discord' | 'email' | 'generic' | 'gotify' | 'ntfy' | 'pushover' | 'slack' | 'telegram';
|
||||
updatedAt: number;
|
||||
};
|
||||
destinationId: number;
|
||||
|
|
@ -2568,6 +2586,15 @@ export type ListNotificationDestinationsResponses = {
|
|||
useTLS: boolean;
|
||||
password?: string;
|
||||
username?: string;
|
||||
} | {
|
||||
method: 'GET' | 'POST';
|
||||
type: 'generic';
|
||||
url: string;
|
||||
contentType?: string;
|
||||
headers?: Array<string>;
|
||||
messageKey?: string;
|
||||
titleKey?: string;
|
||||
useJson?: boolean;
|
||||
} | {
|
||||
priority: 'default' | 'high' | 'low' | 'max' | 'min';
|
||||
topic: string;
|
||||
|
|
@ -2601,7 +2628,7 @@ export type ListNotificationDestinationsResponses = {
|
|||
enabled: boolean;
|
||||
id: number;
|
||||
name: string;
|
||||
type: 'custom' | 'discord' | 'email' | 'gotify' | 'ntfy' | 'pushover' | 'slack' | 'telegram';
|
||||
type: 'custom' | 'discord' | 'email' | 'generic' | 'gotify' | 'ntfy' | 'pushover' | 'slack' | 'telegram';
|
||||
updatedAt: number;
|
||||
}>;
|
||||
};
|
||||
|
|
@ -2629,6 +2656,15 @@ export type CreateNotificationDestinationData = {
|
|||
useTLS: boolean;
|
||||
password?: string;
|
||||
username?: string;
|
||||
} | {
|
||||
method: 'GET' | 'POST';
|
||||
type: 'generic';
|
||||
url: string;
|
||||
contentType?: string;
|
||||
headers?: Array<string>;
|
||||
messageKey?: string;
|
||||
titleKey?: string;
|
||||
useJson?: boolean;
|
||||
} | {
|
||||
priority: 'default' | 'high' | 'low' | 'max' | 'min';
|
||||
topic: string;
|
||||
|
|
@ -2689,6 +2725,15 @@ export type CreateNotificationDestinationResponses = {
|
|||
useTLS: boolean;
|
||||
password?: string;
|
||||
username?: string;
|
||||
} | {
|
||||
method: 'GET' | 'POST';
|
||||
type: 'generic';
|
||||
url: string;
|
||||
contentType?: string;
|
||||
headers?: Array<string>;
|
||||
messageKey?: string;
|
||||
titleKey?: string;
|
||||
useJson?: boolean;
|
||||
} | {
|
||||
priority: 'default' | 'high' | 'low' | 'max' | 'min';
|
||||
topic: string;
|
||||
|
|
@ -2722,7 +2767,7 @@ export type CreateNotificationDestinationResponses = {
|
|||
enabled: boolean;
|
||||
id: number;
|
||||
name: string;
|
||||
type: 'custom' | 'discord' | 'email' | 'gotify' | 'ntfy' | 'pushover' | 'slack' | 'telegram';
|
||||
type: 'custom' | 'discord' | 'email' | 'generic' | 'gotify' | 'ntfy' | 'pushover' | 'slack' | 'telegram';
|
||||
updatedAt: number;
|
||||
};
|
||||
};
|
||||
|
|
@ -2796,6 +2841,15 @@ export type GetNotificationDestinationResponses = {
|
|||
useTLS: boolean;
|
||||
password?: string;
|
||||
username?: string;
|
||||
} | {
|
||||
method: 'GET' | 'POST';
|
||||
type: 'generic';
|
||||
url: string;
|
||||
contentType?: string;
|
||||
headers?: Array<string>;
|
||||
messageKey?: string;
|
||||
titleKey?: string;
|
||||
useJson?: boolean;
|
||||
} | {
|
||||
priority: 'default' | 'high' | 'low' | 'max' | 'min';
|
||||
topic: string;
|
||||
|
|
@ -2829,7 +2883,7 @@ export type GetNotificationDestinationResponses = {
|
|||
enabled: boolean;
|
||||
id: number;
|
||||
name: string;
|
||||
type: 'custom' | 'discord' | 'email' | 'gotify' | 'ntfy' | 'pushover' | 'slack' | 'telegram';
|
||||
type: 'custom' | 'discord' | 'email' | 'generic' | 'gotify' | 'ntfy' | 'pushover' | 'slack' | 'telegram';
|
||||
updatedAt: number;
|
||||
};
|
||||
};
|
||||
|
|
@ -2857,6 +2911,15 @@ export type UpdateNotificationDestinationData = {
|
|||
useTLS: boolean;
|
||||
password?: string;
|
||||
username?: string;
|
||||
} | {
|
||||
method: 'GET' | 'POST';
|
||||
type: 'generic';
|
||||
url: string;
|
||||
contentType?: string;
|
||||
headers?: Array<string>;
|
||||
messageKey?: string;
|
||||
titleKey?: string;
|
||||
useJson?: boolean;
|
||||
} | {
|
||||
priority: 'default' | 'high' | 'low' | 'max' | 'min';
|
||||
topic: string;
|
||||
|
|
@ -2927,6 +2990,15 @@ export type UpdateNotificationDestinationResponses = {
|
|||
useTLS: boolean;
|
||||
password?: string;
|
||||
username?: string;
|
||||
} | {
|
||||
method: 'GET' | 'POST';
|
||||
type: 'generic';
|
||||
url: string;
|
||||
contentType?: string;
|
||||
headers?: Array<string>;
|
||||
messageKey?: string;
|
||||
titleKey?: string;
|
||||
useJson?: boolean;
|
||||
} | {
|
||||
priority: 'default' | 'high' | 'low' | 'max' | 'min';
|
||||
topic: string;
|
||||
|
|
@ -2960,7 +3032,7 @@ export type UpdateNotificationDestinationResponses = {
|
|||
enabled: boolean;
|
||||
id: number;
|
||||
name: string;
|
||||
type: 'custom' | 'discord' | 'email' | 'gotify' | 'ntfy' | 'pushover' | 'slack' | 'telegram';
|
||||
type: 'custom' | 'discord' | 'email' | 'generic' | 'gotify' | 'ntfy' | 'pushover' | 'slack' | 'telegram';
|
||||
updatedAt: number;
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -14,10 +14,19 @@ import {
|
|||
FormMessage,
|
||||
} from "~/client/components/ui/form";
|
||||
import { Input } from "~/client/components/ui/input";
|
||||
import { SecretInput } from "~/client/components/ui/secret-input";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/client/components/ui/select";
|
||||
import { Checkbox } from "~/client/components/ui/checkbox";
|
||||
import { notificationConfigSchemaBase } from "~/schemas/notifications";
|
||||
import {
|
||||
CustomForm,
|
||||
DiscordForm,
|
||||
EmailForm,
|
||||
GenericForm,
|
||||
GotifyForm,
|
||||
NtfyForm,
|
||||
PushoverForm,
|
||||
SlackForm,
|
||||
TelegramForm,
|
||||
} from "./notification-forms";
|
||||
|
||||
export const formSchema = type({
|
||||
name: "2<=string<=32",
|
||||
|
|
@ -48,6 +57,9 @@ const defaultValuesForType = {
|
|||
slack: {
|
||||
type: "slack" as const,
|
||||
webhookUrl: "",
|
||||
channel: "",
|
||||
username: "",
|
||||
iconEmoji: "",
|
||||
},
|
||||
discord: {
|
||||
type: "discord" as const,
|
||||
|
|
@ -75,6 +87,16 @@ const defaultValuesForType = {
|
|||
botToken: "",
|
||||
chatId: "",
|
||||
},
|
||||
generic: {
|
||||
type: "generic" as const,
|
||||
url: "",
|
||||
method: "POST" as const,
|
||||
contentType: "application/json",
|
||||
headers: [],
|
||||
useJson: true,
|
||||
titleKey: "title",
|
||||
messageKey: "message",
|
||||
},
|
||||
custom: {
|
||||
type: "custom" as const,
|
||||
shoutrrrUrl: "",
|
||||
|
|
@ -84,7 +106,9 @@ const defaultValuesForType = {
|
|||
export const CreateNotificationForm = ({ onSubmit, mode = "create", initialValues, formId, className }: Props) => {
|
||||
const form = useForm<NotificationFormValues>({
|
||||
resolver: arktypeResolver(cleanSchema as unknown as typeof formSchema),
|
||||
defaultValues: initialValues,
|
||||
defaultValues: initialValues || {
|
||||
name: "",
|
||||
},
|
||||
resetOptions: {
|
||||
keepDefaultValues: true,
|
||||
keepDirtyValues: false,
|
||||
|
|
@ -97,7 +121,7 @@ export const CreateNotificationForm = ({ onSubmit, mode = "create", initialValue
|
|||
useEffect(() => {
|
||||
if (!initialValues) {
|
||||
form.reset({
|
||||
name: form.getValues().name,
|
||||
name: form.getValues().name || "",
|
||||
...defaultValuesForType[watchedType as keyof typeof defaultValuesForType],
|
||||
});
|
||||
}
|
||||
|
|
@ -152,6 +176,7 @@ export const CreateNotificationForm = ({ onSubmit, mode = "create", initialValue
|
|||
<SelectItem value="ntfy">Ntfy</SelectItem>
|
||||
<SelectItem value="pushover">Pushover</SelectItem>
|
||||
<SelectItem value="telegram">Telegram</SelectItem>
|
||||
<SelectItem value="generic">Generic Webhook</SelectItem>
|
||||
<SelectItem value="custom">Custom (Shoutrrr URL)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
|
@ -161,545 +186,15 @@ export const CreateNotificationForm = ({ onSubmit, mode = "create", initialValue
|
|||
)}
|
||||
/>
|
||||
|
||||
{watchedType === "email" && (
|
||||
<>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="smtpHost"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>SMTP Host</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="smtp.example.com" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="smtpPort"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>SMTP Port</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
type="number"
|
||||
placeholder="587"
|
||||
onChange={(e) => field.onChange(Number(e.target.value))}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="username"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Username (Optional)</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="user@example.com" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Password (Optional)</FormLabel>
|
||||
<FormControl>
|
||||
<SecretInput {...field} placeholder="••••••••" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="from"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>From Address</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="noreply@example.com" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="to"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>To Addresses</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="user@example.com, admin@example.com"
|
||||
value={Array.isArray(field.value) ? field.value.join(", ") : ""}
|
||||
onChange={(e) => field.onChange(e.target.value.split(",").map((email) => email.trim()))}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>Comma-separated list of recipient email addresses.</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="useTLS"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center space-x-3">
|
||||
<FormControl>
|
||||
<Checkbox checked={field.value} onCheckedChange={field.onChange} />
|
||||
</FormControl>
|
||||
<div className="space-y-1 leading-none">
|
||||
<FormLabel>Use TLS</FormLabel>
|
||||
<FormDescription>Enable TLS encryption for SMTP connection.</FormDescription>
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{watchedType === "slack" && (
|
||||
<>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="webhookUrl"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Webhook URL</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXX"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>Get this from your Slack app's Incoming Webhooks settings.</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="channel"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Channel (Optional)</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="#backups" />
|
||||
</FormControl>
|
||||
<FormDescription>Override the default channel (use # for channels, @ for users).</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="username"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Bot Username (Optional)</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="Zerobyte" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="iconEmoji"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Icon Emoji (Optional)</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder=":floppy_disk:" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{watchedType === "discord" && (
|
||||
<>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="webhookUrl"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Webhook URL</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="https://discord.com/api/webhooks/WEBHOOK_ID/WEBHOOK_TOKEN" />
|
||||
</FormControl>
|
||||
<FormDescription>Get this from your Discord server's Integrations settings.</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="username"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Bot Username (Optional)</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="Zerobyte" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="avatarUrl"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Avatar URL (Optional)</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="https://example.com/avatar.png" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</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>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{watchedType === "gotify" && (
|
||||
<>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="serverUrl"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Server URL</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="https://gotify.example.com" />
|
||||
</FormControl>
|
||||
<FormDescription>Your self-hosted Gotify server URL.</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="token"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>App Token</FormLabel>
|
||||
<FormControl>
|
||||
<SecretInput {...field} placeholder="••••••••" />
|
||||
</FormControl>
|
||||
<FormDescription>Application token from Gotify.</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="priority"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Priority</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
type="number"
|
||||
min={0}
|
||||
max={10}
|
||||
onChange={(e) => field.onChange(Number(e.target.value))}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>Priority level (0-10, where 10 is highest).</FormDescription>
|
||||
<FormMessage />
|
||||
</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>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{watchedType === "ntfy" && (
|
||||
<>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="serverUrl"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Server URL (Optional)</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="https://ntfy.example.com" />
|
||||
</FormControl>
|
||||
<FormDescription>Leave empty to use ntfy.sh public service.</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="topic"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Topic</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="ironmount-backups" />
|
||||
</FormControl>
|
||||
<FormDescription>The ntfy topic name to publish to.</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="username"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<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>
|
||||
<SecretInput {...field} placeholder="••••••••" />
|
||||
</FormControl>
|
||||
<FormDescription>Password for server authentication, if required.</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="accessToken"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Access token (Optional)</FormLabel>
|
||||
<FormControl>
|
||||
<SecretInput {...field} placeholder="••••••••" />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Access token for server authentication. Will take precedence over username/password if set.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="priority"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Priority</FormLabel>
|
||||
<Select onValueChange={field.onChange} defaultValue={String(field.value)} value={String(field.value)}>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select priority" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="max">Max (5)</SelectItem>
|
||||
<SelectItem value="high">High (4)</SelectItem>
|
||||
<SelectItem value="default">Default (3)</SelectItem>
|
||||
<SelectItem value="low">Low (2)</SelectItem>
|
||||
<SelectItem value="min">Min (1)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{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>
|
||||
<SecretInput {...field} 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 === "telegram" && (
|
||||
<>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="botToken"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Bot Token</FormLabel>
|
||||
<FormControl>
|
||||
<SecretInput {...field} placeholder="123456789:ABC-DEF1234ghIkl-zyx57W2v1u123ew11" />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Telegram bot token. Get this from BotFather when you create your bot.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="chatId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Chat ID</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="-1231234567890" />
|
||||
</FormControl>
|
||||
<FormDescription>Telegram chat ID to send notifications to.</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{watchedType === "custom" && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="shoutrrrUrl"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Shoutrrr URL</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="smtp://user:pass@smtp.gmail.com:587/?from=you@gmail.com&to=recipient@example.com"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Direct Shoutrrr URL for power users. See
|
||||
<a
|
||||
href="https://shoutrrr.nickfedor.com/v0.12.0/services/overview/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-strong-accent hover:underline"
|
||||
>
|
||||
Shoutrrr documentation
|
||||
</a>
|
||||
for supported services and URL formats.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{watchedType === "email" && <EmailForm form={form} />}
|
||||
{watchedType === "slack" && <SlackForm form={form} />}
|
||||
{watchedType === "discord" && <DiscordForm form={form} />}
|
||||
{watchedType === "gotify" && <GotifyForm form={form} />}
|
||||
{watchedType === "ntfy" && <NtfyForm form={form} />}
|
||||
{watchedType === "pushover" && <PushoverForm form={form} />}
|
||||
{watchedType === "telegram" && <TelegramForm form={form} />}
|
||||
{watchedType === "generic" && <GenericForm form={form} />}
|
||||
{watchedType === "custom" && <CustomForm form={form} />}
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
import type { UseFormReturn } from "react-hook-form";
|
||||
import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "~/client/components/ui/form";
|
||||
import { Input } from "~/client/components/ui/input";
|
||||
import type { NotificationFormValues } from "../create-notification-form";
|
||||
|
||||
type Props = {
|
||||
form: UseFormReturn<NotificationFormValues>;
|
||||
};
|
||||
|
||||
export const CustomForm = ({ form }: Props) => {
|
||||
return (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="shoutrrrUrl"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Shoutrrr URL</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="smtp://user:pass@smtp.gmail.com:587/?from=you@gmail.com&to=recipient@example.com"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Direct Shoutrrr URL for power users. See
|
||||
<a
|
||||
href="https://shoutrrr.nickfedor.com/v0.12.0/services/overview/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-strong-accent hover:underline"
|
||||
>
|
||||
Shoutrrr documentation
|
||||
</a>
|
||||
for supported services and URL formats.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
import type { UseFormReturn } from "react-hook-form";
|
||||
import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "~/client/components/ui/form";
|
||||
import { Input } from "~/client/components/ui/input";
|
||||
import type { NotificationFormValues } from "../create-notification-form";
|
||||
|
||||
type Props = {
|
||||
form: UseFormReturn<NotificationFormValues>;
|
||||
};
|
||||
|
||||
export const DiscordForm = ({ form }: Props) => {
|
||||
return (
|
||||
<>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="webhookUrl"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Webhook URL</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="https://discord.com/api/webhooks/WEBHOOK_ID/WEBHOOK_TOKEN" />
|
||||
</FormControl>
|
||||
<FormDescription>Get this from your Discord server's Integrations settings.</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="username"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Bot Username (Optional)</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="Zerobyte" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="avatarUrl"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Avatar URL (Optional)</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="https://example.com/avatar.png" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</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>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,128 @@
|
|||
import type { UseFormReturn } from "react-hook-form";
|
||||
import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "~/client/components/ui/form";
|
||||
import { Input } from "~/client/components/ui/input";
|
||||
import { SecretInput } from "~/client/components/ui/secret-input";
|
||||
import { Checkbox } from "~/client/components/ui/checkbox";
|
||||
import type { NotificationFormValues } from "../create-notification-form";
|
||||
|
||||
type Props = {
|
||||
form: UseFormReturn<NotificationFormValues>;
|
||||
};
|
||||
|
||||
export const EmailForm = ({ form }: Props) => {
|
||||
return (
|
||||
<>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="smtpHost"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>SMTP Host</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="smtp.example.com" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="smtpPort"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>SMTP Port</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
type="number"
|
||||
placeholder="587"
|
||||
onChange={(e) => field.onChange(Number(e.target.value))}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="username"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Username (Optional)</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="user@example.com" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Password (Optional)</FormLabel>
|
||||
<FormControl>
|
||||
<SecretInput {...field} placeholder="••••••••" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="from"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>From Address</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="noreply@example.com" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="to"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>To Addresses</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="user@example.com, admin@example.com"
|
||||
value={Array.isArray(field.value) ? field.value.join(", ") : ""}
|
||||
onChange={(e) =>
|
||||
field.onChange(
|
||||
e.target.value
|
||||
.split(",")
|
||||
.map((email) => email.trim())
|
||||
.filter(Boolean),
|
||||
)
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>Comma-separated list of recipient email addresses.</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="useTLS"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center space-x-3">
|
||||
<FormControl>
|
||||
<Checkbox checked={field.value} onCheckedChange={field.onChange} />
|
||||
</FormControl>
|
||||
<div className="space-y-1 leading-none">
|
||||
<FormLabel>Use TLS</FormLabel>
|
||||
<FormDescription>Enable TLS encryption for SMTP connection.</FormDescription>
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,173 @@
|
|||
import type { UseFormReturn } from "react-hook-form";
|
||||
import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "~/client/components/ui/form";
|
||||
import { Input } from "~/client/components/ui/input";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/client/components/ui/select";
|
||||
import { Checkbox } from "~/client/components/ui/checkbox";
|
||||
import { Textarea } from "~/client/components/ui/textarea";
|
||||
import { CodeBlock } from "~/client/components/ui/code-block";
|
||||
import { Label } from "~/client/components/ui/label";
|
||||
import type { NotificationFormValues } from "../create-notification-form";
|
||||
|
||||
type Props = {
|
||||
form: UseFormReturn<NotificationFormValues>;
|
||||
};
|
||||
|
||||
const WebhookPreview = ({ values }: { values: Partial<NotificationFormValues> }) => {
|
||||
if (values.type !== "generic") return null;
|
||||
|
||||
const contentType = values.contentType || "application/json";
|
||||
const headers = values.headers || [];
|
||||
const useJson = values.useJson;
|
||||
const titleKey = values.titleKey || "title";
|
||||
const messageKey = values.messageKey || "message";
|
||||
|
||||
let body = "";
|
||||
if (useJson) {
|
||||
body = JSON.stringify(
|
||||
{
|
||||
[titleKey]: "Notification title",
|
||||
[messageKey]: "Notification message",
|
||||
},
|
||||
null,
|
||||
2,
|
||||
);
|
||||
} else {
|
||||
body = "Notification message";
|
||||
}
|
||||
|
||||
const previewCode = `${values.method} ${values.url}\nContent-Type: ${contentType}${headers.length > 0 ? `\n${headers.join("\n")}` : ""}
|
||||
|
||||
${body}`;
|
||||
|
||||
return (
|
||||
<div className="space-y-2 pt-4 border-t">
|
||||
<Label>Request Preview</Label>
|
||||
<CodeBlock code={previewCode} filename="HTTP Request" />
|
||||
<p className="text-[0.8rem] text-muted-foreground">This is a preview of the HTTP request that will be sent.</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const GenericForm = ({ form }: Props) => {
|
||||
const watchedValues = form.watch();
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="url"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Webhook URL</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="https://api.example.com/webhook" />
|
||||
</FormControl>
|
||||
<FormDescription>The target URL for the webhook.</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="method"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Method</FormLabel>
|
||||
<Select onValueChange={field.onChange} value={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select method" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="GET">GET</SelectItem>
|
||||
<SelectItem value="POST">POST</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="contentType"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Content Type</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="application/json" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="headers"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Headers</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
{...field}
|
||||
placeholder="Authorization: Bearer token X-Custom-Header: value"
|
||||
value={Array.isArray(field.value) ? field.value.join("\n") : ""}
|
||||
onChange={(e) => field.onChange(e.target.value.split("\n"))}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>One header per line in Key: Value format.</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="useJson"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center space-x-3">
|
||||
<FormControl>
|
||||
<Checkbox checked={field.value} onCheckedChange={field.onChange} />
|
||||
</FormControl>
|
||||
<div className="space-y-1 leading-none">
|
||||
<FormLabel>Use JSON Template</FormLabel>
|
||||
<FormDescription>Send the message as a JSON object.</FormDescription>
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{form.watch("useJson") && (
|
||||
<>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="titleKey"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Title Key</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="title" />
|
||||
</FormControl>
|
||||
<FormDescription>The JSON key for the notification title.</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="messageKey"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Message Key</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="message" />
|
||||
</FormControl>
|
||||
<FormDescription>The JSON key for the notification message.</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<WebhookPreview values={watchedValues} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
import type { UseFormReturn } from "react-hook-form";
|
||||
import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "~/client/components/ui/form";
|
||||
import { Input } from "~/client/components/ui/input";
|
||||
import { SecretInput } from "~/client/components/ui/secret-input";
|
||||
import type { NotificationFormValues } from "../create-notification-form";
|
||||
|
||||
type Props = {
|
||||
form: UseFormReturn<NotificationFormValues>;
|
||||
};
|
||||
|
||||
export const GotifyForm = ({ form }: Props) => {
|
||||
return (
|
||||
<>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="serverUrl"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Server URL</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="https://gotify.example.com" />
|
||||
</FormControl>
|
||||
<FormDescription>Your self-hosted Gotify server URL.</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="token"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>App Token</FormLabel>
|
||||
<FormControl>
|
||||
<SecretInput {...field} placeholder="••••••••" />
|
||||
</FormControl>
|
||||
<FormDescription>Application token from Gotify.</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="priority"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Priority</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
type="number"
|
||||
min={0}
|
||||
max={10}
|
||||
onChange={(e) => field.onChange(Number(e.target.value))}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>Priority level (0-10, where 10 is highest).</FormDescription>
|
||||
<FormMessage />
|
||||
</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>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
export * from "./email-form";
|
||||
export * from "./slack-form";
|
||||
export * from "./discord-form";
|
||||
export * from "./gotify-form";
|
||||
export * from "./ntfy-form";
|
||||
export * from "./pushover-form";
|
||||
export * from "./telegram-form";
|
||||
export * from "./generic-form";
|
||||
export * from "./custom-form";
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
import type { UseFormReturn } from "react-hook-form";
|
||||
import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "~/client/components/ui/form";
|
||||
import { Input } from "~/client/components/ui/input";
|
||||
import { SecretInput } from "~/client/components/ui/secret-input";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/client/components/ui/select";
|
||||
import type { NotificationFormValues } from "../create-notification-form";
|
||||
|
||||
type Props = {
|
||||
form: UseFormReturn<NotificationFormValues>;
|
||||
};
|
||||
|
||||
export const NtfyForm = ({ form }: Props) => {
|
||||
return (
|
||||
<>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="serverUrl"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Server URL (Optional)</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="https://ntfy.example.com" />
|
||||
</FormControl>
|
||||
<FormDescription>Leave empty to use ntfy.sh public service.</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="topic"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Topic</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="ironmount-backups" />
|
||||
</FormControl>
|
||||
<FormDescription>The ntfy topic name to publish to.</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="username"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<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>
|
||||
<SecretInput {...field} placeholder="••••••••" />
|
||||
</FormControl>
|
||||
<FormDescription>Password for server authentication, if required.</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="accessToken"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Access token (Optional)</FormLabel>
|
||||
<FormControl>
|
||||
<SecretInput {...field} placeholder="••••••••" />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Access token for server authentication. Will take precedence over username/password if set.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="priority"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Priority</FormLabel>
|
||||
<Select onValueChange={field.onChange} defaultValue={String(field.value)} value={String(field.value)}>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select priority" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="max">Max (5)</SelectItem>
|
||||
<SelectItem value="high">High (4)</SelectItem>
|
||||
<SelectItem value="default">Default (3)</SelectItem>
|
||||
<SelectItem value="low">Low (2)</SelectItem>
|
||||
<SelectItem value="min">Min (1)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
import type { UseFormReturn } from "react-hook-form";
|
||||
import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "~/client/components/ui/form";
|
||||
import { Input } from "~/client/components/ui/input";
|
||||
import { SecretInput } from "~/client/components/ui/secret-input";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/client/components/ui/select";
|
||||
import type { NotificationFormValues } from "../create-notification-form";
|
||||
|
||||
type Props = {
|
||||
form: UseFormReturn<NotificationFormValues>;
|
||||
};
|
||||
|
||||
export const PushoverForm = ({ form }: Props) => {
|
||||
return (
|
||||
<>
|
||||
<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>
|
||||
<SecretInput {...field} 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>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
import type { UseFormReturn } from "react-hook-form";
|
||||
import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "~/client/components/ui/form";
|
||||
import { Input } from "~/client/components/ui/input";
|
||||
import type { NotificationFormValues } from "../create-notification-form";
|
||||
|
||||
type Props = {
|
||||
form: UseFormReturn<NotificationFormValues>;
|
||||
};
|
||||
|
||||
export const SlackForm = ({ form }: Props) => {
|
||||
return (
|
||||
<>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="webhookUrl"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Webhook URL</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXX"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>Get this from your Slack app's Incoming Webhooks settings.</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="channel"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Channel (Optional)</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="#backups" />
|
||||
</FormControl>
|
||||
<FormDescription>Override the default channel (use # for channels, @ for users).</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="username"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Bot Username (Optional)</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="Zerobyte" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="iconEmoji"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Icon Emoji (Optional)</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder=":floppy_disk:" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
import type { UseFormReturn } from "react-hook-form";
|
||||
import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "~/client/components/ui/form";
|
||||
import { Input } from "~/client/components/ui/input";
|
||||
import { SecretInput } from "~/client/components/ui/secret-input";
|
||||
import type { NotificationFormValues } from "../create-notification-form";
|
||||
|
||||
type Props = {
|
||||
form: UseFormReturn<NotificationFormValues>;
|
||||
};
|
||||
|
||||
export const TelegramForm = ({ form }: Props) => {
|
||||
return (
|
||||
<>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="botToken"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Bot Token</FormLabel>
|
||||
<FormControl>
|
||||
<SecretInput {...field} placeholder="123456789:ABC-DEF1234ghIkl-zyx57W2v1u123ew11" />
|
||||
</FormControl>
|
||||
<FormDescription>Telegram bot token. Get this from BotFather when you create your bot.</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="chatId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Chat ID</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="-1231234567890" />
|
||||
</FormControl>
|
||||
<FormDescription>Telegram chat ID to send notifications to.</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -8,6 +8,7 @@ export const NOTIFICATION_TYPES = {
|
|||
ntfy: "ntfy",
|
||||
pushover: "pushover",
|
||||
telegram: "telegram",
|
||||
generic: "generic",
|
||||
custom: "custom",
|
||||
} as const;
|
||||
|
||||
|
|
@ -72,6 +73,17 @@ export const telegramNotificationConfigSchema = type({
|
|||
chatId: "string",
|
||||
});
|
||||
|
||||
export const genericNotificationConfigSchema = type({
|
||||
type: "'generic'",
|
||||
url: "string",
|
||||
method: "'GET' | 'POST'",
|
||||
contentType: "string?",
|
||||
headers: "string[]?",
|
||||
useJson: "boolean?",
|
||||
titleKey: "string?",
|
||||
messageKey: "string?",
|
||||
});
|
||||
|
||||
export const customNotificationConfigSchema = type({
|
||||
type: "'custom'",
|
||||
shoutrrrUrl: "string",
|
||||
|
|
@ -84,6 +96,7 @@ export const notificationConfigSchemaBase = emailNotificationConfigSchema
|
|||
.or(ntfyNotificationConfigSchema)
|
||||
.or(pushoverNotificationConfigSchema)
|
||||
.or(telegramNotificationConfigSchema)
|
||||
.or(genericNotificationConfigSchema)
|
||||
.or(customNotificationConfigSchema);
|
||||
|
||||
export const notificationConfigSchema = notificationConfigSchemaBase.onUndeclaredKey("delete");
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { NotificationConfig } from "~/schemas/notifications";
|
||||
|
||||
export function buildCustomShoutrrrUrl(config: Extract<NotificationConfig, { type: "custom" }>): string {
|
||||
export const buildCustomShoutrrrUrl = (config: Extract<NotificationConfig, { type: "custom" }>) => {
|
||||
return config.shoutrrrUrl;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type { NotificationConfig } from "~/schemas/notifications";
|
||||
|
||||
export function buildDiscordShoutrrrUrl(config: Extract<NotificationConfig, { type: "discord" }>): string {
|
||||
export const buildDiscordShoutrrrUrl = (config: Extract<NotificationConfig, { type: "discord" }>) => {
|
||||
const url = new URL(config.webhookUrl);
|
||||
const pathParts = url.pathname.split("/").filter(Boolean);
|
||||
|
||||
|
|
@ -28,4 +28,4 @@ export function buildDiscordShoutrrrUrl(config: Extract<NotificationConfig, { ty
|
|||
}
|
||||
|
||||
return shoutrrrUrl;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type { NotificationConfig } from "~/schemas/notifications";
|
||||
|
||||
export function buildEmailShoutrrrUrl(config: Extract<NotificationConfig, { type: "email" }>): string {
|
||||
export const buildEmailShoutrrrUrl = (config: Extract<NotificationConfig, { type: "email" }>) => {
|
||||
const auth =
|
||||
config.username && config.password
|
||||
? `${encodeURIComponent(config.username)}:${encodeURIComponent(config.password)}@`
|
||||
|
|
@ -10,4 +10,4 @@ export function buildEmailShoutrrrUrl(config: Extract<NotificationConfig, { type
|
|||
const useStartTLS = config.useTLS ? "yes" : "no";
|
||||
|
||||
return `smtp://${auth}${host}/?from=${encodeURIComponent(config.from)}&to=${toRecipients}&starttls=${useStartTLS}`;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
50
app/server/modules/notifications/builders/generic.ts
Normal file
50
app/server/modules/notifications/builders/generic.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import type { NotificationConfig } from "~/schemas/notifications";
|
||||
|
||||
export const buildGenericShoutrrrUrl = (config: Extract<NotificationConfig, { type: "generic" }>) => {
|
||||
const targetUrl = new URL(config.url);
|
||||
const shoutrrrUrl = new URL(`generic://${targetUrl.host}${targetUrl.pathname}`);
|
||||
|
||||
for (const [key, value] of targetUrl.searchParams.entries()) {
|
||||
const reservedKeys = ["contenttype", "disabletls", "messagekey", "method", "template", "title", "titlekey"];
|
||||
if (reservedKeys.includes(key.toLowerCase())) {
|
||||
shoutrrrUrl.searchParams.append(`_${key}`, value);
|
||||
} else {
|
||||
shoutrrrUrl.searchParams.append(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
if (targetUrl.protocol === "http:") {
|
||||
shoutrrrUrl.searchParams.append("disabletls", "yes");
|
||||
}
|
||||
|
||||
if (config.method) {
|
||||
shoutrrrUrl.searchParams.append("method", config.method);
|
||||
}
|
||||
|
||||
if (config.contentType) {
|
||||
shoutrrrUrl.searchParams.append("contenttype", config.contentType);
|
||||
}
|
||||
|
||||
if (config.useJson) {
|
||||
shoutrrrUrl.searchParams.append("template", "json");
|
||||
}
|
||||
|
||||
if (config.titleKey) {
|
||||
shoutrrrUrl.searchParams.append("titlekey", config.titleKey);
|
||||
}
|
||||
|
||||
if (config.messageKey) {
|
||||
shoutrrrUrl.searchParams.append("messagekey", config.messageKey);
|
||||
}
|
||||
|
||||
if (config.headers) {
|
||||
for (const header of config.headers) {
|
||||
const [key, ...valueParts] = header.split(":");
|
||||
if (key && valueParts.length > 0) {
|
||||
shoutrrrUrl.searchParams.append(`@${key.trim()}`, valueParts.join(":").trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return shoutrrrUrl.toString();
|
||||
};
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import type { NotificationConfig } from "~/schemas/notifications";
|
||||
|
||||
export function buildGotifyShoutrrrUrl(config: Extract<NotificationConfig, { type: "gotify" }>): string {
|
||||
export const buildGotifyShoutrrrUrl = (config: Extract<NotificationConfig, { type: "gotify" }>) => {
|
||||
const url = new URL(config.serverUrl);
|
||||
const hostname = url.hostname;
|
||||
const port = url.port ? `:${url.port}` : "";
|
||||
|
|
@ -13,4 +13,4 @@ export function buildGotifyShoutrrrUrl(config: Extract<NotificationConfig, { typ
|
|||
}
|
||||
|
||||
return shoutrrrUrl;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -6,9 +6,10 @@ import { buildGotifyShoutrrrUrl } from "./gotify";
|
|||
import { buildNtfyShoutrrrUrl } from "./ntfy";
|
||||
import { buildPushoverShoutrrrUrl } from "./pushover";
|
||||
import { buildTelegramShoutrrrUrl } from "./telegram";
|
||||
import { buildGenericShoutrrrUrl } from "./generic";
|
||||
import { buildCustomShoutrrrUrl } from "./custom";
|
||||
|
||||
export function buildShoutrrrUrl(config: NotificationConfig): string {
|
||||
export const buildShoutrrrUrl = (config: NotificationConfig) => {
|
||||
switch (config.type) {
|
||||
case "email":
|
||||
return buildEmailShoutrrrUrl(config);
|
||||
|
|
@ -24,12 +25,13 @@ export function buildShoutrrrUrl(config: NotificationConfig): string {
|
|||
return buildPushoverShoutrrrUrl(config);
|
||||
case "telegram":
|
||||
return buildTelegramShoutrrrUrl(config);
|
||||
case "generic":
|
||||
return buildGenericShoutrrrUrl(config);
|
||||
case "custom":
|
||||
return buildCustomShoutrrrUrl(config);
|
||||
default: {
|
||||
// TypeScript exhaustiveness check
|
||||
const _exhaustive: never = config;
|
||||
throw new Error(`Unsupported notification type: ${(_exhaustive as NotificationConfig).type}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type { NotificationConfig } from "~/schemas/notifications";
|
||||
|
||||
export function buildNtfyShoutrrrUrl(config: Extract<NotificationConfig, { type: "ntfy" }>): string {
|
||||
export const buildNtfyShoutrrrUrl = (config: Extract<NotificationConfig, { type: "ntfy" }>) => {
|
||||
let shoutrrrUrl: string;
|
||||
|
||||
const params = new URLSearchParams();
|
||||
|
|
@ -38,4 +38,4 @@ export function buildNtfyShoutrrrUrl(config: Extract<NotificationConfig, { type:
|
|||
}
|
||||
|
||||
return shoutrrrUrl;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type { NotificationConfig } from "~/schemas/notifications";
|
||||
|
||||
export function buildPushoverShoutrrrUrl(config: Extract<NotificationConfig, { type: "pushover" }>): string {
|
||||
export const buildPushoverShoutrrrUrl = (config: Extract<NotificationConfig, { type: "pushover" }>) => {
|
||||
const params = new URLSearchParams();
|
||||
|
||||
if (config.devices) {
|
||||
|
|
@ -19,4 +19,4 @@ export function buildPushoverShoutrrrUrl(config: Extract<NotificationConfig, { t
|
|||
}
|
||||
|
||||
return shoutrrrUrl;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type { NotificationConfig } from "~/schemas/notifications";
|
||||
|
||||
export function buildSlackShoutrrrUrl(config: Extract<NotificationConfig, { type: "slack" }>): string {
|
||||
export const buildSlackShoutrrrUrl = (config: Extract<NotificationConfig, { type: "slack" }>) => {
|
||||
const url = new URL(config.webhookUrl);
|
||||
const pathParts = url.pathname.split("/").filter(Boolean);
|
||||
|
||||
|
|
@ -28,4 +28,4 @@ export function buildSlackShoutrrrUrl(config: Extract<NotificationConfig, { type
|
|||
}
|
||||
|
||||
return shoutrrrUrl;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { NotificationConfig } from "~/schemas/notifications";
|
||||
|
||||
export function buildTelegramShoutrrrUrl(config: Extract<NotificationConfig, { type: "telegram" }>): string {
|
||||
export const buildTelegramShoutrrrUrl = (config: Extract<NotificationConfig, { type: "telegram" }>) => {
|
||||
return `telegram://${config.botToken}@telegram?channels=${config.chatId}`;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -71,6 +71,8 @@ async function encryptSensitiveFields(config: NotificationConfig): Promise<Notif
|
|||
...config,
|
||||
botToken: await cryptoUtils.sealSecret(config.botToken),
|
||||
};
|
||||
case "generic":
|
||||
return config;
|
||||
case "custom":
|
||||
return {
|
||||
...config,
|
||||
|
|
@ -118,6 +120,8 @@ async function decryptSensitiveFields(config: NotificationConfig): Promise<Notif
|
|||
...config,
|
||||
botToken: await cryptoUtils.resolveSecret(config.botToken),
|
||||
};
|
||||
case "generic":
|
||||
return config;
|
||||
case "custom":
|
||||
return {
|
||||
...config,
|
||||
|
|
|
|||
Loading…
Reference in a new issue