mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
Merge pull request #1081 from depado/gotify-notifications
feat(notifications): implement gotify provider
This commit is contained in:
@@ -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) {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
15
apps/dokploy/drizzle/0056_majestic_skaar.sql
Normal file
15
apps/dokploy/drizzle/0056_majestic_skaar.sql
Normal 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 $$;
|
||||||
4314
apps/dokploy/drizzle/meta/0056_snapshot.json
Normal file
4314
apps/dokploy/drizzle/meta/0056_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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 = [
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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")}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user