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

@@ -6,6 +6,7 @@ import { toast } from "sonner";
import { z } from "zod";
import {
DiscordIcon,
MattermostIcon,
GotifyIcon,
LarkIcon,
NtfyIcon,
@@ -108,6 +109,14 @@ export const notificationSchema = z.discriminatedUnion("type", [
})
.merge(notificationBaseSchema),
z
.object({
type: z.literal("mattermost"),
webhookUrl: z.string().min(1, { message: "Webhook URL is required" }),
channel: z.string().optional(),
username: z.string().optional(),
})
.merge(notificationBaseSchema),
z
.object({
type: z.literal("lark"),
webhookUrl: z.string().min(1, { message: "Webhook URL is required" }),
@@ -144,6 +153,10 @@ export const notificationsMap = {
icon: <NtfyIcon />,
label: "ntfy",
},
mattermost: {
icon: <MattermostIcon />,
label: "Mattermost",
},
};
export type NotificationSchema = z.infer<typeof notificationSchema>;
@@ -177,6 +190,8 @@ export const HandleNotifications = ({ notificationId }: Props) => {
api.notification.testGotifyConnection.useMutation();
const { mutateAsync: testNtfyConnection, isLoading: isLoadingNtfy } =
api.notification.testNtfyConnection.useMutation();
const { mutateAsync: testMattermostConnection, isLoading: isLoadingMattermost } =
api.notification.testMattermostConnection.useMutation();
const { mutateAsync: testLarkConnection, isLoading: isLoadingLark } =
api.notification.testLarkConnection.useMutation();
const slackMutation = notificationId
@@ -197,6 +212,9 @@ export const HandleNotifications = ({ notificationId }: Props) => {
const ntfyMutation = notificationId
? api.notification.updateNtfy.useMutation()
: api.notification.createNtfy.useMutation();
const mattermostMutation = notificationId
? api.notification.updateMattermost.useMutation()
: api.notification.createMattermost.useMutation();
const larkMutation = notificationId
? api.notification.updateLark.useMutation()
: api.notification.createLark.useMutation();
@@ -323,6 +341,20 @@ export const HandleNotifications = ({ notificationId }: Props) => {
dockerCleanup: notification.dockerCleanup,
serverThreshold: notification.serverThreshold,
});
} else if (notification.notificationType === "mattermost") {
form.reset({
appBuildError: notification.appBuildError,
appDeploy: notification.appDeploy,
dokployRestart: notification.dokployRestart,
databaseBackup: notification.databaseBackup,
type: notification.notificationType,
webhookUrl: notification.mattermost?.webhookUrl,
channel: notification.mattermost?.channel || "",
username: notification.mattermost?.username || "",
name: notification.name,
dockerCleanup: notification.dockerCleanup,
serverThreshold: notification.serverThreshold,
});
}
} else {
form.reset();
@@ -336,6 +368,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
email: emailMutation,
gotify: gotifyMutation,
ntfy: ntfyMutation,
mattermost: mattermostMutation,
lark: larkMutation,
};
@@ -440,6 +473,21 @@ export const HandleNotifications = ({ notificationId }: Props) => {
notificationId: notificationId || "",
ntfyId: notification?.ntfyId || "",
});
} else if (data.type === "mattermost") {
promise = mattermostMutation.mutateAsync({
appBuildError: appBuildError,
appDeploy: appDeploy,
dokployRestart: dokployRestart,
databaseBackup: databaseBackup,
webhookUrl: data.webhookUrl,
channel: data.channel || undefined,
username: data.username || undefined,
name: data.name,
dockerCleanup: dockerCleanup,
notificationId: notificationId || "",
mattermostId: notification?.mattermostId || "",
serverThreshold: serverThreshold,
});
} else if (data.type === "lark") {
promise = larkMutation.mutateAsync({
appBuildError: appBuildError,
@@ -451,6 +499,8 @@ export const HandleNotifications = ({ notificationId }: Props) => {
dockerCleanup: dockerCleanup,
notificationId: notificationId || "",
larkId: notification?.larkId || "",
serverThreshold: serverThreshold,
});
}
@@ -1040,6 +1090,60 @@ export const HandleNotifications = ({ notificationId }: Props) => {
</>
)}
{type === "mattermost" && (
<>
<FormField
control={form.control}
name="webhookUrl"
render={({ field }) => (
<FormItem>
<FormLabel>Webhook URL</FormLabel>
<FormControl>
<Input
placeholder="https://your-mattermost.com/hooks/xxx-generatedkey-xxx"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="channel"
render={({ field }) => (
<FormItem>
<FormLabel>Channel</FormLabel>
<FormControl>
<Input placeholder="deployments" {...field} />
</FormControl>
<FormDescription>
Optional. Channel to post to (without #).
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input placeholder="Dokploy" {...field} />
</FormControl>
<FormDescription>
Optional. Display name for the webhook.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</>
)}
{type === "lark" && (
<>
<FormField
@@ -1211,6 +1315,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
isLoadingEmail ||
isLoadingGotify ||
isLoadingNtfy ||
isLoadingMattermost ||
isLoadingLark
}
variant="secondary"
@@ -1255,6 +1360,12 @@ export const HandleNotifications = ({ notificationId }: Props) => {
accessToken: form.getValues("accessToken"),
priority: form.getValues("priority"),
});
} else if (type === "mattermost") {
await testMattermostConnection({
webhookUrl: form.getValues("webhookUrl"),
channel: form.getValues("channel") || undefined,
username: form.getValues("username") || undefined,
});
} else if (type === "lark") {
await testLarkConnection({
webhookUrl: form.getValues("webhookUrl"),

View File

@@ -88,6 +88,20 @@ export const DiscordIcon = ({ className }: Props) => {
</svg>
);
};
export const MattermostIcon = ({ className }: Props) => {
return (
<svg
fill="#0061ff"
viewBox="0 0 501 501"
xmlns="http://www.w3.org/2000/svg"
className={cn("size-8", className)}
>
<path d="M236 .7C137.7 7.5 54 68.2 18.2 158.5c-32 81-19.6 172.8 33 242.5 39.8 53 97.2 87 164.3 97 16.5 2.7 48 3.2 63.5 1.2 48.7-6.3 92.2-24.6 129-54.2 13-10.5 33-31.2 42.2-43.7 26.4-35.5 42.8-75.8 49-120.3 1.6-12.3 1.6-48.7 0-61-4-28.3-12-54.8-24.2-79.5-12.8-26-26.5-45.3-46.8-65.8C417.8 64 400.2 49 398.4 49c-.6 0-.4 10.5.3 26l1.3 26 7 8.7c19 23.7 32.8 53.5 38.2 83 2.5 14 3 43 1 55.8-4.5 27.8-15.2 54-31 76.5-8.6 12.2-28 31.6-40.2 40.2-24 17-50 27.6-80 33-10 1.8-49 1.8-59 0-43-7.7-78.8-26-107.2-54.8-29.3-29.7-46.5-64-52.4-104.4-2-14-1.5-42 1-55C90 121.4 132 72 192 49.7c8-3 18.4-5.8 29.5-8.2 1.7-.4 34.4-38 35.3-40.6.3-1-10.2-1-20.8-.4z"/>
<path d="M322.2 24.6c-1.3.8-8.4 9.3-16 18.7-7.4 9.5-22.4 28-33.2 41.2-51 62.2-66 81.6-70.6 91-6 12-8.4 21-9 33-1.2 19.8 5 36 19 50C222 268 230 273 243 277.2c9 3 10.4 3.2 24 3.2 13.8 0 15 0 22.6-3 23.2-9 39-28.4 45-55.7 2-8.2 2-28.7.4-79.7l-2-72c-1-36.8-1.4-41.8-3-44-2-3-4.8-3.6-7.8-1.4z"/>
</svg>
);
};
export const LarkIcon = ({ className }: Props) => {
return (
<svg

View File

@@ -3,6 +3,7 @@ import {
createEmailNotification,
createLarkNotification,
createGotifyNotification,
createMattermostNotification,
createNtfyNotification,
createSlackNotification,
createTelegramNotification,
@@ -13,6 +14,7 @@ import {
sendEmailNotification,
sendLarkNotification,
sendGotifyNotification,
sendMattermostNotification,
sendNtfyNotification,
sendServerThresholdNotifications,
sendSlackNotification,
@@ -21,6 +23,7 @@ import {
updateEmailNotification,
updateLarkNotification,
updateGotifyNotification,
updateMattermostNotification,
updateNtfyNotification,
updateSlackNotification,
updateTelegramNotification,
@@ -40,6 +43,7 @@ import {
apiCreateEmail,
apiCreateLark,
apiCreateGotify,
apiCreateMattermost,
apiCreateNtfy,
apiCreateSlack,
apiCreateTelegram,
@@ -48,6 +52,7 @@ import {
apiTestEmailConnection,
apiTestLarkConnection,
apiTestGotifyConnection,
apiTestMattermostConnection,
apiTestNtfyConnection,
apiTestSlackConnection,
apiTestTelegramConnection,
@@ -55,6 +60,7 @@ import {
apiUpdateEmail,
apiUpdateLark,
apiUpdateGotify,
apiUpdateMattermost,
apiUpdateNtfy,
apiUpdateSlack,
apiUpdateTelegram,
@@ -334,6 +340,7 @@ export const notificationRouter = createTRPCRouter({
email: true,
gotify: true,
ntfy: true,
mattermost: true,
lark: true,
},
orderBy: desc(notifications.createdAt),
@@ -518,7 +525,63 @@ export const notificationRouter = createTRPCRouter({
});
}
}),
createLark: adminProcedure
createMattermost: adminProcedure
.input(apiCreateMattermost)
.mutation(async ({ input, ctx }) => {
try {
return await createMattermostNotification(
input,
ctx.session.activeOrganizationId,
);
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error creating the notification",
cause: error,
});
}
}),
updateMattermost: adminProcedure
.input(apiUpdateMattermost)
.mutation(async ({ input, ctx }) => {
try {
const notification = await findNotificationById(input.notificationId);
if (
IS_CLOUD &&
notification.organizationId !== ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to update this notification",
});
}
return await updateMattermostNotification({
...input,
organizationId: ctx.session.activeOrganizationId,
});
} catch (error) {
throw error;
}
}),
testMattermostConnection: adminProcedure
.input(apiTestMattermostConnection)
.mutation(async ({ input }) => {
try {
await sendMattermostNotification(input, {
text: "Hi, From Dokploy 👋",
channel: input.channel,
username: input.username || "Dokploy Bot",
});
return true;
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error testing the notification",
cause: error,
});
}
}),
createLark: adminProcedure
.input(apiCreateLark)
.mutation(async ({ input, ctx }) => {
try {

View File

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

View File

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

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,