mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-07-10 08: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:
@@ -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"),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user