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,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;
}
};