diff --git a/apps/dokploy/components/dashboard/settings/notifications/handle-notifications.tsx b/apps/dokploy/components/dashboard/settings/notifications/handle-notifications.tsx index 08961f2f..6b44e5de 100644 --- a/apps/dokploy/components/dashboard/settings/notifications/handle-notifications.tsx +++ b/apps/dokploy/components/dashboard/settings/notifications/handle-notifications.tsx @@ -28,7 +28,7 @@ import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { Switch } from "@/components/ui/switch"; import { api } from "@/utils/api"; import { zodResolver } from "@hookform/resolvers/zod"; -import { AlertTriangle, Mail, PenBoxIcon, PlusIcon } from "lucide-react"; +import { AlertTriangle, Mail, PenBoxIcon, PlusIcon, MessageCircleMore } from "lucide-react"; import { useEffect, useState } from "react"; import { useFieldArray, useForm } from "react-hook-form"; import { toast } from "sonner"; @@ -84,6 +84,15 @@ export const notificationSchema = z.discriminatedUnion("type", [ .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), ]); export const notificationsMap = { @@ -103,6 +112,10 @@ export const notificationsMap = { icon: , label: "Email", }, + gotify: { + icon: , + label: "Gotify", + }, }; export type NotificationSchema = z.infer; @@ -126,13 +139,14 @@ export const HandleNotifications = ({ notificationId }: Props) => { ); 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 slackMutation = notificationId ? api.notification.updateSlack.useMutation() : api.notification.createSlack.useMutation(); @@ -145,6 +159,9 @@ export const HandleNotifications = ({ notificationId }: Props) => { const emailMutation = notificationId ? api.notification.updateEmail.useMutation() : api.notification.createEmail.useMutation(); + const gotifyMutation = notificationId + ? api.notification.updateGotify.useMutation() + : api.notification.createGotify.useMutation(); const form = useForm({ defaultValues: { @@ -233,6 +250,7 @@ export const HandleNotifications = ({ notificationId }: Props) => { telegram: telegramMutation, discord: discordMutation, email: emailMutation, + gotify: gotifyMutation, }; const onSubmit = async (data: NotificationSchema) => { @@ -300,6 +318,21 @@ export const HandleNotifications = ({ notificationId }: Props) => { notificationId: notificationId || "", emailId: notification?.emailId || "", }); + } 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 || "", + }); } if (promise) { @@ -700,6 +733,94 @@ export const HandleNotifications = ({ notificationId }: Props) => { )} + + {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. + +
+ + + +
+ )} + /> + + )}
@@ -824,7 +945,8 @@ export const HandleNotifications = ({ notificationId }: Props) => { isLoadingSlack || isLoadingTelegram || isLoadingDiscord || - isLoadingEmail + isLoadingEmail || + isLoadingGotify } variant="secondary" onClick={async () => { @@ -853,6 +975,13 @@ export const HandleNotifications = ({ notificationId }: Props) => { 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"), + }); } toast.success("Connection Success"); } catch (err) { diff --git a/apps/dokploy/components/dashboard/settings/notifications/update-notification.tsx b/apps/dokploy/components/dashboard/settings/notifications/update-notification.tsx new file mode 100644 index 00000000..3d8b61e6 --- /dev/null +++ b/apps/dokploy/components/dashboard/settings/notifications/update-notification.tsx @@ -0,0 +1,864 @@ +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 { Switch } from "@/components/ui/switch"; +import { api } from "@/utils/api"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { Mail, Pen } from "lucide-react"; +import { useEffect, useState } from "react"; +import { useFieldArray, useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { + type NotificationSchema, + notificationSchema, +} from "./add-notification"; + +interface Props { + notificationId: string; +} + +export const UpdateNotification = ({ notificationId }: Props) => { + const utils = api.useUtils(); + const [isOpen, setIsOpen] = useState(false); + const { data, refetch } = api.notification.one.useQuery( + { + 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 slackMutation = api.notification.updateSlack.useMutation(); + const telegramMutation = api.notification.updateTelegram.useMutation(); + const discordMutation = api.notification.updateDiscord.useMutation(); + const emailMutation = api.notification.updateEmail.useMutation(); + const gotifyMutation = api.notification.updateGotify.useMutation(); + const { data: isCloud } = api.settings.isCloud.useQuery(); + const form = useForm({ + defaultValues: { + type: "slack", + webhookUrl: "", + channel: "", + }, + resolver: zodResolver(notificationSchema), + }); + const type = form.watch("type"); + + const { fields, append, remove } = useFieldArray({ + control: form.control, + name: "toAddresses" as never, + }); + + useEffect(() => { + if (data) { + if (data.notificationType === "slack") { + form.reset({ + appBuildError: data.appBuildError, + appDeploy: data.appDeploy, + dokployRestart: data.dokployRestart, + databaseBackup: data.databaseBackup, + dockerCleanup: data.dockerCleanup, + webhookUrl: data.slack?.webhookUrl, + channel: data.slack?.channel || "", + name: data.name, + type: data.notificationType, + }); + } else if (data.notificationType === "telegram") { + form.reset({ + appBuildError: data.appBuildError, + appDeploy: data.appDeploy, + dokployRestart: data.dokployRestart, + databaseBackup: data.databaseBackup, + botToken: data.telegram?.botToken, + chatId: data.telegram?.chatId, + type: data.notificationType, + name: data.name, + dockerCleanup: data.dockerCleanup, + }); + } else if (data.notificationType === "discord") { + form.reset({ + appBuildError: data.appBuildError, + appDeploy: data.appDeploy, + dokployRestart: data.dokployRestart, + databaseBackup: data.databaseBackup, + type: data.notificationType, + webhookUrl: data.discord?.webhookUrl, + decoration: data.discord?.decoration || undefined, + name: data.name, + dockerCleanup: data.dockerCleanup, + }); + } else if (data.notificationType === "email") { + form.reset({ + appBuildError: data.appBuildError, + appDeploy: data.appDeploy, + dokployRestart: data.dokployRestart, + databaseBackup: data.databaseBackup, + type: data.notificationType, + smtpServer: data.email?.smtpServer, + smtpPort: data.email?.smtpPort, + username: data.email?.username, + password: data.email?.password, + toAddresses: data.email?.toAddresses, + fromAddress: data.email?.fromAddress, + name: data.name, + dockerCleanup: data.dockerCleanup, + }); + } else if (data.notificationType === "gotify") { + form.reset({ + appBuildError: data.appBuildError, + appDeploy: data.appDeploy, + dokployRestart: data.dokployRestart, + databaseBackup: data.databaseBackup, + type: data.notificationType, + serverUrl: data.gotify?.serverUrl, + appToken: data.gotify?.appToken, + priority: data.gotify?.priority || 5, + decoration: data.gotify?.decoration || undefined, + name: data.name, + dockerCleanup: data.dockerCleanup, + }); + } + } + }, [form, form.reset, data]); + + const onSubmit = async (formData: NotificationSchema) => { + const { + appBuildError, + appDeploy, + dokployRestart, + databaseBackup, + dockerCleanup, + } = formData; + let promise: Promise | null = null; + if (formData?.type === "slack" && data?.slackId) { + promise = slackMutation.mutateAsync({ + appBuildError: appBuildError, + appDeploy: appDeploy, + dokployRestart: dokployRestart, + databaseBackup: databaseBackup, + webhookUrl: formData.webhookUrl, + channel: formData.channel, + name: formData.name, + notificationId: notificationId, + slackId: data?.slackId, + dockerCleanup: dockerCleanup, + }); + } else if (formData.type === "telegram" && data?.telegramId) { + promise = telegramMutation.mutateAsync({ + appBuildError: appBuildError, + appDeploy: appDeploy, + dokployRestart: dokployRestart, + databaseBackup: databaseBackup, + botToken: formData.botToken, + chatId: formData.chatId, + name: formData.name, + notificationId: notificationId, + telegramId: data?.telegramId, + dockerCleanup: dockerCleanup, + }); + } else if (formData.type === "discord" && data?.discordId) { + promise = discordMutation.mutateAsync({ + appBuildError: appBuildError, + appDeploy: appDeploy, + dokployRestart: dokployRestart, + databaseBackup: databaseBackup, + webhookUrl: formData.webhookUrl, + decoration: formData.decoration, + name: formData.name, + notificationId: notificationId, + discordId: data?.discordId, + dockerCleanup: dockerCleanup, + }); + } else if (formData.type === "email" && data?.emailId) { + promise = emailMutation.mutateAsync({ + appBuildError: appBuildError, + appDeploy: appDeploy, + dokployRestart: dokployRestart, + databaseBackup: databaseBackup, + smtpServer: formData.smtpServer, + smtpPort: formData.smtpPort, + username: formData.username, + password: formData.password, + fromAddress: formData.fromAddress, + toAddresses: formData.toAddresses, + name: formData.name, + notificationId: notificationId, + emailId: data?.emailId, + dockerCleanup: dockerCleanup, + }); + } else if (formData.type === "gotify" && data?.gotifyId) { + promise = gotifyMutation.mutateAsync({ + appBuildError: appBuildError, + appDeploy: appDeploy, + dokployRestart: dokployRestart, + databaseBackup: databaseBackup, + serverUrl: formData.serverUrl, + appToken: formData.appToken, + priority: formData.priority, + decoration: formData.decoration, + name: formData.name, + notificationId: notificationId, + gotifyId: data?.gotifyId, + dockerCleanup: dockerCleanup, + }); + } + + if (promise) { + await promise + .then(async () => { + toast.success("Notification Updated"); + await utils.notification.all.invalidate(); + refetch(); + setIsOpen(false); + }) + .catch(() => { + toast.error("Error updating a notification"); + }); + } + }; + return ( + + + + + + + Update Notification + + Update the current notification config + + +
+ +
+
+
+ + {data?.notificationType === "slack" + ? "Slack" + : data?.notificationType === "telegram" + ? "Telegram" + : data?.notificationType === "discord" + ? "Discord" + : data?.notificationType === "email" + ? "Email" + : "Gotify"} + +
+ {data?.notificationType === "slack" && ( + + )} + {data?.notificationType === "telegram" && ( + + )} + {data?.notificationType === "discord" && ( + + )} + {data?.notificationType === "email" && ( + + )} + {data?.notificationType === "gotify" && ( + + )} +
+ +
+ ( + + Name + + + + + + + )} + /> + + {type === "slack" && ( + <> + ( + + Webhook URL + + + + + + + )} + /> + + ( + + Channel + + + + + + + )} + /> + + )} + + {type === "telegram" && ( + <> + ( + + Bot Token + + + + + + + )} + /> + + ( + + Chat ID + + + + + + + )} + /> + + )} + + {type === "discord" && ( + <> + ( + + Webhook URL + + + + + + + )} + /> + + ( + +
+ Decoration + + Decorate the notification with emojis. + +
+ + + +
+ )} + /> + + )} + {type === "email" && ( + <> +
+ ( + + SMTP Server + + + + + + + )} + /> + ( + + SMTP Port + + { + const value = e.target.value; + if (value) { + const port = Number.parseInt(value); + if (port > 0 && port < 65536) { + field.onChange(port); + } + } + }} + /> + + + + + )} + /> +
+ +
+ ( + + Username + + + + + + + )} + /> + + ( + + Password + + + + + + + )} + /> +
+ + ( + + From Address + + + + + + )} + /> +
+ To Addresses + + {fields.map((field, index) => ( +
+ ( + + + + + + + + )} + /> + +
+ ))} + {type === "email" && + "toAddresses" in form.formState.errors && ( +
+ {form.formState?.errors?.toAddresses?.root?.message} +
+ )} +
+ + + + )} + {type === "gotify" && ( + <> + ( + + Server URL + + + + + + )} + /> + + ( + + App Token + + + + + + )} + /> + + ( + + Priority + + { + const value = e.target.value; + if (value) { + const priority = Number.parseInt(value); + if (priority > 0 && priority < 10) { + field.onChange(priority); + } + } + }} + type="number" + /> + + + + )} + /> + ( + +
+ Decoration + + Decorate the notification with emojis. + +
+ + + +
+ )} + /> + + )} +
+
+
+ + Select the actions. + + +
+ ( + +
+ App Deploy + + Trigger the action when a app is deployed. + +
+ + + +
+ )} + /> + ( + +
+ App Builder 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 a dokploy is restarted. + +
+ + + +
+ )} + /> + )} +
+
+
+ + + + + + +
+
+ ); +}; diff --git a/apps/dokploy/drizzle/meta/0054_snapshot.json b/apps/dokploy/drizzle/meta/0054_snapshot.json index 88447e32..cee6723d 100644 --- a/apps/dokploy/drizzle/meta/0054_snapshot.json +++ b/apps/dokploy/drizzle/meta/0054_snapshot.json @@ -4250,4 +4250,4 @@ "schemas": {}, "tables": {} } -} \ No newline at end of file +} diff --git a/apps/dokploy/drizzle/meta/_journal.json b/apps/dokploy/drizzle/meta/_journal.json index 572e67c6..418ac97a 100644 --- a/apps/dokploy/drizzle/meta/_journal.json +++ b/apps/dokploy/drizzle/meta/_journal.json @@ -395,4 +395,4 @@ "breakpoints": true } ] -} \ No newline at end of file +} diff --git a/apps/dokploy/server/api/routers/notification.ts b/apps/dokploy/server/api/routers/notification.ts index f8869503..77f0287a 100644 --- a/apps/dokploy/server/api/routers/notification.ts +++ b/apps/dokploy/server/api/routers/notification.ts @@ -2,20 +2,24 @@ import { adminProcedure, createTRPCRouter, protectedProcedure, + publicProcedure, } from "@/server/api/trpc"; import { db } from "@/server/db"; import { apiCreateDiscord, apiCreateEmail, + apiCreateGotify, apiCreateSlack, apiCreateTelegram, apiFindOneNotification, apiTestDiscordConnection, apiTestEmailConnection, + apiTestGotifyConnection, apiTestSlackConnection, apiTestTelegramConnection, apiUpdateDiscord, apiUpdateEmail, + apiUpdateGotify, apiUpdateSlack, apiUpdateTelegram, notifications, @@ -24,16 +28,19 @@ import { IS_CLOUD, createDiscordNotification, createEmailNotification, + createGotifyNotification, createSlackNotification, createTelegramNotification, findNotificationById, removeNotificationById, sendDiscordNotification, sendEmailNotification, + sendGotifyNotification, sendSlackNotification, sendTelegramNotification, updateDiscordNotification, updateEmailNotification, + updateGotifyNotification, updateSlackNotification, updateTelegramNotification, } from "@dokploy/server"; @@ -300,10 +307,61 @@ export const notificationRouter = createTRPCRouter({ telegram: true, discord: true, email: true, + gotify: true, }, orderBy: desc(notifications.createdAt), ...(IS_CLOUD && { where: eq(notifications.adminId, ctx.user.adminId) }), // TODO: Remove this line when the cloud version is ready }); }), + createGotify: adminProcedure + .input(apiCreateGotify) + .mutation(async ({ input, ctx }) => { + try { + return await createGotifyNotification(input, ctx.user.adminId); + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error creating the notification", + cause: error, + }); + } + }), + updateGotify: adminProcedure + .input(apiUpdateGotify) + .mutation(async ({ input, ctx }) => { + try { + const notification = await findNotificationById(input.notificationId); + if (IS_CLOUD && notification.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to update this notification", + }); + } + return await updateGotifyNotification({ + ...input, + adminId: ctx.user.adminId, + }); + } catch (error) { + throw error; + } + }), + testGotifyConnection: adminProcedure + .input(apiTestGotifyConnection) + .mutation(async ({ input }) => { + try { + await sendGotifyNotification( + input, + "Test Notification", + "Hi, From Dokploy 👋" + ); + return true; + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error testing the notification", + cause: error, + }); + } + }), }); diff --git a/packages/server/src/db/schema/notification.ts b/packages/server/src/db/schema/notification.ts index 5501621d..bbbe18d2 100644 --- a/packages/server/src/db/schema/notification.ts +++ b/packages/server/src/db/schema/notification.ts @@ -10,6 +10,7 @@ export const notificationType = pgEnum("notificationType", [ "telegram", "discord", "email", + "gotify", ]); export const notifications = pgTable("notification", { @@ -39,6 +40,9 @@ export const notifications = pgTable("notification", { emailId: text("emailId").references(() => email.emailId, { onDelete: "cascade", }), + gotifyId: text("gotifyId").references(() => gotify.gotifyId, { + onDelete: "cascade", + }), adminId: text("adminId").references(() => admins.adminId, { onDelete: "cascade", }), @@ -84,6 +88,17 @@ export const email = pgTable("email", { toAddresses: text("toAddress").array().notNull(), }); +export const gotify = pgTable("gotify", { + gotifyId: text("gotifyId") + .notNull() + .primaryKey() + .$defaultFn(() => nanoid()), + serverUrl: text("serverUrl").notNull(), + appToken: text("appToken").notNull(), + priority: integer("priority").notNull().default(5), + decoration: boolean("decoration"), +}); + export const notificationsRelations = relations(notifications, ({ one }) => ({ slack: one(slack, { fields: [notifications.slackId], @@ -101,6 +116,10 @@ export const notificationsRelations = relations(notifications, ({ one }) => ({ fields: [notifications.emailId], references: [email.emailId], }), + gotify: one(gotify, { + fields: [notifications.gotifyId], + references: [gotify.gotifyId], + }), admin: one(admins, { fields: [notifications.adminId], references: [admins.adminId], @@ -224,6 +243,37 @@ export const apiTestEmailConnection = apiCreateEmail.pick({ fromAddress: true, }); +export const apiCreateGotify = notificationsSchema + .pick({ + appBuildError: true, + databaseBackup: true, + dokployRestart: true, + name: true, + appDeploy: true, + dockerCleanup: true, + }) + .extend({ + serverUrl: z.string().min(1), + appToken: z.string().min(1), + priority: z.number().min(1), + decoration: z.boolean(), + }) + .required(); + +export const apiUpdateGotify = apiCreateGotify.partial().extend({ + notificationId: z.string().min(1), + gotifyId: z.string().min(1), + adminId: z.string().optional(), +}); + +export const apiTestGotifyConnection = apiCreateGotify.pick({ + serverUrl: true, + appToken: true, + priority: true, +}).extend({ + decoration: z.boolean().optional(), +}); + export const apiFindOneNotification = notificationsSchema .pick({ notificationId: true, @@ -242,5 +292,8 @@ export const apiSendTest = notificationsSchema username: z.string(), password: z.string(), toAddresses: z.array(z.string()), + serverUrl: z.string(), + appToken: z.string(), + priority: z.number(), }) .partial(); diff --git a/packages/server/src/services/notification.ts b/packages/server/src/services/notification.ts index e75154df..2b62b457 100644 --- a/packages/server/src/services/notification.ts +++ b/packages/server/src/services/notification.ts @@ -2,14 +2,17 @@ import { db } from "@dokploy/server/db"; import { type apiCreateDiscord, type apiCreateEmail, + type apiCreateGotify, type apiCreateSlack, type apiCreateTelegram, type apiUpdateDiscord, type apiUpdateEmail, + type apiUpdateGotify, type apiUpdateSlack, type apiUpdateTelegram, discord, email, + gotify, notifications, slack, telegram, @@ -379,6 +382,96 @@ export const updateEmailNotification = async ( }); }; +export const createGotifyNotification = async ( + input: typeof apiCreateGotify._type, + adminId: string, +) => { + await db.transaction(async (tx) => { + const newGotify = await tx + .insert(gotify) + .values({ + serverUrl: input.serverUrl, + appToken: input.appToken, + priority: input.priority, + decoration: input.decoration, + }) + .returning() + .then((value) => value[0]); + + if (!newGotify) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error input: Inserting gotify", + }); + } + + const newDestination = await tx + .insert(notifications) + .values({ + gotifyId: newGotify.gotifyId, + name: input.name, + appDeploy: input.appDeploy, + appBuildError: input.appBuildError, + databaseBackup: input.databaseBackup, + dokployRestart: input.dokployRestart, + dockerCleanup: input.dockerCleanup, + notificationType: "gotify", + adminId: adminId, + }) + .returning() + .then((value) => value[0]); + + if (!newDestination) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error input: Inserting notification", + }); + } + + return newDestination; + }); +}; + +export const updateGotifyNotification = async ( + input: typeof apiUpdateGotify._type, +) => { + await db.transaction(async (tx) => { + const newDestination = await tx + .update(notifications) + .set({ + name: input.name, + appDeploy: input.appDeploy, + appBuildError: input.appBuildError, + databaseBackup: input.databaseBackup, + dokployRestart: input.dokployRestart, + dockerCleanup: input.dockerCleanup, + adminId: input.adminId, + }) + .where(eq(notifications.notificationId, input.notificationId)) + .returning() + .then((value) => value[0]); + + if (!newDestination) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error Updating notification", + }); + } + + await tx + .update(gotify) + .set({ + serverUrl: input.serverUrl, + appToken: input.appToken, + priority: input.priority, + decoration: input.decoration, + }) + .where(eq(gotify.gotifyId, input.gotifyId)); + + return newDestination; + }); +}; + export const findNotificationById = async (notificationId: string) => { const notification = await db.query.notifications.findFirst({ where: eq(notifications.notificationId, notificationId), @@ -387,6 +480,7 @@ export const findNotificationById = async (notificationId: string) => { telegram: true, discord: true, email: true, + gotify: true, }, }); if (!notification) { diff --git a/packages/server/src/utils/notifications/utils.ts b/packages/server/src/utils/notifications/utils.ts index 2f8324bb..09fdd7bc 100644 --- a/packages/server/src/utils/notifications/utils.ts +++ b/packages/server/src/utils/notifications/utils.ts @@ -3,6 +3,7 @@ import type { email, slack, telegram, + gotify, } from "@dokploy/server/db/schema"; import nodemailer from "nodemailer"; @@ -87,3 +88,31 @@ export const sendSlackNotification = async ( console.log(err); } }; + +export const sendGotifyNotification = async ( + connection: typeof gotify.$inferInsert, + title: string, + message: string, +) => { + const response = await fetch(`${connection.serverUrl}/message`, { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-Gotify-Key": connection.appToken, + }, + body: JSON.stringify({ + title: title, + message: message, + priority: connection.priority, + extras: { + "client::display": { + "contentType": "text/plain" + } + } + }), + }); + + if (!response.ok) { + throw new Error(`Failed to send Gotify notification: ${response.statusText}`); + } +}; \ No newline at end of file