Merge pull request #1081 from depado/gotify-notifications

feat(notifications): implement gotify provider
This commit is contained in:
Mauricio Siu
2025-01-18 17:33:55 -06:00
committed by GitHub
14 changed files with 4817 additions and 10 deletions

View File

@@ -28,7 +28,13 @@ import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { AlertTriangle, Mail, PenBoxIcon, PlusIcon } from "lucide-react"; import {
AlertTriangle,
Mail,
MessageCircleMore,
PenBoxIcon,
PlusIcon,
} from "lucide-react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useFieldArray, useForm } from "react-hook-form"; import { useFieldArray, useForm } from "react-hook-form";
import { toast } from "sonner"; import { toast } from "sonner";
@@ -84,6 +90,15 @@ export const notificationSchema = z.discriminatedUnion("type", [
.min(1, { message: "At least one email is required" }), .min(1, { message: "At least one email is required" }),
}) })
.merge(notificationBaseSchema), .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 = { export const notificationsMap = {
@@ -103,6 +118,10 @@ export const notificationsMap = {
icon: <Mail size={29} className="text-muted-foreground" />, icon: <Mail size={29} className="text-muted-foreground" />,
label: "Email", label: "Email",
}, },
gotify: {
icon: <MessageCircleMore size={29} className="text-muted-foreground" />,
label: "Gotify",
},
}; };
export type NotificationSchema = z.infer<typeof notificationSchema>; export type NotificationSchema = z.infer<typeof notificationSchema>;
@@ -126,13 +145,14 @@ export const HandleNotifications = ({ notificationId }: Props) => {
); );
const { mutateAsync: testSlackConnection, isLoading: isLoadingSlack } = const { mutateAsync: testSlackConnection, isLoading: isLoadingSlack } =
api.notification.testSlackConnection.useMutation(); api.notification.testSlackConnection.useMutation();
const { mutateAsync: testTelegramConnection, isLoading: isLoadingTelegram } = const { mutateAsync: testTelegramConnection, isLoading: isLoadingTelegram } =
api.notification.testTelegramConnection.useMutation(); api.notification.testTelegramConnection.useMutation();
const { mutateAsync: testDiscordConnection, isLoading: isLoadingDiscord } = const { mutateAsync: testDiscordConnection, isLoading: isLoadingDiscord } =
api.notification.testDiscordConnection.useMutation(); api.notification.testDiscordConnection.useMutation();
const { mutateAsync: testEmailConnection, isLoading: isLoadingEmail } = const { mutateAsync: testEmailConnection, isLoading: isLoadingEmail } =
api.notification.testEmailConnection.useMutation(); api.notification.testEmailConnection.useMutation();
const { mutateAsync: testGotifyConnection, isLoading: isLoadingGotify } =
api.notification.testGotifyConnection.useMutation();
const slackMutation = notificationId const slackMutation = notificationId
? api.notification.updateSlack.useMutation() ? api.notification.updateSlack.useMutation()
: api.notification.createSlack.useMutation(); : api.notification.createSlack.useMutation();
@@ -145,6 +165,9 @@ export const HandleNotifications = ({ notificationId }: Props) => {
const emailMutation = notificationId const emailMutation = notificationId
? api.notification.updateEmail.useMutation() ? api.notification.updateEmail.useMutation()
: api.notification.createEmail.useMutation(); : api.notification.createEmail.useMutation();
const gotifyMutation = notificationId
? api.notification.updateGotify.useMutation()
: api.notification.createGotify.useMutation();
const form = useForm<NotificationSchema>({ const form = useForm<NotificationSchema>({
defaultValues: { defaultValues: {
@@ -222,6 +245,20 @@ export const HandleNotifications = ({ notificationId }: Props) => {
name: notification.name, name: notification.name,
dockerCleanup: notification.dockerCleanup, dockerCleanup: notification.dockerCleanup,
}); });
} 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 { } else {
form.reset(); form.reset();
@@ -233,6 +270,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
telegram: telegramMutation, telegram: telegramMutation,
discord: discordMutation, discord: discordMutation,
email: emailMutation, email: emailMutation,
gotify: gotifyMutation,
}; };
const onSubmit = async (data: NotificationSchema) => { const onSubmit = async (data: NotificationSchema) => {
@@ -300,6 +338,21 @@ export const HandleNotifications = ({ notificationId }: Props) => {
notificationId: notificationId || "", notificationId: notificationId || "",
emailId: notification?.emailId || "", 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) { if (promise) {
@@ -700,6 +753,94 @@ export const HandleNotifications = ({ notificationId }: Props) => {
</Button> </Button>
</> </>
)} )}
{type === "gotify" && (
<>
<FormField
control={form.control}
name="serverUrl"
render={({ field }) => (
<FormItem>
<FormLabel>Server URL</FormLabel>
<FormControl>
<Input
placeholder="https://gotify.example.com"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="appToken"
render={({ field }) => (
<FormItem>
<FormLabel>App Token</FormLabel>
<FormControl>
<Input
placeholder="AzxcvbnmKjhgfdsa..."
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="priority"
defaultValue={5}
render={({ field }) => (
<FormItem className="w-full">
<FormLabel>Priority</FormLabel>
<FormControl>
<Input
placeholder="5"
{...field}
onChange={(e) => {
const value = e.target.value;
if (value) {
const port = Number.parseInt(value);
if (port > 0 && port < 10) {
field.onChange(port);
}
}
}}
type="number"
/>
</FormControl>
<FormDescription>
Message priority (1-10, default: 5)
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="decoration"
defaultValue={true}
render={({ field }) => (
<FormItem className="flex items-center justify-between rounded-lg border p-3 shadow-sm">
<div className="space-y-0.5">
<FormLabel>Decoration</FormLabel>
<FormDescription>
Decorate the notification with emojis.
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
</>
)}
</div> </div>
</div> </div>
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
@@ -824,7 +965,8 @@ export const HandleNotifications = ({ notificationId }: Props) => {
isLoadingSlack || isLoadingSlack ||
isLoadingTelegram || isLoadingTelegram ||
isLoadingDiscord || isLoadingDiscord ||
isLoadingEmail isLoadingEmail ||
isLoadingGotify
} }
variant="secondary" variant="secondary"
onClick={async () => { onClick={async () => {
@@ -853,6 +995,13 @@ export const HandleNotifications = ({ notificationId }: Props) => {
toAddresses: form.getValues("toAddresses"), toAddresses: form.getValues("toAddresses"),
fromAddress: form.getValues("fromAddress"), 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"); toast.success("Connection Success");
} catch (err) { } catch (err) {

View File

@@ -13,7 +13,7 @@ import {
CardTitle, CardTitle,
} from "@/components/ui/card"; } from "@/components/ui/card";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { Bell, Loader2, Mail, Trash2 } from "lucide-react"; import { Bell, Loader2, Mail, MessageCircleMore, Trash2 } from "lucide-react";
import { toast } from "sonner"; import { toast } from "sonner";
import { HandleNotifications } from "./handle-notifications"; import { HandleNotifications } from "./handle-notifications";
@@ -83,6 +83,11 @@ export const ShowNotifications = () => {
<Mail className="size-6 text-muted-foreground" /> <Mail className="size-6 text-muted-foreground" />
</div> </div>
)} )}
{notification.notificationType === "gotify" && (
<div className="flex items-center justify-center rounded-lg ">
<MessageCircleMore className="size-6 text-muted-foreground" />
</div>
)}
{notification.name} {notification.name}
</span> </span>

View File

@@ -0,0 +1,15 @@
ALTER TYPE "notificationType" ADD VALUE 'gotify';--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "gotify" (
"gotifyId" text PRIMARY KEY NOT NULL,
"serverUrl" text NOT NULL,
"appToken" text NOT NULL,
"priority" integer DEFAULT 5 NOT NULL,
"decoration" boolean
);
--> statement-breakpoint
ALTER TABLE "notification" ADD COLUMN "gotifyId" text;--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "notification" ADD CONSTRAINT "notification_gotifyId_gotify_gotifyId_fk" FOREIGN KEY ("gotifyId") REFERENCES "public"."gotify"("gotifyId") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;

File diff suppressed because it is too large Load Diff

View File

@@ -393,6 +393,13 @@
"when": 1736669623831, "when": 1736669623831,
"tag": "0055_next_serpent_society", "tag": "0055_next_serpent_society",
"breakpoints": true "breakpoints": true
},
{
"idx": 56,
"version": "6",
"when": 1736789918294,
"tag": "0056_majestic_skaar",
"breakpoints": true
} }
] ]
} }

View File

@@ -2,20 +2,24 @@ import {
adminProcedure, adminProcedure,
createTRPCRouter, createTRPCRouter,
protectedProcedure, protectedProcedure,
publicProcedure,
} from "@/server/api/trpc"; } from "@/server/api/trpc";
import { db } from "@/server/db"; import { db } from "@/server/db";
import { import {
apiCreateDiscord, apiCreateDiscord,
apiCreateEmail, apiCreateEmail,
apiCreateGotify,
apiCreateSlack, apiCreateSlack,
apiCreateTelegram, apiCreateTelegram,
apiFindOneNotification, apiFindOneNotification,
apiTestDiscordConnection, apiTestDiscordConnection,
apiTestEmailConnection, apiTestEmailConnection,
apiTestGotifyConnection,
apiTestSlackConnection, apiTestSlackConnection,
apiTestTelegramConnection, apiTestTelegramConnection,
apiUpdateDiscord, apiUpdateDiscord,
apiUpdateEmail, apiUpdateEmail,
apiUpdateGotify,
apiUpdateSlack, apiUpdateSlack,
apiUpdateTelegram, apiUpdateTelegram,
notifications, notifications,
@@ -24,16 +28,19 @@ import {
IS_CLOUD, IS_CLOUD,
createDiscordNotification, createDiscordNotification,
createEmailNotification, createEmailNotification,
createGotifyNotification,
createSlackNotification, createSlackNotification,
createTelegramNotification, createTelegramNotification,
findNotificationById, findNotificationById,
removeNotificationById, removeNotificationById,
sendDiscordNotification, sendDiscordNotification,
sendEmailNotification, sendEmailNotification,
sendGotifyNotification,
sendSlackNotification, sendSlackNotification,
sendTelegramNotification, sendTelegramNotification,
updateDiscordNotification, updateDiscordNotification,
updateEmailNotification, updateEmailNotification,
updateGotifyNotification,
updateSlackNotification, updateSlackNotification,
updateTelegramNotification, updateTelegramNotification,
} from "@dokploy/server"; } from "@dokploy/server";
@@ -300,10 +307,61 @@ export const notificationRouter = createTRPCRouter({
telegram: true, telegram: true,
discord: true, discord: true,
email: true, email: true,
gotify: true,
}, },
orderBy: desc(notifications.createdAt), orderBy: desc(notifications.createdAt),
...(IS_CLOUD && { where: eq(notifications.adminId, ctx.user.adminId) }), ...(IS_CLOUD && { where: eq(notifications.adminId, ctx.user.adminId) }),
// TODO: Remove this line when the cloud version is ready // 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,
});
}
}),
}); });

View File

@@ -10,6 +10,7 @@ export const notificationType = pgEnum("notificationType", [
"telegram", "telegram",
"discord", "discord",
"email", "email",
"gotify",
]); ]);
export const notifications = pgTable("notification", { export const notifications = pgTable("notification", {
@@ -39,6 +40,9 @@ export const notifications = pgTable("notification", {
emailId: text("emailId").references(() => email.emailId, { emailId: text("emailId").references(() => email.emailId, {
onDelete: "cascade", onDelete: "cascade",
}), }),
gotifyId: text("gotifyId").references(() => gotify.gotifyId, {
onDelete: "cascade",
}),
adminId: text("adminId").references(() => admins.adminId, { adminId: text("adminId").references(() => admins.adminId, {
onDelete: "cascade", onDelete: "cascade",
}), }),
@@ -84,6 +88,17 @@ export const email = pgTable("email", {
toAddresses: text("toAddress").array().notNull(), 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 }) => ({ export const notificationsRelations = relations(notifications, ({ one }) => ({
slack: one(slack, { slack: one(slack, {
fields: [notifications.slackId], fields: [notifications.slackId],
@@ -101,6 +116,10 @@ export const notificationsRelations = relations(notifications, ({ one }) => ({
fields: [notifications.emailId], fields: [notifications.emailId],
references: [email.emailId], references: [email.emailId],
}), }),
gotify: one(gotify, {
fields: [notifications.gotifyId],
references: [gotify.gotifyId],
}),
admin: one(admins, { admin: one(admins, {
fields: [notifications.adminId], fields: [notifications.adminId],
references: [admins.adminId], references: [admins.adminId],
@@ -224,6 +243,39 @@ export const apiTestEmailConnection = apiCreateEmail.pick({
fromAddress: true, 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 export const apiFindOneNotification = notificationsSchema
.pick({ .pick({
notificationId: true, notificationId: true,
@@ -242,5 +294,8 @@ export const apiSendTest = notificationsSchema
username: z.string(), username: z.string(),
password: z.string(), password: z.string(),
toAddresses: z.array(z.string()), toAddresses: z.array(z.string()),
serverUrl: z.string(),
appToken: z.string(),
priority: z.number(),
}) })
.partial(); .partial();

View File

@@ -2,14 +2,17 @@ import { db } from "@dokploy/server/db";
import { import {
type apiCreateDiscord, type apiCreateDiscord,
type apiCreateEmail, type apiCreateEmail,
type apiCreateGotify,
type apiCreateSlack, type apiCreateSlack,
type apiCreateTelegram, type apiCreateTelegram,
type apiUpdateDiscord, type apiUpdateDiscord,
type apiUpdateEmail, type apiUpdateEmail,
type apiUpdateGotify,
type apiUpdateSlack, type apiUpdateSlack,
type apiUpdateTelegram, type apiUpdateTelegram,
discord, discord,
email, email,
gotify,
notifications, notifications,
slack, slack,
telegram, 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) => { export const findNotificationById = async (notificationId: string) => {
const notification = await db.query.notifications.findFirst({ const notification = await db.query.notifications.findFirst({
where: eq(notifications.notificationId, notificationId), where: eq(notifications.notificationId, notificationId),
@@ -387,6 +480,7 @@ export const findNotificationById = async (notificationId: string) => {
telegram: true, telegram: true,
discord: true, discord: true,
email: true, email: true,
gotify: true,
}, },
}); });
if (!notification) { if (!notification) {

View File

@@ -7,6 +7,7 @@ import { format } from "date-fns";
import { import {
sendDiscordNotification, sendDiscordNotification,
sendEmailNotification, sendEmailNotification,
sendGotifyNotification,
sendSlackNotification, sendSlackNotification,
sendTelegramNotification, sendTelegramNotification,
} from "./utils"; } from "./utils";
@@ -40,11 +41,12 @@ export const sendBuildErrorNotifications = async ({
discord: true, discord: true,
telegram: true, telegram: true,
slack: true, slack: true,
gotify: true,
}, },
}); });
for (const notification of notificationList) { for (const notification of notificationList) {
const { email, discord, telegram, slack } = notification; const { email, discord, telegram, slack, gotify } = notification;
if (email) { if (email) {
const template = await renderAsync( const template = await renderAsync(
BuildFailedEmail({ BuildFailedEmail({
@@ -113,6 +115,21 @@ export const sendBuildErrorNotifications = async ({
}); });
} }
if (gotify) {
const decorate = (decoration: string, text: string) =>
`${gotify.decoration ? decoration : ""} ${text}\n`;
await sendGotifyNotification(
gotify,
decorate("⚠️", "Build Failed"),
`${decorate("🛠️", `Project: ${projectName}`)}` +
`${decorate("⚙️", `Application: ${applicationName}`)}` +
`${decorate("❔", `Type: ${applicationType}`)}` +
`${decorate("🕒", `Date: ${date.toLocaleString()}`)}` +
`${decorate("⚠️", `Error:\n${errorMessage}`)}` +
`${decorate("🔗", `Build details:\n${buildLink}`)}`,
);
}
if (telegram) { if (telegram) {
const inlineButton = [ const inlineButton = [
[ [

View File

@@ -8,6 +8,7 @@ import { format } from "date-fns";
import { import {
sendDiscordNotification, sendDiscordNotification,
sendEmailNotification, sendEmailNotification,
sendGotifyNotification,
sendSlackNotification, sendSlackNotification,
sendTelegramNotification, sendTelegramNotification,
} from "./utils"; } from "./utils";
@@ -41,11 +42,12 @@ export const sendBuildSuccessNotifications = async ({
discord: true, discord: true,
telegram: true, telegram: true,
slack: true, slack: true,
gotify: true,
}, },
}); });
for (const notification of notificationList) { for (const notification of notificationList) {
const { email, discord, telegram, slack } = notification; const { email, discord, telegram, slack, gotify } = notification;
if (email) { if (email) {
const template = await renderAsync( const template = await renderAsync(
@@ -110,6 +112,20 @@ export const sendBuildSuccessNotifications = async ({
}); });
} }
if (gotify) {
const decorate = (decoration: string, text: string) =>
`${gotify.decoration ? decoration : ""} ${text}\n`;
await sendGotifyNotification(
gotify,
decorate("✅", "Build Success"),
`${decorate("🛠️", `Project: ${projectName}`)}` +
`${decorate("⚙️", `Application: ${applicationName}`)}` +
`${decorate("❔", `Type: ${applicationType}`)}` +
`${decorate("🕒", `Date: ${date.toLocaleString()}`)}` +
`${decorate("🔗", `Build details:\n${buildLink}`)}`,
);
}
if (telegram) { if (telegram) {
const chunkArray = <T>(array: T[], chunkSize: number): T[][] => const chunkArray = <T>(array: T[], chunkSize: number): T[][] =>
Array.from({ length: Math.ceil(array.length / chunkSize) }, (_, i) => array.slice(i * chunkSize, i * chunkSize + chunkSize) Array.from({ length: Math.ceil(array.length / chunkSize) }, (_, i) => array.slice(i * chunkSize, i * chunkSize + chunkSize)

View File

@@ -1,3 +1,4 @@
import { error } from "node:console";
import { db } from "@dokploy/server/db"; import { db } from "@dokploy/server/db";
import { notifications } from "@dokploy/server/db/schema"; import { notifications } from "@dokploy/server/db/schema";
import DatabaseBackupEmail from "@dokploy/server/emails/emails/database-backup"; import DatabaseBackupEmail from "@dokploy/server/emails/emails/database-backup";
@@ -7,6 +8,7 @@ import { format } from "date-fns";
import { import {
sendDiscordNotification, sendDiscordNotification,
sendEmailNotification, sendEmailNotification,
sendGotifyNotification,
sendSlackNotification, sendSlackNotification,
sendTelegramNotification, sendTelegramNotification,
} from "./utils"; } from "./utils";
@@ -38,11 +40,12 @@ export const sendDatabaseBackupNotifications = async ({
discord: true, discord: true,
telegram: true, telegram: true,
slack: true, slack: true,
gotify: true,
}, },
}); });
for (const notification of notificationList) { for (const notification of notificationList) {
const { email, discord, telegram, slack } = notification; const { email, discord, telegram, slack, gotify } = notification;
if (email) { if (email) {
const template = await renderAsync( const template = await renderAsync(
@@ -121,6 +124,24 @@ export const sendDatabaseBackupNotifications = async ({
}); });
} }
if (gotify) {
const decorate = (decoration: string, text: string) =>
`${gotify.decoration ? decoration : ""} ${text}\n`;
await sendGotifyNotification(
gotify,
decorate(
type === "success" ? "✅" : "❌",
`Database Backup ${type === "success" ? "Successful" : "Failed"}`,
),
`${decorate("🛠️", `Project: ${projectName}`)}` +
`${decorate("⚙️", `Application: ${applicationName}`)}` +
`${decorate("❔", `Type: ${databaseType}`)}` +
`${decorate("🕒", `Date: ${date.toLocaleString()}`)}` +
`${type === "error" && errorMessage ? decorate("❌", `Error:\n${errorMessage}`) : ""}`,
);
}
if (telegram) { if (telegram) {
const isError = type === "error" && errorMessage; const isError = type === "error" && errorMessage;

View File

@@ -7,6 +7,7 @@ import { format } from "date-fns";
import { import {
sendDiscordNotification, sendDiscordNotification,
sendEmailNotification, sendEmailNotification,
sendGotifyNotification,
sendSlackNotification, sendSlackNotification,
sendTelegramNotification, sendTelegramNotification,
} from "./utils"; } from "./utils";
@@ -27,11 +28,12 @@ export const sendDockerCleanupNotifications = async (
discord: true, discord: true,
telegram: true, telegram: true,
slack: true, slack: true,
gotify: true,
}, },
}); });
for (const notification of notificationList) { for (const notification of notificationList) {
const { email, discord, telegram, slack } = notification; const { email, discord, telegram, slack, gotify } = notification;
if (email) { if (email) {
const template = await renderAsync( const template = await renderAsync(
@@ -80,6 +82,17 @@ export const sendDockerCleanupNotifications = async (
}); });
} }
if (gotify) {
const decorate = (decoration: string, text: string) =>
`${gotify.decoration ? decoration : ""} ${text}\n`;
await sendGotifyNotification(
gotify,
decorate("✅", "Docker Cleanup"),
`${decorate("🕒", `Date: ${date.toLocaleString()}`)}` +
`${decorate("📜", `Message:\n${message}`)}`,
);
}
if (telegram) { if (telegram) {
await sendTelegramNotification( await sendTelegramNotification(
telegram, telegram,

View File

@@ -6,6 +6,7 @@ import { eq } from "drizzle-orm";
import { import {
sendDiscordNotification, sendDiscordNotification,
sendEmailNotification, sendEmailNotification,
sendGotifyNotification,
sendSlackNotification, sendSlackNotification,
sendTelegramNotification, sendTelegramNotification,
} from "./utils"; } from "./utils";
@@ -21,11 +22,12 @@ export const sendDokployRestartNotifications = async () => {
discord: true, discord: true,
telegram: true, telegram: true,
slack: true, slack: true,
gotify: true,
}, },
}); });
for (const notification of notificationList) { for (const notification of notificationList) {
const { email, discord, telegram, slack } = notification; const { email, discord, telegram, slack, gotify } = notification;
if (email) { if (email) {
const template = await renderAsync( const template = await renderAsync(
@@ -65,10 +67,20 @@ export const sendDokployRestartNotifications = async () => {
}); });
} }
if (gotify) {
const decorate = (decoration: string, text: string) =>
`${gotify.decoration ? decoration : ""} ${text}\n`;
await sendGotifyNotification(
gotify,
decorate("✅", "Dokploy Server Restarted"),
`${decorate("🕒", `Date: ${date.toLocaleString()}`)}`,
);
}
if (telegram) { if (telegram) {
await sendTelegramNotification( await sendTelegramNotification(
telegram, telegram,
`<b>✅ Dokploy Serverd Restarted</b>\n\n<b>Date:</b> ${format(date, "PP")}\n<b>Time:</b> ${format(date, "pp")}` `<b>✅ Dokploy Server Restarted</b>\n\n<b>Date:</b> ${format(date, "PP")}\n<b>Time:</b> ${format(date, "pp")}`
); );
} }

View File

@@ -1,6 +1,7 @@
import type { import type {
discord, discord,
email, email,
gotify,
slack, slack,
telegram, telegram,
} from "@dokploy/server/db/schema"; } from "@dokploy/server/db/schema";
@@ -94,3 +95,33 @@ export const sendSlackNotification = async (
console.log(err); 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}`,
);
}
};