mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-07-04 05:25:22 +02:00
feat: add mattermost notification provider
Add comprehensive Mattermost integration as a new notification provider: ## Backend Implementation: - Add `mattermost` to notificationType enum and database schema - Create mattermost table with webhookUrl, channel, username fields - Implement CRUD operations: createMattermostNotification, updateMattermostNotification - Add API routes: createMattermost, updateMattermost, testMattermostConnection - Add sendMattermostNotification utility with proper payload formatting ## Frontend Implementation: - Add MattermostIcon component with provided SVG logo - Extend notification form with Mattermost schema validation - Add webhook URL (required), channel and username (optional) form fields - Integrate test connection functionality - Add Mattermost to provider selection UI ## Notification Integration: - Integrate across all notification types: - Build success/error notifications - Database backup notifications - Docker cleanup notifications - Dokploy restart notifications - Server threshold alerts - Format messages using Markdown for Mattermost compatibility - Handle optional channel (#prefix) and username override - Graceful fallback for empty optional fields ## Features: - Webhook-based messaging to Mattermost channels - Optional channel targeting and custom username display - Consistent formatting with other notification providers - Full CRUD support with proper validation - Test connection capability Closes: Support for Mattermost team communication platform # Conflicts: # apps/dokploy/components/dashboard/settings/notifications/handle-notifications.tsx # apps/dokploy/components/icons/notification-icons.tsx # apps/dokploy/server/api/routers/notification.ts # packages/server/src/db/schema/notification.ts # packages/server/src/services/notification.ts # packages/server/src/utils/notifications/build-error.ts # packages/server/src/utils/notifications/build-success.ts # packages/server/src/utils/notifications/database-backup.ts # packages/server/src/utils/notifications/docker-cleanup.ts # packages/server/src/utils/notifications/dokploy-restart.ts # packages/server/src/utils/notifications/server-threshold.ts # packages/server/src/utils/notifications/utils.ts
This commit is contained in:
@@ -12,6 +12,7 @@ export const notificationType = pgEnum("notificationType", [
|
||||
"email",
|
||||
"gotify",
|
||||
"ntfy",
|
||||
"mattermost",
|
||||
"lark",
|
||||
]);
|
||||
|
||||
@@ -49,6 +50,9 @@ export const notifications = pgTable("notification", {
|
||||
ntfyId: text("ntfyId").references(() => ntfy.ntfyId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
mattermostId: text("mattermostId").references(() => mattermost.mattermostId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
larkId: text("larkId").references(() => lark.larkId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
@@ -120,6 +124,16 @@ export const ntfy = pgTable("ntfy", {
|
||||
priority: integer("priority").notNull().default(3),
|
||||
});
|
||||
|
||||
export const mattermost = pgTable("mattermost", {
|
||||
mattermostId: text("mattermostId")
|
||||
.notNull()
|
||||
.primaryKey()
|
||||
.$defaultFn(() => nanoid()),
|
||||
webhookUrl: text("webhookUrl").notNull(),
|
||||
channel: text("channel"),
|
||||
username: text("username"),
|
||||
});
|
||||
|
||||
export const lark = pgTable("lark", {
|
||||
larkId: text("larkId")
|
||||
.notNull()
|
||||
@@ -153,6 +167,10 @@ export const notificationsRelations = relations(notifications, ({ one }) => ({
|
||||
fields: [notifications.ntfyId],
|
||||
references: [ntfy.ntfyId],
|
||||
}),
|
||||
mattermost: one(mattermost, {
|
||||
fields: [notifications.mattermostId],
|
||||
references: [mattermost.mattermostId],
|
||||
}),
|
||||
lark: one(lark, {
|
||||
fields: [notifications.larkId],
|
||||
references: [lark.larkId],
|
||||
@@ -349,6 +367,49 @@ export const apiTestNtfyConnection = apiCreateNtfy.pick({
|
||||
priority: true,
|
||||
});
|
||||
|
||||
export const apiCreateMattermost = 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().optional(),
|
||||
username: z.string().optional(),
|
||||
})
|
||||
.required({
|
||||
name: true,
|
||||
webhookUrl: true,
|
||||
appBuildError: true,
|
||||
databaseBackup: true,
|
||||
dokployRestart: true,
|
||||
appDeploy: true,
|
||||
dockerCleanup: true,
|
||||
serverThreshold: true,
|
||||
});
|
||||
|
||||
export const apiUpdateMattermost = apiCreateMattermost.partial().extend({
|
||||
notificationId: z.string().min(1),
|
||||
mattermostId: z.string().min(1),
|
||||
organizationId: z.string().optional(),
|
||||
});
|
||||
|
||||
export const apiTestMattermostConnection = apiCreateMattermost
|
||||
.pick({
|
||||
webhookUrl: true,
|
||||
channel: true,
|
||||
username: true,
|
||||
})
|
||||
.extend({
|
||||
channel: z.string().optional(),
|
||||
username: z.string().optional(),
|
||||
});
|
||||
|
||||
export const apiFindOneNotification = notificationsSchema
|
||||
.pick({
|
||||
notificationId: true,
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
type apiCreateEmail,
|
||||
type apiCreateLark,
|
||||
type apiCreateGotify,
|
||||
type apiCreateMattermost,
|
||||
type apiCreateNtfy,
|
||||
type apiCreateSlack,
|
||||
type apiCreateTelegram,
|
||||
@@ -11,6 +12,7 @@ import {
|
||||
type apiUpdateEmail,
|
||||
type apiUpdateLark,
|
||||
type apiUpdateGotify,
|
||||
type apiUpdateMattermost,
|
||||
type apiUpdateNtfy,
|
||||
type apiUpdateSlack,
|
||||
type apiUpdateTelegram,
|
||||
@@ -18,6 +20,7 @@ import {
|
||||
email,
|
||||
lark,
|
||||
gotify,
|
||||
mattermost,
|
||||
notifications,
|
||||
ntfy,
|
||||
slack,
|
||||
@@ -588,6 +591,7 @@ export const findNotificationById = async (notificationId: string) => {
|
||||
email: true,
|
||||
gotify: true,
|
||||
ntfy: true,
|
||||
mattermost: true,
|
||||
lark: true,
|
||||
},
|
||||
});
|
||||
@@ -711,3 +715,95 @@ export const updateNotificationById = async (
|
||||
|
||||
return result[0];
|
||||
};
|
||||
|
||||
export const createMattermostNotification = async (
|
||||
input: typeof apiCreateMattermost._type,
|
||||
organizationId: string,
|
||||
) => {
|
||||
await db.transaction(async (tx) => {
|
||||
const newMattermost = await tx
|
||||
.insert(mattermost)
|
||||
.values({
|
||||
webhookUrl: input.webhookUrl,
|
||||
channel: input.channel,
|
||||
username: input.username,
|
||||
})
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
|
||||
if (!newMattermost) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error input: Inserting mattermost",
|
||||
});
|
||||
}
|
||||
|
||||
const newDestination = await tx
|
||||
.insert(notifications)
|
||||
.values({
|
||||
mattermostId: newMattermost.mattermostId,
|
||||
name: input.name,
|
||||
appDeploy: input.appDeploy,
|
||||
appBuildError: input.appBuildError,
|
||||
databaseBackup: input.databaseBackup,
|
||||
dokployRestart: input.dokployRestart,
|
||||
dockerCleanup: input.dockerCleanup,
|
||||
notificationType: "mattermost",
|
||||
organizationId: organizationId,
|
||||
serverThreshold: input.serverThreshold,
|
||||
})
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
|
||||
if (!newDestination) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error input: Inserting notification",
|
||||
});
|
||||
}
|
||||
|
||||
return newDestination;
|
||||
});
|
||||
};
|
||||
|
||||
export const updateMattermostNotification = async (
|
||||
input: typeof apiUpdateMattermost._type,
|
||||
) => {
|
||||
await db.transaction(async (tx) => {
|
||||
const newDestination = await tx
|
||||
.update(notifications)
|
||||
.set({
|
||||
name: input.name,
|
||||
appDeploy: input.appDeploy,
|
||||
appBuildError: input.appBuildError,
|
||||
databaseBackup: input.databaseBackup,
|
||||
dokployRestart: input.dokployRestart,
|
||||
dockerCleanup: input.dockerCleanup,
|
||||
organizationId: input.organizationId,
|
||||
serverThreshold: input.serverThreshold,
|
||||
})
|
||||
.where(eq(notifications.notificationId, input.notificationId))
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
|
||||
if (!newDestination) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error Updating notification",
|
||||
});
|
||||
}
|
||||
|
||||
await tx
|
||||
.update(mattermost)
|
||||
.set({
|
||||
webhookUrl: input.webhookUrl,
|
||||
channel: input.channel,
|
||||
username: input.username,
|
||||
})
|
||||
.where(eq(mattermost.mattermostId, input.mattermostId))
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
|
||||
return newDestination;
|
||||
});
|
||||
};
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
sendEmailNotification,
|
||||
sendLarkNotification,
|
||||
sendGotifyNotification,
|
||||
sendMattermostNotification,
|
||||
sendNtfyNotification,
|
||||
sendSlackNotification,
|
||||
sendTelegramNotification,
|
||||
@@ -45,13 +46,13 @@ export const sendBuildErrorNotifications = async ({
|
||||
slack: true,
|
||||
gotify: true,
|
||||
ntfy: true,
|
||||
mattermost: true,
|
||||
lark: true,
|
||||
},
|
||||
});
|
||||
|
||||
for (const notification of notificationList) {
|
||||
const { email, discord, telegram, slack, gotify, ntfy, lark } =
|
||||
notification;
|
||||
const { email, discord, telegram, slack, gotify, ntfy, mattermost, lark } = notification;
|
||||
if (email) {
|
||||
const template = await renderAsync(
|
||||
BuildFailedEmail({
|
||||
@@ -215,6 +216,26 @@ export const sendBuildErrorNotifications = async ({
|
||||
});
|
||||
}
|
||||
|
||||
if (mattermost) {
|
||||
await sendMattermostNotification(mattermost, {
|
||||
text: `:warning: **Build Failed**
|
||||
|
||||
**Project:** ${projectName}
|
||||
**Application:** ${applicationName}
|
||||
**Type:** ${applicationType}
|
||||
**Time:** ${date.toLocaleString()}
|
||||
|
||||
**Error:**
|
||||
\`\`\`
|
||||
${errorMessage}
|
||||
\`\`\`
|
||||
|
||||
[View Build Details](${buildLink})`,
|
||||
channel: mattermost.channel,
|
||||
username: mattermost.username || "Dokploy Bot",
|
||||
});
|
||||
}
|
||||
|
||||
if (lark) {
|
||||
const limitCharacter = 800;
|
||||
const truncatedErrorMessage = errorMessage.substring(0, limitCharacter);
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
sendEmailNotification,
|
||||
sendLarkNotification,
|
||||
sendGotifyNotification,
|
||||
sendMattermostNotification,
|
||||
sendNtfyNotification,
|
||||
sendSlackNotification,
|
||||
sendTelegramNotification,
|
||||
@@ -46,13 +47,13 @@ export const sendBuildSuccessNotifications = async ({
|
||||
slack: true,
|
||||
gotify: true,
|
||||
ntfy: true,
|
||||
mattermost: true,
|
||||
lark: true,
|
||||
},
|
||||
});
|
||||
|
||||
for (const notification of notificationList) {
|
||||
const { email, discord, telegram, slack, gotify, ntfy, lark } =
|
||||
notification;
|
||||
const { email, discord, telegram, slack, gotify, ntfy, mattermost, lark } = notification;
|
||||
|
||||
if (email) {
|
||||
const template = await renderAsync(
|
||||
@@ -317,5 +318,13 @@ export const sendBuildSuccessNotifications = async ({
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (mattermost) {
|
||||
await sendMattermostNotification(mattermost, {
|
||||
text: `**✅ Build Success**\n\n**Project:** ${projectName}\n**Application:** ${applicationName}\n**Type:** ${applicationType}\n**Date:** ${format(date, "PP")}\n**Time:** ${format(date, "pp")}\n\n[View Build Details](${buildLink})`,
|
||||
channel: mattermost.channel,
|
||||
username: mattermost.username || "Dokploy",
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
sendEmailNotification,
|
||||
sendLarkNotification,
|
||||
sendGotifyNotification,
|
||||
sendMattermostNotification,
|
||||
sendNtfyNotification,
|
||||
sendSlackNotification,
|
||||
sendTelegramNotification,
|
||||
@@ -45,13 +46,13 @@ export const sendDatabaseBackupNotifications = async ({
|
||||
slack: true,
|
||||
gotify: true,
|
||||
ntfy: true,
|
||||
mattermost: true,
|
||||
lark: true,
|
||||
},
|
||||
});
|
||||
|
||||
for (const notification of notificationList) {
|
||||
const { email, discord, telegram, slack, gotify, ntfy, lark } =
|
||||
notification;
|
||||
const { email, discord, telegram, slack, gotify, ntfy, mattermost, lark } = notification;
|
||||
|
||||
if (email) {
|
||||
const template = await renderAsync(
|
||||
@@ -356,5 +357,19 @@ export const sendDatabaseBackupNotifications = async ({
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (mattermost) {
|
||||
const statusEmoji = type === "success" ? "✅" : "❌";
|
||||
const typeStatus = type === "success" ? "Successful" : "Failed";
|
||||
const errorMsg = type === "error" && errorMessage
|
||||
? `\n\n**Error:**\n\`\`\`\n${errorMessage}\n\`\`\``
|
||||
: "";
|
||||
|
||||
await sendMattermostNotification(mattermost, {
|
||||
text: `**${statusEmoji} Database Backup ${typeStatus}**\n\n**Project:** ${projectName}\n**Application:** ${applicationName}\n**Type:** ${databaseType}\n**Database Name:** ${databaseName}\n**Date:** ${format(date, "PP")}\n**Time:** ${format(date, "pp")}${errorMsg}`,
|
||||
channel: mattermost.channel,
|
||||
username: mattermost.username || "Dokploy",
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
sendEmailNotification,
|
||||
sendLarkNotification,
|
||||
sendGotifyNotification,
|
||||
sendMattermostNotification,
|
||||
sendNtfyNotification,
|
||||
sendSlackNotification,
|
||||
sendTelegramNotification,
|
||||
@@ -32,13 +33,13 @@ export const sendDockerCleanupNotifications = async (
|
||||
slack: true,
|
||||
gotify: true,
|
||||
ntfy: true,
|
||||
mattermost: true,
|
||||
lark: true,
|
||||
},
|
||||
});
|
||||
|
||||
for (const notification of notificationList) {
|
||||
const { email, discord, telegram, slack, gotify, ntfy, lark } =
|
||||
notification;
|
||||
const { email, discord, telegram, slack, gotify, ntfy, mattermost, lark } = notification;
|
||||
|
||||
if (email) {
|
||||
const template = await renderAsync(
|
||||
@@ -139,7 +140,15 @@ export const sendDockerCleanupNotifications = async (
|
||||
});
|
||||
}
|
||||
|
||||
if (lark) {
|
||||
if (mattermost) {
|
||||
await sendMattermostNotification(mattermost, {
|
||||
text: `**✅ Docker Cleanup**\n\n**Message:** ${message}\n**Date:** ${format(date, "PP")}\n**Time:** ${format(date, "pp")}`,
|
||||
channel: mattermost.channel,
|
||||
username: mattermost.username || "Dokploy",
|
||||
});
|
||||
}
|
||||
|
||||
if (lark) {
|
||||
await sendLarkNotification(lark, {
|
||||
msg_type: "interactive",
|
||||
card: {
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
sendEmailNotification,
|
||||
sendLarkNotification,
|
||||
sendGotifyNotification,
|
||||
sendMattermostNotification,
|
||||
sendNtfyNotification,
|
||||
sendSlackNotification,
|
||||
sendTelegramNotification,
|
||||
@@ -26,13 +27,13 @@ export const sendDokployRestartNotifications = async () => {
|
||||
slack: true,
|
||||
gotify: true,
|
||||
ntfy: true,
|
||||
mattermost: true,
|
||||
lark: true,
|
||||
},
|
||||
});
|
||||
|
||||
for (const notification of notificationList) {
|
||||
const { email, discord, telegram, slack, gotify, ntfy, lark } =
|
||||
notification;
|
||||
const { email, discord, telegram, slack, gotify, ntfy, mattermost, lark } = notification;
|
||||
|
||||
if (email) {
|
||||
const template = await renderAsync(
|
||||
@@ -139,6 +140,18 @@ export const sendDokployRestartNotifications = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
if (mattermost) {
|
||||
try {
|
||||
await sendMattermostNotification(mattermost, {
|
||||
text: `**✅ Dokploy Server Restarted**\n\n**Date:** ${format(date, "PP")}\n**Time:** ${format(date, "pp")}`,
|
||||
channel: mattermost.channel,
|
||||
username: mattermost.username || "Dokploy",
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
if (lark) {
|
||||
try {
|
||||
await sendLarkNotification(lark, {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { db } from "../../db";
|
||||
import { notifications } from "../../db/schema";
|
||||
import {
|
||||
sendDiscordNotification,
|
||||
sendMattermostNotification,
|
||||
sendLarkNotification,
|
||||
sendSlackNotification,
|
||||
sendTelegramNotification,
|
||||
@@ -35,6 +36,7 @@ export const sendServerThresholdNotifications = async (
|
||||
discord: true,
|
||||
telegram: true,
|
||||
slack: true,
|
||||
mattermost: true,
|
||||
lark: true,
|
||||
},
|
||||
});
|
||||
@@ -43,7 +45,7 @@ export const sendServerThresholdNotifications = async (
|
||||
const typeColor = 0xff0000; // Rojo para indicar alerta
|
||||
|
||||
for (const notification of notificationList) {
|
||||
const { discord, telegram, slack, lark } = notification;
|
||||
const { discord, telegram, slack, mattermost, lark } = notification;
|
||||
|
||||
if (discord) {
|
||||
const decorate = (decoration: string, text: string) =>
|
||||
@@ -154,7 +156,15 @@ export const sendServerThresholdNotifications = async (
|
||||
});
|
||||
}
|
||||
|
||||
if (lark) {
|
||||
if (mattermost) {
|
||||
await sendMattermostNotification(mattermost, {
|
||||
text: `**⚠️ Server ${payload.Type} Alert**\n\n**Server Name:** ${payload.ServerName}\n**Type:** ${payload.Type}\n**Current Value:** ${payload.Value.toFixed(2)}%\n**Threshold:** ${payload.Threshold.toFixed(2)}%\n**Message:** ${payload.Message}\n**Time:** ${date.toLocaleString()}`,
|
||||
channel: mattermost.channel,
|
||||
username: mattermost.username || "Dokploy",
|
||||
});
|
||||
}
|
||||
|
||||
if (lark) {
|
||||
await sendLarkNotification(lark, {
|
||||
msg_type: "interactive",
|
||||
card: {
|
||||
|
||||
@@ -3,6 +3,7 @@ import type {
|
||||
email,
|
||||
lark,
|
||||
gotify,
|
||||
mattermost,
|
||||
ntfy,
|
||||
slack,
|
||||
telegram,
|
||||
@@ -154,6 +155,28 @@ export const sendNtfyNotification = async (
|
||||
}
|
||||
};
|
||||
|
||||
export const sendMattermostNotification = async (
|
||||
connection: typeof mattermost.$inferInsert,
|
||||
message: any,
|
||||
) => {
|
||||
try {
|
||||
const payload = {
|
||||
...message,
|
||||
// Only include username if it's provided and not empty
|
||||
...(message.username && message.username.trim() && { username: message.username }),
|
||||
// Only include wchannel if it's provided and not empty
|
||||
...(message.channel && message.channel.trim() && { channel: `#${message.channel.replace('#', '')}` }),
|
||||
};
|
||||
|
||||
await fetch(connection.webhookUrl, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
|
||||
export const sendLarkNotification = async (
|
||||
connection: typeof lark.$inferInsert,
|
||||
message: any,
|
||||
|
||||
Reference in New Issue
Block a user