Merge branch 'feature/add-custom-webhook-notification-provider' of github.com:ChristoferMendes/dokploy into feature/add-custom-webhook-notification-provider

This commit is contained in:
ChristoferMendes
2025-09-29 08:54:21 -03:00
11 changed files with 2322 additions and 2313 deletions

View File

@@ -6,157 +6,160 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
interface KeyValuePair {
key: string;
value: string;
enabled: boolean;
key: string;
value: string;
enabled: boolean;
}
interface KeyValueInputProps {
value: string;
onChange: (value: string) => void;
placeholder?: string;
label: string;
description?: string;
value: string;
onChange: (value: string) => void;
placeholder?: string;
label: string;
description?: string;
}
const createEmptyPair = (): KeyValuePair => ({
key: "",
value: "",
enabled: true,
key: "",
value: "",
enabled: true,
});
const parseJsonToPairs = (jsonString: string): KeyValuePair[] => {
try {
const parsed = JSON.parse(jsonString);
const pairs = Object.entries(parsed).map(([key, val]) => ({
key,
value: String(val),
enabled: true,
}));
return pairs.length > 0 ? pairs : [createEmptyPair()];
} catch {
return [createEmptyPair()];
}
try {
const parsed = JSON.parse(jsonString);
const pairs = Object.entries(parsed).map(([key, val]) => ({
key,
value: String(val),
enabled: true,
}));
return pairs.length > 0 ? pairs : [createEmptyPair()];
} catch {
return [createEmptyPair()];
}
};
const convertPairsToJson = (pairs: KeyValuePair[]): string => {
const enabledPairs = pairs.filter((pair) => pair.enabled && pair.key.trim());
const enabledPairs = pairs.filter((pair) => pair.enabled && pair.key.trim());
if (enabledPairs.length === 0) {
return "";
}
if (enabledPairs.length === 0) {
return "";
}
const obj = enabledPairs.reduce((acc, pair) => {
acc[pair.key.trim()] = pair.value;
return acc;
}, {} as Record<string, string>);
const obj = enabledPairs.reduce(
(acc, pair) => {
acc[pair.key.trim()] = pair.value;
return acc;
},
{} as Record<string, string>,
);
return JSON.stringify(obj, null, 2);
return JSON.stringify(obj, null, 2);
};
export const KeyValueInput = ({
value,
onChange,
label,
description,
value,
onChange,
label,
description,
}: KeyValueInputProps) => {
const [pairs, setPairs] = useState<KeyValuePair[]>([]);
const [pairs, setPairs] = useState<KeyValuePair[]>([]);
useEffect(() => {
const newPairs = value ? parseJsonToPairs(value) : [createEmptyPair()];
setPairs(newPairs);
}, [value]);
useEffect(() => {
const newPairs = value ? parseJsonToPairs(value) : [createEmptyPair()];
setPairs(newPairs);
}, [value]);
const syncPairsWithParent = (newPairs: KeyValuePair[]) => {
setPairs(newPairs);
onChange(convertPairsToJson(newPairs));
};
const syncPairsWithParent = (newPairs: KeyValuePair[]) => {
setPairs(newPairs);
onChange(convertPairsToJson(newPairs));
};
const addPair = () => {
syncPairsWithParent([...pairs, createEmptyPair()]);
};
const addPair = () => {
syncPairsWithParent([...pairs, createEmptyPair()]);
};
const removePair = (index: number) => {
const filteredPairs = pairs.filter((_, i) => i !== index);
syncPairsWithParent(filteredPairs);
};
const removePair = (index: number) => {
const filteredPairs = pairs.filter((_, i) => i !== index);
syncPairsWithParent(filteredPairs);
};
const updatePair = (
index: number,
field: keyof KeyValuePair,
newValue: string | boolean
) => {
const updatedPairs = pairs.map((pair, i) =>
i === index ? { ...pair, [field]: newValue } : pair
);
syncPairsWithParent(updatedPairs);
};
const updatePair = (
index: number,
field: keyof KeyValuePair,
newValue: string | boolean,
) => {
const updatedPairs = pairs.map((pair, i) =>
i === index ? { ...pair, [field]: newValue } : pair,
);
syncPairsWithParent(updatedPairs);
};
return (
<div className="space-y-3">
<div>
<Label className="text-sm font-medium">{label}</Label>
{description && (
<p className="text-sm text-muted-foreground mt-1">{description}</p>
)}
</div>
return (
<div className="space-y-3">
<div>
<Label className="text-sm font-medium">{label}</Label>
{description && (
<p className="text-sm text-muted-foreground mt-1">{description}</p>
)}
</div>
<div className="space-y-2">
{pairs.map((pair, index) => (
<div
key={index}
className="flex items-center gap-2 p-2 border rounded-md bg-muted/50"
>
<div className="flex items-center">
<Checkbox
checked={pair.enabled}
onCheckedChange={(checked) =>
updatePair(index, "enabled", checked)
}
className="mr-2"
/>
</div>
<div className="flex-1">
<Input
placeholder="Key"
value={pair.key}
onChange={(e) => updatePair(index, "key", e.target.value)}
className="text-sm"
disabled={!pair.enabled}
/>
</div>
<div className="flex-[2]">
<Input
placeholder="Value"
value={pair.value}
onChange={(e) => updatePair(index, "value", e.target.value)}
className="text-sm"
disabled={!pair.enabled}
/>
</div>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => removePair(index)}
className="text-red-500 hover:text-red-700 hover:bg-red-50"
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
))}
</div>
<div className="space-y-2">
{pairs.map((pair, index) => (
<div
key={index}
className="flex items-center gap-2 p-2 border rounded-md bg-muted/50"
>
<div className="flex items-center">
<Checkbox
checked={pair.enabled}
onCheckedChange={(checked) =>
updatePair(index, "enabled", checked)
}
className="mr-2"
/>
</div>
<div className="flex-1">
<Input
placeholder="Key"
value={pair.key}
onChange={(e) => updatePair(index, "key", e.target.value)}
className="text-sm"
disabled={!pair.enabled}
/>
</div>
<div className="flex-[2]">
<Input
placeholder="Value"
value={pair.value}
onChange={(e) => updatePair(index, "value", e.target.value)}
className="text-sm"
disabled={!pair.enabled}
/>
</div>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => removePair(index)}
className="text-red-500 hover:text-red-700 hover:bg-red-50"
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
))}
</div>
<Button
type="button"
variant="outline"
size="sm"
onClick={addPair}
className="w-full"
>
<Plus className="h-4 w-4 mr-2" />
Add {label.toLowerCase()}
</Button>
</div>
);
<Button
type="button"
variant="outline"
size="sm"
onClick={addPair}
className="w-full"
>
<Plus className="h-4 w-4 mr-2" />
Add {label.toLowerCase()}
</Button>
</div>
);
};

View File

@@ -1,164 +1,164 @@
import {
Bell,
Loader2,
Mail,
MessageCircleMore,
PenBoxIcon,
Trash2,
Bell,
Loader2,
Mail,
MessageCircleMore,
PenBoxIcon,
Trash2,
} from "lucide-react";
import { toast } from "sonner";
import {
DiscordIcon,
SlackIcon,
TelegramIcon,
DiscordIcon,
SlackIcon,
TelegramIcon,
} from "@/components/icons/notification-icons";
import { DialogAction } from "@/components/shared/dialog-action";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { api } from "@/utils/api";
import { HandleNotifications } from "./handle-notifications";
export const ShowNotifications = () => {
const { data, isLoading, refetch } = api.notification.all.useQuery();
const { mutateAsync, isLoading: isRemoving } =
api.notification.remove.useMutation();
const { data, isLoading, refetch } = api.notification.all.useQuery();
const { mutateAsync, isLoading: isRemoving } =
api.notification.remove.useMutation();
return (
<div className="w-full">
<Card className="h-full bg-sidebar p-2.5 rounded-xl max-w-5xl mx-auto">
<div className="rounded-xl bg-background shadow-md ">
<CardHeader className="">
<CardTitle className="text-xl flex flex-row gap-2">
<Bell className="size-6 text-muted-foreground self-center" />
Notifications
</CardTitle>
<CardDescription>
Add your providers to receive notifications, like Discord, Slack,
Telegram, Email.
</CardDescription>
</CardHeader>
<CardContent className="space-y-2 py-8 border-t">
{isLoading ? (
<div className="flex flex-row gap-2 items-center justify-center text-sm text-muted-foreground min-h-[25vh]">
<span>Loading...</span>
<Loader2 className="animate-spin size-4" />
</div>
) : (
<>
{data?.length === 0 ? (
<div className="flex flex-col items-center gap-3 min-h-[25vh] justify-center">
<Bell />
<span className="text-base text-muted-foreground text-center">
To send notifications it is required to set at least 1
provider.
</span>
<HandleNotifications />
</div>
) : (
<div className="flex flex-col gap-4 min-h-[25vh]">
<div className="flex flex-col gap-4 rounded-lg ">
{data?.map((notification, _index) => (
<div
key={notification.notificationId}
className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg"
>
<div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full">
<span className="text-sm flex flex-row items-center gap-4">
{notification.notificationType === "slack" && (
<div className="flex items-center justify-center rounded-lg">
<SlackIcon className="size-6" />
</div>
)}
{notification.notificationType === "telegram" && (
<div className="flex items-center justify-center rounded-lg ">
<TelegramIcon className="size-7 " />
</div>
)}
{notification.notificationType === "discord" && (
<div className="flex items-center justify-center rounded-lg">
<DiscordIcon className="size-7 " />
</div>
)}
{notification.notificationType === "email" && (
<div className="flex items-center justify-center rounded-lg ">
<Mail className="size-6 text-muted-foreground" />
</div>
)}
{notification.notificationType === "gotify" && (
<div className="flex items-center justify-center rounded-lg ">
<MessageCircleMore className="size-6 text-muted-foreground" />
</div>
)}
{notification.notificationType === "ntfy" && (
<div className="flex items-center justify-center rounded-lg ">
<MessageCircleMore className="size-6 text-muted-foreground" />
</div>
)}
{notification.notificationType === "custom" && (
<div className="flex items-center justify-center rounded-lg ">
<PenBoxIcon className="size-6 text-muted-foreground" />
</div>
)}
return (
<div className="w-full">
<Card className="h-full bg-sidebar p-2.5 rounded-xl max-w-5xl mx-auto">
<div className="rounded-xl bg-background shadow-md ">
<CardHeader className="">
<CardTitle className="text-xl flex flex-row gap-2">
<Bell className="size-6 text-muted-foreground self-center" />
Notifications
</CardTitle>
<CardDescription>
Add your providers to receive notifications, like Discord, Slack,
Telegram, Email.
</CardDescription>
</CardHeader>
<CardContent className="space-y-2 py-8 border-t">
{isLoading ? (
<div className="flex flex-row gap-2 items-center justify-center text-sm text-muted-foreground min-h-[25vh]">
<span>Loading...</span>
<Loader2 className="animate-spin size-4" />
</div>
) : (
<>
{data?.length === 0 ? (
<div className="flex flex-col items-center gap-3 min-h-[25vh] justify-center">
<Bell />
<span className="text-base text-muted-foreground text-center">
To send notifications it is required to set at least 1
provider.
</span>
<HandleNotifications />
</div>
) : (
<div className="flex flex-col gap-4 min-h-[25vh]">
<div className="flex flex-col gap-4 rounded-lg ">
{data?.map((notification, _index) => (
<div
key={notification.notificationId}
className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg"
>
<div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full">
<span className="text-sm flex flex-row items-center gap-4">
{notification.notificationType === "slack" && (
<div className="flex items-center justify-center rounded-lg">
<SlackIcon className="size-6" />
</div>
)}
{notification.notificationType === "telegram" && (
<div className="flex items-center justify-center rounded-lg ">
<TelegramIcon className="size-7 " />
</div>
)}
{notification.notificationType === "discord" && (
<div className="flex items-center justify-center rounded-lg">
<DiscordIcon className="size-7 " />
</div>
)}
{notification.notificationType === "email" && (
<div className="flex items-center justify-center rounded-lg ">
<Mail className="size-6 text-muted-foreground" />
</div>
)}
{notification.notificationType === "gotify" && (
<div className="flex items-center justify-center rounded-lg ">
<MessageCircleMore className="size-6 text-muted-foreground" />
</div>
)}
{notification.notificationType === "ntfy" && (
<div className="flex items-center justify-center rounded-lg ">
<MessageCircleMore className="size-6 text-muted-foreground" />
</div>
)}
{notification.notificationType === "custom" && (
<div className="flex items-center justify-center rounded-lg ">
<PenBoxIcon className="size-6 text-muted-foreground" />
</div>
)}
{notification.name}
</span>
<div className="flex flex-row gap-1">
<HandleNotifications
notificationId={notification.notificationId}
/>
{notification.name}
</span>
<div className="flex flex-row gap-1">
<HandleNotifications
notificationId={notification.notificationId}
/>
<DialogAction
title="Delete Notification"
description="Are you sure you want to delete this notification?"
type="destructive"
onClick={async () => {
await mutateAsync({
notificationId: notification.notificationId,
})
.then(() => {
toast.success(
"Notification deleted successfully"
);
refetch();
})
.catch(() => {
toast.error(
"Error deleting notification"
);
});
}}
>
<Button
variant="ghost"
size="icon"
className="group hover:bg-red-500/10 "
isLoading={isRemoving}
>
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
</Button>
</DialogAction>
</div>
</div>
</div>
))}
</div>
<DialogAction
title="Delete Notification"
description="Are you sure you want to delete this notification?"
type="destructive"
onClick={async () => {
await mutateAsync({
notificationId: notification.notificationId,
})
.then(() => {
toast.success(
"Notification deleted successfully",
);
refetch();
})
.catch(() => {
toast.error(
"Error deleting notification",
);
});
}}
>
<Button
variant="ghost"
size="icon"
className="group hover:bg-red-500/10 "
isLoading={isRemoving}
>
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
</Button>
</DialogAction>
</div>
</div>
</div>
))}
</div>
<div className="flex flex-row gap-2 flex-wrap w-full justify-end mr-4">
<HandleNotifications />
</div>
</div>
)}
</>
)}
</CardContent>
</div>
</Card>
</div>
);
<div className="flex flex-row gap-2 flex-wrap w-full justify-end mr-4">
<HandleNotifications />
</div>
</div>
)}
</>
)}
</CardContent>
</div>
</Card>
</div>
);
};

File diff suppressed because it is too large Load Diff

View File

@@ -6,400 +6,400 @@ import { z } from "zod";
import { organization } from "./account";
export const notificationType = pgEnum("notificationType", [
"slack",
"telegram",
"discord",
"email",
"gotify",
"ntfy",
"custom",
"slack",
"telegram",
"discord",
"email",
"gotify",
"ntfy",
"custom",
]);
export const notifications = pgTable("notification", {
notificationId: text("notificationId")
.notNull()
.primaryKey()
.$defaultFn(() => nanoid()),
name: text("name").notNull(),
appDeploy: boolean("appDeploy").notNull().default(false),
appBuildError: boolean("appBuildError").notNull().default(false),
databaseBackup: boolean("databaseBackup").notNull().default(false),
dokployRestart: boolean("dokployRestart").notNull().default(false),
dockerCleanup: boolean("dockerCleanup").notNull().default(false),
serverThreshold: boolean("serverThreshold").notNull().default(false),
notificationType: notificationType("notificationType").notNull(),
createdAt: text("createdAt")
.notNull()
.$defaultFn(() => new Date().toISOString()),
slackId: text("slackId").references(() => slack.slackId, {
onDelete: "cascade",
}),
telegramId: text("telegramId").references(() => telegram.telegramId, {
onDelete: "cascade",
}),
discordId: text("discordId").references(() => discord.discordId, {
onDelete: "cascade",
}),
emailId: text("emailId").references(() => email.emailId, {
onDelete: "cascade",
}),
gotifyId: text("gotifyId").references(() => gotify.gotifyId, {
onDelete: "cascade",
}),
ntfyId: text("ntfyId").references(() => ntfy.ntfyId, {
onDelete: "cascade",
}),
customId: text("customId").references(() => custom.customId, {
onDelete: "cascade",
}),
organizationId: text("organizationId")
.notNull()
.references(() => organization.id, { onDelete: "cascade" }),
notificationId: text("notificationId")
.notNull()
.primaryKey()
.$defaultFn(() => nanoid()),
name: text("name").notNull(),
appDeploy: boolean("appDeploy").notNull().default(false),
appBuildError: boolean("appBuildError").notNull().default(false),
databaseBackup: boolean("databaseBackup").notNull().default(false),
dokployRestart: boolean("dokployRestart").notNull().default(false),
dockerCleanup: boolean("dockerCleanup").notNull().default(false),
serverThreshold: boolean("serverThreshold").notNull().default(false),
notificationType: notificationType("notificationType").notNull(),
createdAt: text("createdAt")
.notNull()
.$defaultFn(() => new Date().toISOString()),
slackId: text("slackId").references(() => slack.slackId, {
onDelete: "cascade",
}),
telegramId: text("telegramId").references(() => telegram.telegramId, {
onDelete: "cascade",
}),
discordId: text("discordId").references(() => discord.discordId, {
onDelete: "cascade",
}),
emailId: text("emailId").references(() => email.emailId, {
onDelete: "cascade",
}),
gotifyId: text("gotifyId").references(() => gotify.gotifyId, {
onDelete: "cascade",
}),
ntfyId: text("ntfyId").references(() => ntfy.ntfyId, {
onDelete: "cascade",
}),
customId: text("customId").references(() => custom.customId, {
onDelete: "cascade",
}),
organizationId: text("organizationId")
.notNull()
.references(() => organization.id, { onDelete: "cascade" }),
});
export const slack = pgTable("slack", {
slackId: text("slackId")
.notNull()
.primaryKey()
.$defaultFn(() => nanoid()),
webhookUrl: text("webhookUrl").notNull(),
channel: text("channel"),
slackId: text("slackId")
.notNull()
.primaryKey()
.$defaultFn(() => nanoid()),
webhookUrl: text("webhookUrl").notNull(),
channel: text("channel"),
});
export const telegram = pgTable("telegram", {
telegramId: text("telegramId")
.notNull()
.primaryKey()
.$defaultFn(() => nanoid()),
botToken: text("botToken").notNull(),
chatId: text("chatId").notNull(),
messageThreadId: text("messageThreadId"),
telegramId: text("telegramId")
.notNull()
.primaryKey()
.$defaultFn(() => nanoid()),
botToken: text("botToken").notNull(),
chatId: text("chatId").notNull(),
messageThreadId: text("messageThreadId"),
});
export const discord = pgTable("discord", {
discordId: text("discordId")
.notNull()
.primaryKey()
.$defaultFn(() => nanoid()),
webhookUrl: text("webhookUrl").notNull(),
decoration: boolean("decoration"),
discordId: text("discordId")
.notNull()
.primaryKey()
.$defaultFn(() => nanoid()),
webhookUrl: text("webhookUrl").notNull(),
decoration: boolean("decoration"),
});
export const email = pgTable("email", {
emailId: text("emailId")
.notNull()
.primaryKey()
.$defaultFn(() => nanoid()),
smtpServer: text("smtpServer").notNull(),
smtpPort: integer("smtpPort").notNull(),
username: text("username").notNull(),
password: text("password").notNull(),
fromAddress: text("fromAddress").notNull(),
toAddresses: text("toAddress").array().notNull(),
emailId: text("emailId")
.notNull()
.primaryKey()
.$defaultFn(() => nanoid()),
smtpServer: text("smtpServer").notNull(),
smtpPort: integer("smtpPort").notNull(),
username: text("username").notNull(),
password: text("password").notNull(),
fromAddress: text("fromAddress").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"),
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 ntfy = pgTable("ntfy", {
ntfyId: text("ntfyId")
.notNull()
.primaryKey()
.$defaultFn(() => nanoid()),
serverUrl: text("serverUrl").notNull(),
topic: text("topic").notNull(),
accessToken: text("accessToken").notNull(),
priority: integer("priority").notNull().default(3),
ntfyId: text("ntfyId")
.notNull()
.primaryKey()
.$defaultFn(() => nanoid()),
serverUrl: text("serverUrl").notNull(),
topic: text("topic").notNull(),
accessToken: text("accessToken").notNull(),
priority: integer("priority").notNull().default(3),
});
export const custom = pgTable("custom", {
customId: text("customId")
.notNull()
.primaryKey()
.$defaultFn(() => nanoid()),
endpoint: text("endpoint").notNull(),
headers: text("headers"), // JSON string
customId: text("customId")
.notNull()
.primaryKey()
.$defaultFn(() => nanoid()),
endpoint: text("endpoint").notNull(),
headers: text("headers"), // JSON string
});
export const notificationsRelations = relations(notifications, ({ one }) => ({
slack: one(slack, {
fields: [notifications.slackId],
references: [slack.slackId],
}),
telegram: one(telegram, {
fields: [notifications.telegramId],
references: [telegram.telegramId],
}),
discord: one(discord, {
fields: [notifications.discordId],
references: [discord.discordId],
}),
email: one(email, {
fields: [notifications.emailId],
references: [email.emailId],
}),
gotify: one(gotify, {
fields: [notifications.gotifyId],
references: [gotify.gotifyId],
}),
ntfy: one(ntfy, {
fields: [notifications.ntfyId],
references: [ntfy.ntfyId],
}),
custom: one(custom, {
fields: [notifications.customId],
references: [custom.customId],
}),
organization: one(organization, {
fields: [notifications.organizationId],
references: [organization.id],
}),
slack: one(slack, {
fields: [notifications.slackId],
references: [slack.slackId],
}),
telegram: one(telegram, {
fields: [notifications.telegramId],
references: [telegram.telegramId],
}),
discord: one(discord, {
fields: [notifications.discordId],
references: [discord.discordId],
}),
email: one(email, {
fields: [notifications.emailId],
references: [email.emailId],
}),
gotify: one(gotify, {
fields: [notifications.gotifyId],
references: [gotify.gotifyId],
}),
ntfy: one(ntfy, {
fields: [notifications.ntfyId],
references: [ntfy.ntfyId],
}),
custom: one(custom, {
fields: [notifications.customId],
references: [custom.customId],
}),
organization: one(organization, {
fields: [notifications.organizationId],
references: [organization.id],
}),
}));
export const notificationsSchema = createInsertSchema(notifications);
export const apiCreateSlack = notificationsSchema
.pick({
appBuildError: true,
databaseBackup: true,
dokployRestart: true,
name: true,
appDeploy: true,
dockerCleanup: true,
serverThreshold: true,
})
.extend({
webhookUrl: z.string().min(1),
channel: z.string(),
})
.required();
.pick({
appBuildError: true,
databaseBackup: true,
dokployRestart: true,
name: true,
appDeploy: true,
dockerCleanup: true,
serverThreshold: true,
})
.extend({
webhookUrl: z.string().min(1),
channel: z.string(),
})
.required();
export const apiUpdateSlack = apiCreateSlack.partial().extend({
notificationId: z.string().min(1),
slackId: z.string(),
organizationId: z.string().optional(),
notificationId: z.string().min(1),
slackId: z.string(),
organizationId: z.string().optional(),
});
export const apiTestSlackConnection = apiCreateSlack.pick({
webhookUrl: true,
channel: true,
webhookUrl: true,
channel: true,
});
export const apiCreateTelegram = notificationsSchema
.pick({
appBuildError: true,
databaseBackup: true,
dokployRestart: true,
name: true,
appDeploy: true,
dockerCleanup: true,
serverThreshold: true,
})
.extend({
botToken: z.string().min(1),
chatId: z.string().min(1),
messageThreadId: z.string(),
})
.required();
.pick({
appBuildError: true,
databaseBackup: true,
dokployRestart: true,
name: true,
appDeploy: true,
dockerCleanup: true,
serverThreshold: true,
})
.extend({
botToken: z.string().min(1),
chatId: z.string().min(1),
messageThreadId: z.string(),
})
.required();
export const apiUpdateTelegram = apiCreateTelegram.partial().extend({
notificationId: z.string().min(1),
telegramId: z.string().min(1),
organizationId: z.string().optional(),
notificationId: z.string().min(1),
telegramId: z.string().min(1),
organizationId: z.string().optional(),
});
export const apiTestTelegramConnection = apiCreateTelegram.pick({
botToken: true,
chatId: true,
messageThreadId: true,
botToken: true,
chatId: true,
messageThreadId: true,
});
export const apiCreateDiscord = notificationsSchema
.pick({
appBuildError: true,
databaseBackup: true,
dokployRestart: true,
name: true,
appDeploy: true,
dockerCleanup: true,
serverThreshold: true,
})
.extend({
webhookUrl: z.string().min(1),
decoration: z.boolean(),
})
.required();
.pick({
appBuildError: true,
databaseBackup: true,
dokployRestart: true,
name: true,
appDeploy: true,
dockerCleanup: true,
serverThreshold: true,
})
.extend({
webhookUrl: z.string().min(1),
decoration: z.boolean(),
})
.required();
export const apiUpdateDiscord = apiCreateDiscord.partial().extend({
notificationId: z.string().min(1),
discordId: z.string().min(1),
organizationId: z.string().optional(),
notificationId: z.string().min(1),
discordId: z.string().min(1),
organizationId: z.string().optional(),
});
export const apiTestDiscordConnection = apiCreateDiscord
.pick({
webhookUrl: true,
})
.extend({
decoration: z.boolean().optional(),
});
.pick({
webhookUrl: true,
})
.extend({
decoration: z.boolean().optional(),
});
export const apiCreateEmail = notificationsSchema
.pick({
appBuildError: true,
databaseBackup: true,
dokployRestart: true,
name: true,
appDeploy: true,
dockerCleanup: true,
serverThreshold: true,
})
.extend({
smtpServer: z.string().min(1),
smtpPort: z.number().min(1),
username: z.string().min(1),
password: z.string().min(1),
fromAddress: z.string().min(1),
toAddresses: z.array(z.string()).min(1),
})
.required();
.pick({
appBuildError: true,
databaseBackup: true,
dokployRestart: true,
name: true,
appDeploy: true,
dockerCleanup: true,
serverThreshold: true,
})
.extend({
smtpServer: z.string().min(1),
smtpPort: z.number().min(1),
username: z.string().min(1),
password: z.string().min(1),
fromAddress: z.string().min(1),
toAddresses: z.array(z.string()).min(1),
})
.required();
export const apiUpdateEmail = apiCreateEmail.partial().extend({
notificationId: z.string().min(1),
emailId: z.string().min(1),
organizationId: z.string().optional(),
notificationId: z.string().min(1),
emailId: z.string().min(1),
organizationId: z.string().optional(),
});
export const apiTestEmailConnection = apiCreateEmail.pick({
smtpServer: true,
smtpPort: true,
username: true,
password: true,
toAddresses: true,
fromAddress: true,
smtpServer: true,
smtpPort: true,
username: true,
password: true,
toAddresses: 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();
.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),
organizationId: z.string().optional(),
notificationId: z.string().min(1),
gotifyId: z.string().min(1),
organizationId: z.string().optional(),
});
export const apiTestGotifyConnection = apiCreateGotify
.pick({
serverUrl: true,
appToken: true,
priority: true,
})
.extend({
decoration: z.boolean().optional(),
});
.pick({
serverUrl: true,
appToken: true,
priority: true,
})
.extend({
decoration: z.boolean().optional(),
});
export const apiCreateNtfy = notificationsSchema
.pick({
appBuildError: true,
databaseBackup: true,
dokployRestart: true,
name: true,
appDeploy: true,
dockerCleanup: true,
})
.extend({
serverUrl: z.string().min(1),
topic: z.string().min(1),
accessToken: z.string().min(1),
priority: z.number().min(1),
})
.required();
.pick({
appBuildError: true,
databaseBackup: true,
dokployRestart: true,
name: true,
appDeploy: true,
dockerCleanup: true,
})
.extend({
serverUrl: z.string().min(1),
topic: z.string().min(1),
accessToken: z.string().min(1),
priority: z.number().min(1),
})
.required();
export const apiUpdateNtfy = apiCreateNtfy.partial().extend({
notificationId: z.string().min(1),
ntfyId: z.string().min(1),
organizationId: z.string().optional(),
notificationId: z.string().min(1),
ntfyId: z.string().min(1),
organizationId: z.string().optional(),
});
export const apiTestNtfyConnection = apiCreateNtfy.pick({
serverUrl: true,
topic: true,
accessToken: true,
priority: true,
serverUrl: true,
topic: true,
accessToken: true,
priority: true,
});
export const apiFindOneNotification = notificationsSchema
.pick({
notificationId: true,
})
.required();
.pick({
notificationId: true,
})
.required();
export const apiCreateCustom = notificationsSchema
.pick({
appBuildError: true,
databaseBackup: true,
dokployRestart: true,
name: true,
appDeploy: true,
dockerCleanup: true,
serverThreshold: true,
})
.extend({
endpoint: z.string().min(1),
headers: z.string().optional(),
});
.pick({
appBuildError: true,
databaseBackup: true,
dokployRestart: true,
name: true,
appDeploy: true,
dockerCleanup: true,
serverThreshold: true,
})
.extend({
endpoint: z.string().min(1),
headers: z.string().optional(),
});
export const apiUpdateCustom = apiCreateCustom.partial().extend({
notificationId: z.string().min(1),
customId: z.string().min(1),
organizationId: z.string().optional(),
notificationId: z.string().min(1),
customId: z.string().min(1),
organizationId: z.string().optional(),
});
export const apiTestCustomConnection = z.object({
endpoint: z.string().min(1),
headers: z.string().optional(),
endpoint: z.string().min(1),
headers: z.string().optional(),
});
export const apiSendTest = notificationsSchema
.extend({
botToken: z.string(),
chatId: z.string(),
webhookUrl: z.string(),
channel: z.string(),
smtpServer: z.string(),
smtpPort: z.number(),
fromAddress: z.string(),
username: z.string(),
password: z.string(),
toAddresses: z.array(z.string()),
serverUrl: z.string(),
topic: z.string(),
appToken: z.string(),
accessToken: z.string(),
priority: z.number(),
endpoint: z.string(),
headers: z.string(),
})
.partial();
.extend({
botToken: z.string(),
chatId: z.string(),
webhookUrl: z.string(),
channel: z.string(),
smtpServer: z.string(),
smtpPort: z.number(),
fromAddress: z.string(),
username: z.string(),
password: z.string(),
toAddresses: z.array(z.string()),
serverUrl: z.string(),
topic: z.string(),
appToken: z.string(),
accessToken: z.string(),
priority: z.number(),
endpoint: z.string(),
headers: z.string(),
})
.partial();

File diff suppressed because it is too large Load Diff

View File

@@ -5,236 +5,236 @@ import { renderAsync } from "@react-email/components";
import { format } from "date-fns";
import { and, eq } from "drizzle-orm";
import {
sendCustomNotification,
sendDiscordNotification,
sendEmailNotification,
sendGotifyNotification,
sendNtfyNotification,
sendSlackNotification,
sendTelegramNotification,
sendCustomNotification,
sendDiscordNotification,
sendEmailNotification,
sendGotifyNotification,
sendNtfyNotification,
sendSlackNotification,
sendTelegramNotification,
} from "./utils";
interface Props {
projectName: string;
applicationName: string;
applicationType: string;
errorMessage: string;
buildLink: string;
organizationId: string;
projectName: string;
applicationName: string;
applicationType: string;
errorMessage: string;
buildLink: string;
organizationId: string;
}
export const sendBuildErrorNotifications = async ({
projectName,
applicationName,
applicationType,
errorMessage,
buildLink,
organizationId,
projectName,
applicationName,
applicationType,
errorMessage,
buildLink,
organizationId,
}: Props) => {
const date = new Date();
const unixDate = ~~(Number(date) / 1000);
const notificationList = await db.query.notifications.findMany({
where: and(
eq(notifications.appBuildError, true),
eq(notifications.organizationId, organizationId)
),
with: {
email: true,
discord: true,
telegram: true,
slack: true,
gotify: true,
ntfy: true,
custom: true,
},
});
const date = new Date();
const unixDate = ~~(Number(date) / 1000);
const notificationList = await db.query.notifications.findMany({
where: and(
eq(notifications.appBuildError, true),
eq(notifications.organizationId, organizationId),
),
with: {
email: true,
discord: true,
telegram: true,
slack: true,
gotify: true,
ntfy: true,
custom: true,
},
});
for (const notification of notificationList) {
const { email, discord, telegram, slack, gotify, ntfy, custom } =
notification;
if (email) {
const template = await renderAsync(
BuildFailedEmail({
projectName,
applicationName,
applicationType,
errorMessage: errorMessage,
buildLink,
date: date.toLocaleString(),
})
).catch();
await sendEmailNotification(email, "Build failed for dokploy", template);
}
for (const notification of notificationList) {
const { email, discord, telegram, slack, gotify, ntfy, custom } =
notification;
if (email) {
const template = await renderAsync(
BuildFailedEmail({
projectName,
applicationName,
applicationType,
errorMessage: errorMessage,
buildLink,
date: date.toLocaleString(),
}),
).catch();
await sendEmailNotification(email, "Build failed for dokploy", template);
}
if (discord) {
const decorate = (decoration: string, text: string) =>
`${discord.decoration ? decoration : ""} ${text}`.trim();
if (discord) {
const decorate = (decoration: string, text: string) =>
`${discord.decoration ? decoration : ""} ${text}`.trim();
const limitCharacter = 800;
const truncatedErrorMessage = errorMessage.substring(0, limitCharacter);
await sendDiscordNotification(discord, {
title: decorate(">", "`⚠️` Build Failed"),
color: 0xed4245,
fields: [
{
name: decorate("`🛠️`", "Project"),
value: projectName,
inline: true,
},
{
name: decorate("`⚙️`", "Application"),
value: applicationName,
inline: true,
},
{
name: decorate("`❔`", "Type"),
value: applicationType,
inline: true,
},
{
name: decorate("`📅`", "Date"),
value: `<t:${unixDate}:D>`,
inline: true,
},
{
name: decorate("`⌚`", "Time"),
value: `<t:${unixDate}:t>`,
inline: true,
},
{
name: decorate("`❓`", "Type"),
value: "Failed",
inline: true,
},
{
name: decorate("`⚠️`", "Error Message"),
value: `\`\`\`${truncatedErrorMessage}\`\`\``,
},
{
name: decorate("`🧷`", "Build Link"),
value: `[Click here to access build link](${buildLink})`,
},
],
timestamp: date.toISOString(),
footer: {
text: "Dokploy Build Notification",
},
});
}
const limitCharacter = 800;
const truncatedErrorMessage = errorMessage.substring(0, limitCharacter);
await sendDiscordNotification(discord, {
title: decorate(">", "`⚠️` Build Failed"),
color: 0xed4245,
fields: [
{
name: decorate("`🛠️`", "Project"),
value: projectName,
inline: true,
},
{
name: decorate("`⚙️`", "Application"),
value: applicationName,
inline: true,
},
{
name: decorate("`❔`", "Type"),
value: applicationType,
inline: true,
},
{
name: decorate("`📅`", "Date"),
value: `<t:${unixDate}:D>`,
inline: true,
},
{
name: decorate("`⌚`", "Time"),
value: `<t:${unixDate}:t>`,
inline: true,
},
{
name: decorate("`❓`", "Type"),
value: "Failed",
inline: true,
},
{
name: decorate("`⚠️`", "Error Message"),
value: `\`\`\`${truncatedErrorMessage}\`\`\``,
},
{
name: decorate("`🧷`", "Build Link"),
value: `[Click here to access build link](${buildLink})`,
},
],
timestamp: date.toISOString(),
footer: {
text: "Dokploy Build Notification",
},
});
}
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 (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 (ntfy) {
await sendNtfyNotification(
ntfy,
"Build Failed",
"warning",
`view, Build details, ${buildLink}, clear=true;`,
`🛠Project: ${projectName}\n` +
`Application: ${applicationName}\n` +
`❔Type: ${applicationType}\n` +
`🕒Date: ${date.toLocaleString()}\n` +
`Error:\n${errorMessage}`
);
}
if (ntfy) {
await sendNtfyNotification(
ntfy,
"Build Failed",
"warning",
`view, Build details, ${buildLink}, clear=true;`,
`🛠Project: ${projectName}\n` +
`Application: ${applicationName}\n` +
`❔Type: ${applicationType}\n` +
`🕒Date: ${date.toLocaleString()}\n` +
`Error:\n${errorMessage}`,
);
}
if (telegram) {
const inlineButton = [
[
{
text: "Deployment Logs",
url: buildLink,
},
],
];
if (telegram) {
const inlineButton = [
[
{
text: "Deployment Logs",
url: buildLink,
},
],
];
await sendTelegramNotification(
telegram,
`<b>⚠️ Build Failed</b>\n\n<b>Project:</b> ${projectName}\n<b>Application:</b> ${applicationName}\n<b>Type:</b> ${applicationType}\n<b>Date:</b> ${format(
date,
"PP"
)}\n<b>Time:</b> ${format(
date,
"pp"
)}\n\n<b>Error:</b>\n<pre>${errorMessage}</pre>`,
inlineButton
);
}
await sendTelegramNotification(
telegram,
`<b>⚠️ Build Failed</b>\n\n<b>Project:</b> ${projectName}\n<b>Application:</b> ${applicationName}\n<b>Type:</b> ${applicationType}\n<b>Date:</b> ${format(
date,
"PP",
)}\n<b>Time:</b> ${format(
date,
"pp",
)}\n\n<b>Error:</b>\n<pre>${errorMessage}</pre>`,
inlineButton,
);
}
if (slack) {
const { channel } = slack;
await sendSlackNotification(slack, {
channel: channel,
attachments: [
{
color: "#FF0000",
pretext: ":warning: *Build Failed*",
fields: [
{
title: "Project",
value: projectName,
short: true,
},
{
title: "Application",
value: applicationName,
short: true,
},
{
title: "Type",
value: applicationType,
short: true,
},
{
title: "Time",
value: date.toLocaleString(),
short: true,
},
{
title: "Error",
value: `\`\`\`${errorMessage}\`\`\``,
short: false,
},
],
actions: [
{
type: "button",
text: "View Build Details",
url: buildLink,
},
],
},
],
});
}
if (slack) {
const { channel } = slack;
await sendSlackNotification(slack, {
channel: channel,
attachments: [
{
color: "#FF0000",
pretext: ":warning: *Build Failed*",
fields: [
{
title: "Project",
value: projectName,
short: true,
},
{
title: "Application",
value: applicationName,
short: true,
},
{
title: "Type",
value: applicationType,
short: true,
},
{
title: "Time",
value: date.toLocaleString(),
short: true,
},
{
title: "Error",
value: `\`\`\`${errorMessage}\`\`\``,
short: false,
},
],
actions: [
{
type: "button",
text: "View Build Details",
url: buildLink,
},
],
},
],
});
}
if (custom) {
await sendCustomNotification(custom, {
title: "Build Error",
message: "Build failed with errors",
projectName,
applicationName,
applicationType,
errorMessage,
buildLink,
timestamp: date.toISOString(),
date: date.toLocaleString(),
status: "error",
type: "build",
});
}
}
if (custom) {
await sendCustomNotification(custom, {
title: "Build Error",
message: "Build failed with errors",
projectName,
applicationName,
applicationType,
errorMessage,
buildLink,
timestamp: date.toISOString(),
date: date.toLocaleString(),
status: "error",
type: "build",
});
}
}
};

View File

@@ -6,231 +6,231 @@ import { renderAsync } from "@react-email/components";
import { format } from "date-fns";
import { and, eq } from "drizzle-orm";
import {
sendCustomNotification,
sendDiscordNotification,
sendEmailNotification,
sendGotifyNotification,
sendNtfyNotification,
sendSlackNotification,
sendTelegramNotification,
sendCustomNotification,
sendDiscordNotification,
sendEmailNotification,
sendGotifyNotification,
sendNtfyNotification,
sendSlackNotification,
sendTelegramNotification,
} from "./utils";
interface Props {
projectName: string;
applicationName: string;
applicationType: string;
buildLink: string;
organizationId: string;
domains: Domain[];
projectName: string;
applicationName: string;
applicationType: string;
buildLink: string;
organizationId: string;
domains: Domain[];
}
export const sendBuildSuccessNotifications = async ({
projectName,
applicationName,
applicationType,
buildLink,
organizationId,
domains,
projectName,
applicationName,
applicationType,
buildLink,
organizationId,
domains,
}: Props) => {
const date = new Date();
const unixDate = ~~(Number(date) / 1000);
const notificationList = await db.query.notifications.findMany({
where: and(
eq(notifications.appDeploy, true),
eq(notifications.organizationId, organizationId)
),
with: {
email: true,
discord: true,
telegram: true,
slack: true,
gotify: true,
ntfy: true,
custom: true,
},
});
const date = new Date();
const unixDate = ~~(Number(date) / 1000);
const notificationList = await db.query.notifications.findMany({
where: and(
eq(notifications.appDeploy, true),
eq(notifications.organizationId, organizationId),
),
with: {
email: true,
discord: true,
telegram: true,
slack: true,
gotify: true,
ntfy: true,
custom: true,
},
});
for (const notification of notificationList) {
const { email, discord, telegram, slack, gotify, ntfy, custom } =
notification;
for (const notification of notificationList) {
const { email, discord, telegram, slack, gotify, ntfy, custom } =
notification;
if (email) {
const template = await renderAsync(
BuildSuccessEmail({
projectName,
applicationName,
applicationType,
buildLink,
date: date.toLocaleString(),
})
).catch();
await sendEmailNotification(email, "Build success for dokploy", template);
}
if (email) {
const template = await renderAsync(
BuildSuccessEmail({
projectName,
applicationName,
applicationType,
buildLink,
date: date.toLocaleString(),
}),
).catch();
await sendEmailNotification(email, "Build success for dokploy", template);
}
if (discord) {
const decorate = (decoration: string, text: string) =>
`${discord.decoration ? decoration : ""} ${text}`.trim();
if (discord) {
const decorate = (decoration: string, text: string) =>
`${discord.decoration ? decoration : ""} ${text}`.trim();
await sendDiscordNotification(discord, {
title: decorate(">", "`✅` Build Success"),
color: 0x57f287,
fields: [
{
name: decorate("`🛠️`", "Project"),
value: projectName,
inline: true,
},
{
name: decorate("`⚙️`", "Application"),
value: applicationName,
inline: true,
},
{
name: decorate("`❔`", "Type"),
value: applicationType,
inline: true,
},
{
name: decorate("`📅`", "Date"),
value: `<t:${unixDate}:D>`,
inline: true,
},
{
name: decorate("`⌚`", "Time"),
value: `<t:${unixDate}:t>`,
inline: true,
},
{
name: decorate("`❓`", "Type"),
value: "Successful",
inline: true,
},
{
name: decorate("`🧷`", "Build Link"),
value: `[Click here to access build link](${buildLink})`,
},
],
timestamp: date.toISOString(),
footer: {
text: "Dokploy Build Notification",
},
});
}
await sendDiscordNotification(discord, {
title: decorate(">", "`✅` Build Success"),
color: 0x57f287,
fields: [
{
name: decorate("`🛠️`", "Project"),
value: projectName,
inline: true,
},
{
name: decorate("`⚙️`", "Application"),
value: applicationName,
inline: true,
},
{
name: decorate("`❔`", "Type"),
value: applicationType,
inline: true,
},
{
name: decorate("`📅`", "Date"),
value: `<t:${unixDate}:D>`,
inline: true,
},
{
name: decorate("`⌚`", "Time"),
value: `<t:${unixDate}:t>`,
inline: true,
},
{
name: decorate("`❓`", "Type"),
value: "Successful",
inline: true,
},
{
name: decorate("`🧷`", "Build Link"),
value: `[Click here to access build link](${buildLink})`,
},
],
timestamp: date.toISOString(),
footer: {
text: "Dokploy Build Notification",
},
});
}
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 (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 (ntfy) {
await sendNtfyNotification(
ntfy,
"Build Success",
"white_check_mark",
`view, Build details, ${buildLink}, clear=true;`,
`🛠Project: ${projectName}\n` +
`Application: ${applicationName}\n` +
`❔Type: ${applicationType}\n` +
`🕒Date: ${date.toLocaleString()}`
);
}
if (ntfy) {
await sendNtfyNotification(
ntfy,
"Build Success",
"white_check_mark",
`view, Build details, ${buildLink}, clear=true;`,
`🛠Project: ${projectName}\n` +
`Application: ${applicationName}\n` +
`❔Type: ${applicationType}\n` +
`🕒Date: ${date.toLocaleString()}`,
);
}
if (telegram) {
const chunkArray = <T>(array: T[], chunkSize: number): T[][] =>
Array.from({ length: Math.ceil(array.length / chunkSize) }, (_, i) =>
array.slice(i * chunkSize, i * chunkSize + chunkSize)
);
if (telegram) {
const chunkArray = <T>(array: T[], chunkSize: number): T[][] =>
Array.from({ length: Math.ceil(array.length / chunkSize) }, (_, i) =>
array.slice(i * chunkSize, i * chunkSize + chunkSize),
);
const inlineButton = [
[
{
text: "Deployment Logs",
url: buildLink,
},
],
...chunkArray(domains, 2).map((chunk) =>
chunk.map((data) => ({
text: data.host,
url: `${data.https ? "https" : "http"}://${data.host}`,
}))
),
];
const inlineButton = [
[
{
text: "Deployment Logs",
url: buildLink,
},
],
...chunkArray(domains, 2).map((chunk) =>
chunk.map((data) => ({
text: data.host,
url: `${data.https ? "https" : "http"}://${data.host}`,
})),
),
];
await sendTelegramNotification(
telegram,
`<b>✅ Build Success</b>\n\n<b>Project:</b> ${projectName}\n<b>Application:</b> ${applicationName}\n<b>Type:</b> ${applicationType}\n<b>Date:</b> ${format(
date,
"PP"
)}\n<b>Time:</b> ${format(date, "pp")}`,
inlineButton
);
}
await sendTelegramNotification(
telegram,
`<b>✅ Build Success</b>\n\n<b>Project:</b> ${projectName}\n<b>Application:</b> ${applicationName}\n<b>Type:</b> ${applicationType}\n<b>Date:</b> ${format(
date,
"PP",
)}\n<b>Time:</b> ${format(date, "pp")}`,
inlineButton,
);
}
if (slack) {
const { channel } = slack;
await sendSlackNotification(slack, {
channel: channel,
attachments: [
{
color: "#00FF00",
pretext: ":white_check_mark: *Build Success*",
fields: [
{
title: "Project",
value: projectName,
short: true,
},
{
title: "Application",
value: applicationName,
short: true,
},
{
title: "Type",
value: applicationType,
short: true,
},
{
title: "Time",
value: date.toLocaleString(),
short: true,
},
],
actions: [
{
type: "button",
text: "View Build Details",
url: buildLink,
},
],
},
],
});
}
if (slack) {
const { channel } = slack;
await sendSlackNotification(slack, {
channel: channel,
attachments: [
{
color: "#00FF00",
pretext: ":white_check_mark: *Build Success*",
fields: [
{
title: "Project",
value: projectName,
short: true,
},
{
title: "Application",
value: applicationName,
short: true,
},
{
title: "Type",
value: applicationType,
short: true,
},
{
title: "Time",
value: date.toLocaleString(),
short: true,
},
],
actions: [
{
type: "button",
text: "View Build Details",
url: buildLink,
},
],
},
],
});
}
if (custom) {
await sendCustomNotification(custom, {
title: "Build Success",
message: "Build completed successfully",
projectName,
applicationName,
applicationType,
buildLink,
timestamp: date.toISOString(),
date: date.toLocaleString(),
domains: domains.map((domain) => domain.host).join(", "),
status: "success",
type: "build",
});
}
}
if (custom) {
await sendCustomNotification(custom, {
title: "Build Success",
message: "Build completed successfully",
projectName,
applicationName,
applicationType,
buildLink,
timestamp: date.toISOString(),
date: date.toLocaleString(),
domains: domains.map((domain) => domain.host).join(", "),
status: "success",
type: "build",
});
}
}
};

View File

@@ -50,7 +50,8 @@ export const sendDatabaseBackupNotifications = async ({
});
for (const notification of notificationList) {
const { email, discord, telegram, slack, gotify, ntfy, custom } = notification;
const { email, discord, telegram, slack, gotify, ntfy, custom } =
notification;
if (email) {
const template = await renderAsync(
@@ -244,7 +245,10 @@ export const sendDatabaseBackupNotifications = async ({
if (custom) {
await sendCustomNotification(custom, {
title: `Database Backup ${type === "success" ? "Successful" : "Failed"}`,
message: type === "success" ? "Database backup completed successfully" : "Database backup failed",
message:
type === "success"
? "Database backup completed successfully"
: "Database backup failed",
projectName,
applicationName,
databaseType,

View File

@@ -37,7 +37,8 @@ export const sendDockerCleanupNotifications = async (
});
for (const notification of notificationList) {
const { email, discord, telegram, slack, gotify, ntfy, custom } = notification;
const { email, discord, telegram, slack, gotify, ntfy, custom } =
notification;
if (email) {
const template = await renderAsync(

View File

@@ -31,7 +31,8 @@ export const sendDokployRestartNotifications = async () => {
});
for (const notification of notificationList) {
const { email, discord, telegram, slack, gotify, ntfy, custom } = notification;
const { email, discord, telegram, slack, gotify, ntfy, custom } =
notification;
if (email) {
const template = await renderAsync(

View File

@@ -1,193 +1,193 @@
import type {
custom,
discord,
email,
gotify,
ntfy,
slack,
telegram,
custom,
discord,
email,
gotify,
ntfy,
slack,
telegram,
} from "@dokploy/server/db/schema";
import nodemailer from "nodemailer";
export const sendEmailNotification = async (
connection: typeof email.$inferInsert,
subject: string,
htmlContent: string
connection: typeof email.$inferInsert,
subject: string,
htmlContent: string,
) => {
try {
const {
smtpServer,
smtpPort,
username,
password,
fromAddress,
toAddresses,
} = connection;
const transporter = nodemailer.createTransport({
host: smtpServer,
port: smtpPort,
auth: { user: username, pass: password },
});
try {
const {
smtpServer,
smtpPort,
username,
password,
fromAddress,
toAddresses,
} = connection;
const transporter = nodemailer.createTransport({
host: smtpServer,
port: smtpPort,
auth: { user: username, pass: password },
});
await transporter.sendMail({
from: fromAddress,
to: toAddresses.join(", "),
subject,
html: htmlContent,
});
} catch (err) {
console.log(err);
}
await transporter.sendMail({
from: fromAddress,
to: toAddresses.join(", "),
subject,
html: htmlContent,
});
} catch (err) {
console.log(err);
}
};
export const sendDiscordNotification = async (
connection: typeof discord.$inferInsert,
embed: any
connection: typeof discord.$inferInsert,
embed: any,
) => {
// try {
await fetch(connection.webhookUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ embeds: [embed] }),
});
// } catch (err) {
// console.log(err);
// }
// try {
await fetch(connection.webhookUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ embeds: [embed] }),
});
// } catch (err) {
// console.log(err);
// }
};
export const sendTelegramNotification = async (
connection: typeof telegram.$inferInsert,
messageText: string,
inlineButton?: {
text: string;
url: string;
}[][]
connection: typeof telegram.$inferInsert,
messageText: string,
inlineButton?: {
text: string;
url: string;
}[][],
) => {
try {
const url = `https://api.telegram.org/bot${connection.botToken}/sendMessage`;
await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
chat_id: connection.chatId,
message_thread_id: connection.messageThreadId,
text: messageText,
parse_mode: "HTML",
disable_web_page_preview: true,
reply_markup: {
inline_keyboard: inlineButton,
},
}),
});
} catch (err) {
console.log(err);
}
try {
const url = `https://api.telegram.org/bot${connection.botToken}/sendMessage`;
await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
chat_id: connection.chatId,
message_thread_id: connection.messageThreadId,
text: messageText,
parse_mode: "HTML",
disable_web_page_preview: true,
reply_markup: {
inline_keyboard: inlineButton,
},
}),
});
} catch (err) {
console.log(err);
}
};
export const sendSlackNotification = async (
connection: typeof slack.$inferInsert,
message: any
connection: typeof slack.$inferInsert,
message: any,
) => {
try {
await fetch(connection.webhookUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(message),
});
} catch (err) {
console.log(err);
}
try {
await fetch(connection.webhookUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(message),
});
} catch (err) {
console.log(err);
}
};
export const sendGotifyNotification = async (
connection: typeof gotify.$inferInsert,
title: string,
message: string
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",
},
},
}),
});
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}`
);
}
if (!response.ok) {
throw new Error(
`Failed to send Gotify notification: ${response.statusText}`,
);
}
};
export const sendNtfyNotification = async (
connection: typeof ntfy.$inferInsert,
title: string,
tags: string,
actions: string,
message: string
connection: typeof ntfy.$inferInsert,
title: string,
tags: string,
actions: string,
message: string,
) => {
const response = await fetch(`${connection.serverUrl}/${connection.topic}`, {
method: "POST",
headers: {
Authorization: `Bearer ${connection.accessToken}`,
"X-Priority": connection.priority?.toString() || "3",
"X-Title": title,
"X-Tags": tags,
"X-Actions": actions,
},
body: message,
});
const response = await fetch(`${connection.serverUrl}/${connection.topic}`, {
method: "POST",
headers: {
Authorization: `Bearer ${connection.accessToken}`,
"X-Priority": connection.priority?.toString() || "3",
"X-Title": title,
"X-Tags": tags,
"X-Actions": actions,
},
body: message,
});
if (!response.ok) {
throw new Error(`Failed to send ntfy notification: ${response.statusText}`);
}
if (!response.ok) {
throw new Error(`Failed to send ntfy notification: ${response.statusText}`);
}
};
export const sendCustomNotification = async (
connection: typeof custom.$inferInsert,
payload: Record<string, any>
connection: typeof custom.$inferInsert,
payload: Record<string, any>,
) => {
try {
// Parse headers if provided
let headers: Record<string, string> = {
"Content-Type": "application/json",
};
if (connection.headers) {
try {
headers = { ...headers, ...JSON.parse(connection.headers) };
} catch (error) {
console.error("Error parsing headers:", error);
}
}
try {
// Parse headers if provided
let headers: Record<string, string> = {
"Content-Type": "application/json",
};
if (connection.headers) {
try {
headers = { ...headers, ...JSON.parse(connection.headers) };
} catch (error) {
console.error("Error parsing headers:", error);
}
}
// Default body with payload
const body = JSON.stringify(payload);
// Default body with payload
const body = JSON.stringify(payload);
const response = await fetch(connection.endpoint, {
method: "POST",
headers,
body,
});
const response = await fetch(connection.endpoint, {
method: "POST",
headers,
body,
});
if (!response.ok) {
throw new Error(
`Failed to send custom notification: ${response.statusText}`
);
}
if (!response.ok) {
throw new Error(
`Failed to send custom notification: ${response.statusText}`,
);
}
return response;
} catch (error) {
console.error("Error sending custom notification:", error);
throw error;
}
return response;
} catch (error) {
console.error("Error sending custom notification:", error);
throw error;
}
};