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:
Hootan
2025-09-29 18:46:25 +02:00
parent dadef000d5
commit a0c87358eb
12 changed files with 459 additions and 14 deletions

View File

@@ -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);

View File

@@ -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",
});
}
}
};

View File

@@ -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",
});
}
}
};

View File

@@ -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: {

View File

@@ -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, {

View File

@@ -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: {

View File

@@ -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,