import { zodResolver } from "@hookform/resolvers/zod"; import { AlertTriangle, Mail, MessageCircleMore, PenBoxIcon, PlusIcon, } from "lucide-react"; import { useEffect, useState } from "react"; import { useFieldArray, useForm } from "react-hook-form"; import { toast } from "sonner"; import { z } from "zod"; import { DiscordIcon, SlackIcon, TelegramIcon, } from "@/components/icons/notification-icons"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { Switch } from "@/components/ui/switch"; import { api } from "@/utils/api"; const notificationBaseSchema = z.object({ name: z.string().min(1, { message: "Name is required", }), appDeploy: z.boolean().default(false), appBuildError: z.boolean().default(false), databaseBackup: z.boolean().default(false), dokployRestart: z.boolean().default(false), dockerCleanup: z.boolean().default(false), serverThreshold: z.boolean().default(false), }); export const notificationSchema = z.discriminatedUnion("type", [ z .object({ type: z.literal("slack"), webhookUrl: z.string().min(1, { message: "Webhook URL is required" }), channel: z.string(), }) .merge(notificationBaseSchema), z .object({ type: z.literal("telegram"), botToken: z.string().min(1, { message: "Bot Token is required" }), chatId: z.string().min(1, { message: "Chat ID is required" }), messageThreadId: z.string().optional(), }) .merge(notificationBaseSchema), z .object({ type: z.literal("discord"), webhookUrl: z.string().min(1, { message: "Webhook URL is required" }), decoration: z.boolean().default(true), }) .merge(notificationBaseSchema), z .object({ type: z.literal("email"), smtpServer: z.string().min(1, { message: "SMTP Server is required" }), smtpPort: z.number().min(1, { message: "SMTP Port is required" }), username: z.string().min(1, { message: "Username is required" }), password: z.string().min(1, { message: "Password is required" }), fromAddress: z.string().min(1, { message: "From Address is required" }), toAddresses: z .array( z.string().min(1, { message: "Email is required" }).email({ message: "Email is invalid", }), ) .min(1, { message: "At least one email is required" }), }) .merge(notificationBaseSchema), z .object({ type: z.literal("gotify"), serverUrl: z.string().min(1, { message: "Server URL is required" }), appToken: z.string().min(1, { message: "App Token is required" }), priority: z.number().min(1).max(10).default(5), decoration: z.boolean().default(true), }) .merge(notificationBaseSchema), z .object({ type: z.literal("ntfy"), serverUrl: z.string().min(1, { message: "Server URL is required" }), topic: z.string().min(1, { message: "Topic is required" }), accessToken: z.string().min(1, { message: "Access Token is required" }), priority: z.number().min(1).max(5).default(3), }) .merge(notificationBaseSchema), ]); export const notificationsMap = { slack: { icon: , label: "Slack", }, telegram: { icon: , label: "Telegram", }, discord: { icon: , label: "Discord", }, email: { icon: , label: "Email", }, gotify: { icon: , label: "Gotify", }, ntfy: { icon: , label: "ntfy", }, }; export type NotificationSchema = z.infer; interface Props { notificationId?: string; } export const HandleNotifications = ({ notificationId }: Props) => { const utils = api.useUtils(); const [visible, setVisible] = useState(false); const { data: isCloud } = api.settings.isCloud.useQuery(); const { data: notification } = api.notification.one.useQuery( { notificationId: notificationId || "", }, { enabled: !!notificationId, }, ); const { mutateAsync: testSlackConnection, isLoading: isLoadingSlack } = api.notification.testSlackConnection.useMutation(); const { mutateAsync: testTelegramConnection, isLoading: isLoadingTelegram } = api.notification.testTelegramConnection.useMutation(); const { mutateAsync: testDiscordConnection, isLoading: isLoadingDiscord } = api.notification.testDiscordConnection.useMutation(); const { mutateAsync: testEmailConnection, isLoading: isLoadingEmail } = api.notification.testEmailConnection.useMutation(); const { mutateAsync: testGotifyConnection, isLoading: isLoadingGotify } = api.notification.testGotifyConnection.useMutation(); const { mutateAsync: testNtfyConnection, isLoading: isLoadingNtfy } = api.notification.testNtfyConnection.useMutation(); const slackMutation = notificationId ? api.notification.updateSlack.useMutation() : api.notification.createSlack.useMutation(); const telegramMutation = notificationId ? api.notification.updateTelegram.useMutation() : api.notification.createTelegram.useMutation(); const discordMutation = notificationId ? api.notification.updateDiscord.useMutation() : api.notification.createDiscord.useMutation(); const emailMutation = notificationId ? api.notification.updateEmail.useMutation() : api.notification.createEmail.useMutation(); const gotifyMutation = notificationId ? api.notification.updateGotify.useMutation() : api.notification.createGotify.useMutation(); const ntfyMutation = notificationId ? api.notification.updateNtfy.useMutation() : api.notification.createNtfy.useMutation(); const form = useForm({ defaultValues: { type: "slack", webhookUrl: "", channel: "", name: "", }, resolver: zodResolver(notificationSchema), }); const type = form.watch("type"); const { fields, append, remove } = useFieldArray({ control: form.control, name: "toAddresses" as never, }); useEffect(() => { if (type === "email") { append(""); } }, [type, append]); useEffect(() => { if (notification) { if (notification.notificationType === "slack") { form.reset({ appBuildError: notification.appBuildError, appDeploy: notification.appDeploy, dokployRestart: notification.dokployRestart, databaseBackup: notification.databaseBackup, dockerCleanup: notification.dockerCleanup, webhookUrl: notification.slack?.webhookUrl, channel: notification.slack?.channel || "", name: notification.name, type: notification.notificationType, serverThreshold: notification.serverThreshold, }); } else if (notification.notificationType === "telegram") { form.reset({ appBuildError: notification.appBuildError, appDeploy: notification.appDeploy, dokployRestart: notification.dokployRestart, databaseBackup: notification.databaseBackup, botToken: notification.telegram?.botToken, messageThreadId: notification.telegram?.messageThreadId || "", chatId: notification.telegram?.chatId, type: notification.notificationType, name: notification.name, dockerCleanup: notification.dockerCleanup, serverThreshold: notification.serverThreshold, }); } else if (notification.notificationType === "discord") { form.reset({ appBuildError: notification.appBuildError, appDeploy: notification.appDeploy, dokployRestart: notification.dokployRestart, databaseBackup: notification.databaseBackup, type: notification.notificationType, webhookUrl: notification.discord?.webhookUrl, decoration: notification.discord?.decoration || undefined, name: notification.name, dockerCleanup: notification.dockerCleanup, serverThreshold: notification.serverThreshold, }); } else if (notification.notificationType === "email") { form.reset({ appBuildError: notification.appBuildError, appDeploy: notification.appDeploy, dokployRestart: notification.dokployRestart, databaseBackup: notification.databaseBackup, type: notification.notificationType, smtpServer: notification.email?.smtpServer, smtpPort: notification.email?.smtpPort, username: notification.email?.username, password: notification.email?.password, toAddresses: notification.email?.toAddresses, fromAddress: notification.email?.fromAddress, name: notification.name, dockerCleanup: notification.dockerCleanup, serverThreshold: notification.serverThreshold, }); } else if (notification.notificationType === "gotify") { form.reset({ appBuildError: notification.appBuildError, appDeploy: notification.appDeploy, dokployRestart: notification.dokployRestart, databaseBackup: notification.databaseBackup, type: notification.notificationType, appToken: notification.gotify?.appToken, decoration: notification.gotify?.decoration || undefined, priority: notification.gotify?.priority, serverUrl: notification.gotify?.serverUrl, name: notification.name, dockerCleanup: notification.dockerCleanup, }); } else if (notification.notificationType === "ntfy") { form.reset({ appBuildError: notification.appBuildError, appDeploy: notification.appDeploy, dokployRestart: notification.dokployRestart, databaseBackup: notification.databaseBackup, type: notification.notificationType, accessToken: notification.ntfy?.accessToken, topic: notification.ntfy?.topic, priority: notification.ntfy?.priority, serverUrl: notification.ntfy?.serverUrl, name: notification.name, dockerCleanup: notification.dockerCleanup, }); } } else { form.reset(); } }, [form, form.reset, form.formState.isSubmitSuccessful, notification]); const activeMutation = { slack: slackMutation, telegram: telegramMutation, discord: discordMutation, email: emailMutation, gotify: gotifyMutation, ntfy: ntfyMutation, }; const onSubmit = async (data: NotificationSchema) => { const { appBuildError, appDeploy, dokployRestart, databaseBackup, dockerCleanup, serverThreshold, } = data; let promise: Promise | null = null; if (data.type === "slack") { promise = slackMutation.mutateAsync({ appBuildError: appBuildError, appDeploy: appDeploy, dokployRestart: dokployRestart, databaseBackup: databaseBackup, webhookUrl: data.webhookUrl, channel: data.channel, name: data.name, dockerCleanup: dockerCleanup, slackId: notification?.slackId || "", notificationId: notificationId || "", serverThreshold: serverThreshold, }); } else if (data.type === "telegram") { promise = telegramMutation.mutateAsync({ appBuildError: appBuildError, appDeploy: appDeploy, dokployRestart: dokployRestart, databaseBackup: databaseBackup, botToken: data.botToken, messageThreadId: data.messageThreadId || "", chatId: data.chatId, name: data.name, dockerCleanup: dockerCleanup, notificationId: notificationId || "", telegramId: notification?.telegramId || "", serverThreshold: serverThreshold, }); } else if (data.type === "discord") { promise = discordMutation.mutateAsync({ appBuildError: appBuildError, appDeploy: appDeploy, dokployRestart: dokployRestart, databaseBackup: databaseBackup, webhookUrl: data.webhookUrl, decoration: data.decoration, name: data.name, dockerCleanup: dockerCleanup, notificationId: notificationId || "", discordId: notification?.discordId || "", serverThreshold: serverThreshold, }); } else if (data.type === "email") { promise = emailMutation.mutateAsync({ appBuildError: appBuildError, appDeploy: appDeploy, dokployRestart: dokployRestart, databaseBackup: databaseBackup, smtpServer: data.smtpServer, smtpPort: data.smtpPort, username: data.username, password: data.password, fromAddress: data.fromAddress, toAddresses: data.toAddresses, name: data.name, dockerCleanup: dockerCleanup, notificationId: notificationId || "", emailId: notification?.emailId || "", serverThreshold: serverThreshold, }); } else if (data.type === "gotify") { promise = gotifyMutation.mutateAsync({ appBuildError: appBuildError, appDeploy: appDeploy, dokployRestart: dokployRestart, databaseBackup: databaseBackup, serverUrl: data.serverUrl, appToken: data.appToken, priority: data.priority, name: data.name, dockerCleanup: dockerCleanup, decoration: data.decoration, notificationId: notificationId || "", gotifyId: notification?.gotifyId || "", }); } else if (data.type === "ntfy") { promise = ntfyMutation.mutateAsync({ appBuildError: appBuildError, appDeploy: appDeploy, dokployRestart: dokployRestart, databaseBackup: databaseBackup, serverUrl: data.serverUrl, accessToken: data.accessToken, topic: data.topic, priority: data.priority, name: data.name, dockerCleanup: dockerCleanup, notificationId: notificationId || "", ntfyId: notification?.ntfyId || "", }); } if (promise) { await promise .then(async () => { toast.success( notificationId ? "Notification Updated" : "Notification Created", ); form.reset({ type: "slack", webhookUrl: "", }); setVisible(false); await utils.notification.all.invalidate(); }) .catch(() => { toast.error( notificationId ? "Error updating a notification" : "Error creating a notification", ); }); } }; return ( {notificationId ? ( ) : ( Add Notification )} {notificationId ? "Update" : "Add"} Notification {notificationId ? "Update your notification providers for multiple channels." : "Create new notification providers for multiple channels."} ( Select a provider {Object.entries(notificationsMap).map(([key, value]) => ( {value.icon} {value.label} ))} {activeMutation[field.value].isError && ( {activeMutation[field.value].error?.message} )} )} /> Fill the next fields. ( Name )} /> {type === "slack" && ( <> ( Webhook URL )} /> ( Channel )} /> > )} {type === "telegram" && ( <> ( Bot Token )} /> ( Chat ID )} /> ( Message Thread ID Optional. Use it when you want to send notifications to a specific topic in a group. )} /> > )} {type === "discord" && ( <> ( Webhook URL )} /> ( Decoration Decorate the notification with emojis. )} /> > )} {type === "email" && ( <> ( SMTP Server )} /> ( SMTP Port { const value = e.target.value; if (value === "") { field.onChange(undefined); } else { const port = Number.parseInt(value); if (port > 0 && port < 65536) { field.onChange(port); } } }} value={field.value || ""} type="number" /> )} /> ( Username )} /> ( Password )} /> ( From Address )} /> To Addresses {fields.map((field, index) => ( ( )} /> { remove(index); }} > Remove ))} {type === "email" && "toAddresses" in form.formState.errors && ( {form.formState?.errors?.toAddresses?.root?.message} )} { append(""); }} > Add > )} {type === "gotify" && ( <> ( Server URL )} /> ( App Token )} /> ( Priority { const value = e.target.value; if (value) { const port = Number.parseInt(value); if (port > 0 && port < 10) { field.onChange(port); } } }} type="number" /> Message priority (1-10, default: 5) )} /> ( Decoration Decorate the notification with emojis. )} /> > )} {type === "ntfy" && ( <> ( Server URL )} /> ( Server URL )} /> ( Access Token )} /> ( Priority { const value = e.target.value; if (value) { const port = Number.parseInt(value); if (port > 0 && port <= 5) { field.onChange(port); } } }} type="number" /> Message priority (1-5, default: 3) )} /> > )} Select the actions. ( App Deploy Trigger the action when a app is deployed. )} /> ( App Build Error Trigger the action when the build fails. )} /> ( Database Backup Trigger the action when a database backup is created. )} /> ( Docker Cleanup Trigger the action when the docker cleanup is performed. )} /> {!isCloud && ( ( Dokploy Restart Trigger the action when dokploy is restarted. )} /> )} {isCloud && ( ( Server Threshold Trigger the action when the server threshold is reached. )} /> )} { try { if (type === "slack") { await testSlackConnection({ webhookUrl: form.getValues("webhookUrl"), channel: form.getValues("channel"), }); } else if (type === "telegram") { await testTelegramConnection({ botToken: form.getValues("botToken"), chatId: form.getValues("chatId"), messageThreadId: form.getValues("messageThreadId") || "", }); } else if (type === "discord") { await testDiscordConnection({ webhookUrl: form.getValues("webhookUrl"), decoration: form.getValues("decoration"), }); } else if (type === "email") { await testEmailConnection({ smtpServer: form.getValues("smtpServer"), smtpPort: form.getValues("smtpPort"), username: form.getValues("username"), password: form.getValues("password"), toAddresses: form.getValues("toAddresses"), fromAddress: form.getValues("fromAddress"), }); } else if (type === "gotify") { await testGotifyConnection({ serverUrl: form.getValues("serverUrl"), appToken: form.getValues("appToken"), priority: form.getValues("priority"), decoration: form.getValues("decoration"), }); } else if (type === "ntfy") { await testNtfyConnection({ serverUrl: form.getValues("serverUrl"), topic: form.getValues("topic"), accessToken: form.getValues("accessToken"), priority: form.getValues("priority"), }); } toast.success("Connection Success"); } catch { toast.error("Error testing the provider"); } }} > Test Notification {notificationId ? "Update" : "Create"} ); };