Merge branch 'tvarohohlavy-telegram-notification'

This commit is contained in:
Nicolas Meienberger
2025-11-30 17:06:07 +01:00
8 changed files with 107 additions and 13 deletions

View File

@@ -1965,6 +1965,10 @@ export type GetScheduleNotificationsResponses = {
type: 'pushover'; type: 'pushover';
userKey: string; userKey: string;
devices?: string; devices?: string;
} | {
botToken: string;
chatId: string;
type: 'telegram';
} | { } | {
from: string; from: string;
password: string; password: string;
@@ -2007,7 +2011,7 @@ export type GetScheduleNotificationsResponses = {
enabled: boolean; enabled: boolean;
id: number; id: number;
name: string; name: string;
type: 'custom' | 'discord' | 'email' | 'gotify' | 'ntfy' | 'pushover' | 'slack'; type: 'custom' | 'discord' | 'email' | 'gotify' | 'ntfy' | 'pushover' | 'slack' | 'telegram';
updatedAt: number; updatedAt: number;
}; };
destinationId: number; destinationId: number;
@@ -2049,6 +2053,10 @@ export type UpdateScheduleNotificationsResponses = {
type: 'pushover'; type: 'pushover';
userKey: string; userKey: string;
devices?: string; devices?: string;
} | {
botToken: string;
chatId: string;
type: 'telegram';
} | { } | {
from: string; from: string;
password: string; password: string;
@@ -2091,7 +2099,7 @@ export type UpdateScheduleNotificationsResponses = {
enabled: boolean; enabled: boolean;
id: number; id: number;
name: string; name: string;
type: 'custom' | 'discord' | 'email' | 'gotify' | 'ntfy' | 'pushover' | 'slack'; type: 'custom' | 'discord' | 'email' | 'gotify' | 'ntfy' | 'pushover' | 'slack' | 'telegram';
updatedAt: number; updatedAt: number;
}; };
destinationId: number; destinationId: number;
@@ -2122,6 +2130,10 @@ export type ListNotificationDestinationsResponses = {
type: 'pushover'; type: 'pushover';
userKey: string; userKey: string;
devices?: string; devices?: string;
} | {
botToken: string;
chatId: string;
type: 'telegram';
} | { } | {
from: string; from: string;
password: string; password: string;
@@ -2164,7 +2176,7 @@ export type ListNotificationDestinationsResponses = {
enabled: boolean; enabled: boolean;
id: number; id: number;
name: string; name: string;
type: 'custom' | 'discord' | 'email' | 'gotify' | 'ntfy' | 'pushover' | 'slack'; type: 'custom' | 'discord' | 'email' | 'gotify' | 'ntfy' | 'pushover' | 'slack' | 'telegram';
updatedAt: number; updatedAt: number;
}>; }>;
}; };
@@ -2179,6 +2191,10 @@ export type CreateNotificationDestinationData = {
type: 'pushover'; type: 'pushover';
userKey: string; userKey: string;
devices?: string; devices?: string;
} | {
botToken: string;
chatId: string;
type: 'telegram';
} | { } | {
from: string; from: string;
password: string; password: string;
@@ -2235,6 +2251,10 @@ export type CreateNotificationDestinationResponses = {
type: 'pushover'; type: 'pushover';
userKey: string; userKey: string;
devices?: string; devices?: string;
} | {
botToken: string;
chatId: string;
type: 'telegram';
} | { } | {
from: string; from: string;
password: string; password: string;
@@ -2277,7 +2297,7 @@ export type CreateNotificationDestinationResponses = {
enabled: boolean; enabled: boolean;
id: number; id: number;
name: string; name: string;
type: 'custom' | 'discord' | 'email' | 'gotify' | 'ntfy' | 'pushover' | 'slack'; type: 'custom' | 'discord' | 'email' | 'gotify' | 'ntfy' | 'pushover' | 'slack' | 'telegram';
updatedAt: number; updatedAt: number;
}; };
}; };
@@ -2338,6 +2358,10 @@ export type GetNotificationDestinationResponses = {
type: 'pushover'; type: 'pushover';
userKey: string; userKey: string;
devices?: string; devices?: string;
} | {
botToken: string;
chatId: string;
type: 'telegram';
} | { } | {
from: string; from: string;
password: string; password: string;
@@ -2380,7 +2404,7 @@ export type GetNotificationDestinationResponses = {
enabled: boolean; enabled: boolean;
id: number; id: number;
name: string; name: string;
type: 'custom' | 'discord' | 'email' | 'gotify' | 'ntfy' | 'pushover' | 'slack'; type: 'custom' | 'discord' | 'email' | 'gotify' | 'ntfy' | 'pushover' | 'slack' | 'telegram';
updatedAt: number; updatedAt: number;
}; };
}; };
@@ -2395,6 +2419,10 @@ export type UpdateNotificationDestinationData = {
type: 'pushover'; type: 'pushover';
userKey: string; userKey: string;
devices?: string; devices?: string;
} | {
botToken: string;
chatId: string;
type: 'telegram';
} | { } | {
from: string; from: string;
password: string; password: string;
@@ -2461,6 +2489,10 @@ export type UpdateNotificationDestinationResponses = {
type: 'pushover'; type: 'pushover';
userKey: string; userKey: string;
devices?: string; devices?: string;
} | {
botToken: string;
chatId: string;
type: 'telegram';
} | { } | {
from: string; from: string;
password: string; password: string;
@@ -2503,7 +2535,7 @@ export type UpdateNotificationDestinationResponses = {
enabled: boolean; enabled: boolean;
id: number; id: number;
name: string; name: string;
type: 'custom' | 'discord' | 'email' | 'gotify' | 'ntfy' | 'pushover' | 'slack'; type: 'custom' | 'discord' | 'email' | 'gotify' | 'ntfy' | 'pushover' | 'slack' | 'telegram';
updatedAt: number; updatedAt: number;
}; };
}; };

View File

@@ -69,6 +69,11 @@ const defaultValuesForType = {
apiToken: "", apiToken: "",
priority: 0 as const, priority: 0 as const,
}, },
telegram: {
type: "telegram" as const,
botToken: "",
chatId: "",
},
custom: { custom: {
type: "custom" as const, type: "custom" as const,
shoutrrrUrl: "", shoutrrrUrl: "",
@@ -145,6 +150,7 @@ export const CreateNotificationForm = ({ onSubmit, mode = "create", initialValue
<SelectItem value="gotify">Gotify</SelectItem> <SelectItem value="gotify">Gotify</SelectItem>
<SelectItem value="ntfy">Ntfy</SelectItem> <SelectItem value="ntfy">Ntfy</SelectItem>
<SelectItem value="pushover">Pushover</SelectItem> <SelectItem value="pushover">Pushover</SelectItem>
<SelectItem value="telegram">Telegram</SelectItem>
<SelectItem value="custom">Custom (Shoutrrr URL)</SelectItem> <SelectItem value="custom">Custom (Shoutrrr URL)</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
@@ -612,6 +618,41 @@ export const CreateNotificationForm = ({ onSubmit, mode = "create", initialValue
</> </>
)} )}
{watchedType === "telegram" && (
<>
<FormField
control={form.control}
name="botToken"
render={({ field }) => (
<FormItem>
<FormLabel>Bot Token</FormLabel>
<FormControl>
<Input {...field} type="password" 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" && ( {watchedType === "custom" && (
<FormField <FormField
control={form.control} control={form.control}

View File

@@ -171,13 +171,7 @@ export default function NotificationDetailsPage({ loaderData }: Route.ComponentP
</AlertDescription> </AlertDescription>
</Alert> </Alert>
)} )}
<CreateNotificationForm <CreateNotificationForm mode="update" formId={formId} onSubmit={handleSubmit} initialValues={data.config} />
mode="update"
formId={formId}
onSubmit={handleSubmit}
initialValues={data.config}
loading={updateDestination.isPending}
/>
<div className="flex justify-end gap-2 pt-4 border-t"> <div className="flex justify-end gap-2 pt-4 border-t">
<Button type="submit" form={formId} loading={updateDestination.isPending}> <Button type="submit" form={formId} loading={updateDestination.isPending}>
Save Changes Save Changes

View File

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

View File

@@ -7,6 +7,7 @@ export const NOTIFICATION_TYPES = {
gotify: "gotify", gotify: "gotify",
ntfy: "ntfy", ntfy: "ntfy",
pushover: "pushover", pushover: "pushover",
telegram: "telegram",
custom: "custom", custom: "custom",
} as const; } as const;
@@ -64,6 +65,12 @@ export const pushoverNotificationConfigSchema = type({
priority: "-1 | 0 | 1", priority: "-1 | 0 | 1",
}); });
export const telegramNotificationConfigSchema = type({
type: "'telegram'",
botToken: "string",
chatId: "string",
});
export const customNotificationConfigSchema = type({ export const customNotificationConfigSchema = type({
type: "'custom'", type: "'custom'",
shoutrrrUrl: "string", shoutrrrUrl: "string",
@@ -75,6 +82,7 @@ export const notificationConfigSchema = emailNotificationConfigSchema
.or(gotifyNotificationConfigSchema) .or(gotifyNotificationConfigSchema)
.or(ntfyNotificationConfigSchema) .or(ntfyNotificationConfigSchema)
.or(pushoverNotificationConfigSchema) .or(pushoverNotificationConfigSchema)
.or(telegramNotificationConfigSchema)
.or(customNotificationConfigSchema); .or(customNotificationConfigSchema);
export type NotificationConfig = typeof notificationConfigSchema.infer; export type NotificationConfig = typeof notificationConfigSchema.infer;

View File

@@ -5,6 +5,7 @@ import { buildDiscordShoutrrrUrl } from "./discord";
import { buildGotifyShoutrrrUrl } from "./gotify"; import { buildGotifyShoutrrrUrl } from "./gotify";
import { buildNtfyShoutrrrUrl } from "./ntfy"; import { buildNtfyShoutrrrUrl } from "./ntfy";
import { buildPushoverShoutrrrUrl } from "./pushover"; import { buildPushoverShoutrrrUrl } from "./pushover";
import { buildTelegramShoutrrrUrl } from "./telegram";
import { buildCustomShoutrrrUrl } from "./custom"; import { buildCustomShoutrrrUrl } from "./custom";
export function buildShoutrrrUrl(config: NotificationConfig): string { export function buildShoutrrrUrl(config: NotificationConfig): string {
@@ -21,6 +22,8 @@ export function buildShoutrrrUrl(config: NotificationConfig): string {
return buildNtfyShoutrrrUrl(config); return buildNtfyShoutrrrUrl(config);
case "pushover": case "pushover":
return buildPushoverShoutrrrUrl(config); return buildPushoverShoutrrrUrl(config);
case "telegram":
return buildTelegramShoutrrrUrl(config);
case "custom": case "custom":
return buildCustomShoutrrrUrl(config); return buildCustomShoutrrrUrl(config);
default: { default: {

View File

@@ -0,0 +1,5 @@
import type { NotificationConfig } from "~/schemas/notifications";
export function buildTelegramShoutrrrUrl(config: Extract<NotificationConfig, { type: "telegram" }>): string {
return `telegram://${config.botToken}@telegram?channels=${config.chatId}`;
}

View File

@@ -65,6 +65,11 @@ async function encryptSensitiveFields(config: NotificationConfig): Promise<Notif
...config, ...config,
apiToken: await cryptoUtils.encrypt(config.apiToken), apiToken: await cryptoUtils.encrypt(config.apiToken),
}; };
case "telegram":
return {
...config,
botToken: await cryptoUtils.encrypt(config.botToken),
};
case "custom": case "custom":
return { return {
...config, ...config,
@@ -107,6 +112,11 @@ async function decryptSensitiveFields(config: NotificationConfig): Promise<Notif
...config, ...config,
apiToken: await cryptoUtils.decrypt(config.apiToken), apiToken: await cryptoUtils.decrypt(config.apiToken),
}; };
case "telegram":
return {
...config,
botToken: await cryptoUtils.decrypt(config.botToken),
};
case "custom": case "custom":
return { return {
...config, ...config,