mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-15 20:25:23 +02:00
Merge pull request #3729 from Dokploy/feat/add-teams-notification-provider
feat(notifications): add Microsoft Teams integration for notifications
This commit is contained in:
@@ -18,6 +18,7 @@ import {
|
||||
PushoverIcon,
|
||||
ResendIcon,
|
||||
SlackIcon,
|
||||
TeamsIcon,
|
||||
TelegramIcon,
|
||||
} from "@/components/icons/notification-icons";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -164,6 +165,12 @@ export const notificationSchema = z.discriminatedUnion("type", [
|
||||
webhookUrl: z.string().min(1, { message: "Webhook URL is required" }),
|
||||
})
|
||||
.merge(notificationBaseSchema),
|
||||
z
|
||||
.object({
|
||||
type: z.literal("teams"),
|
||||
webhookUrl: z.string().min(1, { message: "Webhook URL is required" }),
|
||||
})
|
||||
.merge(notificationBaseSchema),
|
||||
]);
|
||||
|
||||
export const notificationsMap = {
|
||||
@@ -183,6 +190,10 @@ export const notificationsMap = {
|
||||
icon: <LarkIcon className="text-muted-foreground" />,
|
||||
label: "Lark",
|
||||
},
|
||||
teams: {
|
||||
icon: <TeamsIcon className="text-muted-foreground" />,
|
||||
label: "Microsoft Teams",
|
||||
},
|
||||
email: {
|
||||
icon: <Mail size={29} className="text-muted-foreground" />,
|
||||
label: "Email",
|
||||
@@ -244,6 +255,8 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
||||
api.notification.testNtfyConnection.useMutation();
|
||||
const { mutateAsync: testLarkConnection, isLoading: isLoadingLark } =
|
||||
api.notification.testLarkConnection.useMutation();
|
||||
const { mutateAsync: testTeamsConnection, isLoading: isLoadingTeams } =
|
||||
api.notification.testTeamsConnection.useMutation();
|
||||
|
||||
const { mutateAsync: testCustomConnection, isLoading: isLoadingCustom } =
|
||||
api.notification.testCustomConnection.useMutation();
|
||||
@@ -278,6 +291,9 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
||||
const larkMutation = notificationId
|
||||
? api.notification.updateLark.useMutation()
|
||||
: api.notification.createLark.useMutation();
|
||||
const teamsMutation = notificationId
|
||||
? api.notification.updateTeams.useMutation()
|
||||
: api.notification.createTeams.useMutation();
|
||||
const pushoverMutation = notificationId
|
||||
? api.notification.updatePushover.useMutation()
|
||||
: api.notification.createPushover.useMutation();
|
||||
@@ -435,6 +451,19 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
||||
volumeBackup: notification.volumeBackup,
|
||||
serverThreshold: notification.serverThreshold,
|
||||
});
|
||||
} else if (notification.notificationType === "teams") {
|
||||
form.reset({
|
||||
appBuildError: notification.appBuildError,
|
||||
appDeploy: notification.appDeploy,
|
||||
dokployRestart: notification.dokployRestart,
|
||||
databaseBackup: notification.databaseBackup,
|
||||
volumeBackup: notification.volumeBackup,
|
||||
type: notification.notificationType,
|
||||
webhookUrl: notification.teams?.webhookUrl,
|
||||
name: notification.name,
|
||||
dockerCleanup: notification.dockerCleanup,
|
||||
serverThreshold: notification.serverThreshold,
|
||||
});
|
||||
} else if (notification.notificationType === "custom") {
|
||||
form.reset({
|
||||
appBuildError: notification.appBuildError,
|
||||
@@ -488,6 +517,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
||||
gotify: gotifyMutation,
|
||||
ntfy: ntfyMutation,
|
||||
lark: larkMutation,
|
||||
teams: teamsMutation,
|
||||
custom: customMutation,
|
||||
pushover: pushoverMutation,
|
||||
};
|
||||
@@ -630,6 +660,20 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
||||
larkId: notification?.larkId || "",
|
||||
serverThreshold: serverThreshold,
|
||||
});
|
||||
} else if (data.type === "teams") {
|
||||
promise = teamsMutation.mutateAsync({
|
||||
appBuildError: appBuildError,
|
||||
appDeploy: appDeploy,
|
||||
dokployRestart: dokployRestart,
|
||||
databaseBackup: databaseBackup,
|
||||
volumeBackup: volumeBackup,
|
||||
webhookUrl: data.webhookUrl,
|
||||
name: data.name,
|
||||
dockerCleanup: dockerCleanup,
|
||||
notificationId: notificationId || "",
|
||||
teamsId: notification?.teamsId || "",
|
||||
serverThreshold: serverThreshold,
|
||||
});
|
||||
} else if (data.type === "custom") {
|
||||
// Convert headers array to object
|
||||
const headersRecord =
|
||||
@@ -1465,6 +1509,32 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{type === "teams" && (
|
||||
<>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="webhookUrl"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Webhook URL</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="https://xxx.webhook.office.com/webhookb2/..."
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Incoming Webhook URL from a Teams channel. Add an
|
||||
Incoming Webhook in your channel settings to get the
|
||||
URL.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{type === "pushover" && (
|
||||
<>
|
||||
<FormField
|
||||
@@ -1780,6 +1850,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
||||
isLoadingGotify ||
|
||||
isLoadingNtfy ||
|
||||
isLoadingLark ||
|
||||
isLoadingTeams ||
|
||||
isLoadingCustom ||
|
||||
isLoadingPushover
|
||||
}
|
||||
@@ -1841,6 +1912,10 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
||||
await testLarkConnection({
|
||||
webhookUrl: data.webhookUrl,
|
||||
});
|
||||
} else if (data.type === "teams") {
|
||||
await testTeamsConnection({
|
||||
webhookUrl: data.webhookUrl,
|
||||
});
|
||||
} else if (data.type === "custom") {
|
||||
const headersRecord =
|
||||
data.headers && data.headers.length > 0
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
NtfyIcon,
|
||||
ResendIcon,
|
||||
SlackIcon,
|
||||
TeamsIcon,
|
||||
TelegramIcon,
|
||||
} from "@/components/icons/notification-icons";
|
||||
import { DialogAction } from "@/components/shared/dialog-action";
|
||||
@@ -37,7 +38,7 @@ export const ShowNotifications = () => {
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Add your providers to receive notifications, like Discord, Slack,
|
||||
Telegram, Email, Resend, Lark.
|
||||
Telegram, Teams, Email, Resend, Lark.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2 py-8 border-t">
|
||||
@@ -112,6 +113,11 @@ export const ShowNotifications = () => {
|
||||
<LarkIcon className="size-7 text-muted-foreground" />
|
||||
</div>
|
||||
)}
|
||||
{notification.notificationType === "teams" && (
|
||||
<div className="flex items-center justify-center rounded-lg">
|
||||
<TeamsIcon className="size-7 text-muted-foreground" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{notification.name}
|
||||
</span>
|
||||
|
||||
@@ -88,6 +88,35 @@ export const DiscordIcon = ({ className }: Props) => {
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
export const TeamsIcon = ({ className }: Props) => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="26"
|
||||
height="36"
|
||||
viewBox="0 0 512 476"
|
||||
className={cn("size-9", className)}
|
||||
>
|
||||
<g>
|
||||
<rect x="116" y="50" width="280" height="276" rx="64" fill="#6264A7" />
|
||||
<rect x="236" y="138" width="180" height="224" rx="60" fill="#5059C9" />
|
||||
<circle cx="122" cy="332" r="80" fill="#B2B4D3" />
|
||||
<circle cx="370" cy="364" r="64" fill="#A6A7DC" />
|
||||
<text
|
||||
x="180"
|
||||
y="270"
|
||||
fill="#fff"
|
||||
font-family="Segoe UI, Arial, sans-serif"
|
||||
font-size="110"
|
||||
font-weight="bold"
|
||||
>
|
||||
T
|
||||
</text>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const LarkIcon = ({ className }: Props) => {
|
||||
return (
|
||||
<svg
|
||||
|
||||
8
apps/dokploy/drizzle/0144_odd_gunslinger.sql
Normal file
8
apps/dokploy/drizzle/0144_odd_gunslinger.sql
Normal file
@@ -0,0 +1,8 @@
|
||||
ALTER TYPE "public"."notificationType" ADD VALUE 'teams';--> statement-breakpoint
|
||||
CREATE TABLE "teams" (
|
||||
"teamsId" text PRIMARY KEY NOT NULL,
|
||||
"webhookUrl" text NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "notification" ADD COLUMN "teamsId" text;--> statement-breakpoint
|
||||
ALTER TABLE "notification" ADD CONSTRAINT "notification_teamsId_teams_teamsId_fk" FOREIGN KEY ("teamsId") REFERENCES "public"."teams"("teamsId") ON DELETE cascade ON UPDATE no action;
|
||||
7336
apps/dokploy/drizzle/meta/0144_snapshot.json
Normal file
7336
apps/dokploy/drizzle/meta/0144_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1009,6 +1009,13 @@
|
||||
"when": 1770961667210,
|
||||
"tag": "0143_brown_ultron",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 144,
|
||||
"version": "7",
|
||||
"when": 1771297084611,
|
||||
"tag": "0144_odd_gunslinger",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
createPushoverNotification,
|
||||
createResendNotification,
|
||||
createSlackNotification,
|
||||
createTeamsNotification,
|
||||
createTelegramNotification,
|
||||
findNotificationById,
|
||||
getWebServerSettings,
|
||||
@@ -23,6 +24,7 @@ import {
|
||||
sendResendNotification,
|
||||
sendServerThresholdNotifications,
|
||||
sendSlackNotification,
|
||||
sendTeamsNotification,
|
||||
sendTelegramNotification,
|
||||
updateCustomNotification,
|
||||
updateDiscordNotification,
|
||||
@@ -33,6 +35,7 @@ import {
|
||||
updatePushoverNotification,
|
||||
updateResendNotification,
|
||||
updateSlackNotification,
|
||||
updateTeamsNotification,
|
||||
updateTelegramNotification,
|
||||
} from "@dokploy/server";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
@@ -55,6 +58,7 @@ import {
|
||||
apiCreatePushover,
|
||||
apiCreateResend,
|
||||
apiCreateSlack,
|
||||
apiCreateTeams,
|
||||
apiCreateTelegram,
|
||||
apiFindOneNotification,
|
||||
apiTestCustomConnection,
|
||||
@@ -66,6 +70,7 @@ import {
|
||||
apiTestPushoverConnection,
|
||||
apiTestResendConnection,
|
||||
apiTestSlackConnection,
|
||||
apiTestTeamsConnection,
|
||||
apiTestTelegramConnection,
|
||||
apiUpdateCustom,
|
||||
apiUpdateDiscord,
|
||||
@@ -76,6 +81,7 @@ import {
|
||||
apiUpdatePushover,
|
||||
apiUpdateResend,
|
||||
apiUpdateSlack,
|
||||
apiUpdateTeams,
|
||||
apiUpdateTelegram,
|
||||
notifications,
|
||||
server,
|
||||
@@ -413,6 +419,7 @@ export const notificationRouter = createTRPCRouter({
|
||||
custom: true,
|
||||
lark: true,
|
||||
pushover: true,
|
||||
teams: true,
|
||||
},
|
||||
orderBy: desc(notifications.createdAt),
|
||||
where: eq(notifications.organizationId, ctx.session.activeOrganizationId),
|
||||
@@ -705,6 +712,61 @@ export const notificationRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
}),
|
||||
createTeams: adminProcedure
|
||||
.input(apiCreateTeams)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
return await createTeamsNotification(
|
||||
input,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error creating the notification",
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
}),
|
||||
updateTeams: adminProcedure
|
||||
.input(apiUpdateTeams)
|
||||
.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 updateTeamsNotification({
|
||||
...input,
|
||||
organizationId: ctx.session.activeOrganizationId,
|
||||
});
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}),
|
||||
testTeamsConnection: adminProcedure
|
||||
.input(apiTestTeamsConnection)
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
await sendTeamsNotification(input, {
|
||||
title: "🤚 Test Notification",
|
||||
facts: [{ name: "Message", value: "Hi, From Dokploy 👋" }],
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: `${error instanceof Error ? error.message : "Unknown error"}`,
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
}),
|
||||
createPushover: adminProcedure
|
||||
.input(apiCreatePushover)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
|
||||
@@ -23,6 +23,7 @@ export const notificationType = pgEnum("notificationType", [
|
||||
"pushover",
|
||||
"custom",
|
||||
"lark",
|
||||
"teams",
|
||||
]);
|
||||
|
||||
export const notifications = pgTable("notification", {
|
||||
@@ -72,6 +73,9 @@ export const notifications = pgTable("notification", {
|
||||
pushoverId: text("pushoverId").references(() => pushover.pushoverId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
teamsId: text("teamsId").references(() => teams.teamsId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
organizationId: text("organizationId")
|
||||
.notNull()
|
||||
.references(() => organization.id, { onDelete: "cascade" }),
|
||||
@@ -179,6 +183,14 @@ export const pushover = pgTable("pushover", {
|
||||
expire: integer("expire"),
|
||||
});
|
||||
|
||||
export const teams = pgTable("teams", {
|
||||
teamsId: text("teamsId")
|
||||
.notNull()
|
||||
.primaryKey()
|
||||
.$defaultFn(() => nanoid()),
|
||||
webhookUrl: text("webhookUrl").notNull(),
|
||||
});
|
||||
|
||||
export const notificationsRelations = relations(notifications, ({ one }) => ({
|
||||
slack: one(slack, {
|
||||
fields: [notifications.slackId],
|
||||
@@ -220,6 +232,10 @@ export const notificationsRelations = relations(notifications, ({ one }) => ({
|
||||
fields: [notifications.pushoverId],
|
||||
references: [pushover.pushoverId],
|
||||
}),
|
||||
teams: one(teams, {
|
||||
fields: [notifications.teamsId],
|
||||
references: [teams.teamsId],
|
||||
}),
|
||||
organization: one(organization, {
|
||||
fields: [notifications.organizationId],
|
||||
references: [organization.id],
|
||||
@@ -507,6 +523,32 @@ export const apiTestLarkConnection = apiCreateLark.pick({
|
||||
webhookUrl: true,
|
||||
});
|
||||
|
||||
export const apiCreateTeams = notificationsSchema
|
||||
.pick({
|
||||
appBuildError: true,
|
||||
databaseBackup: true,
|
||||
volumeBackup: true,
|
||||
dokployRestart: true,
|
||||
name: true,
|
||||
appDeploy: true,
|
||||
dockerCleanup: true,
|
||||
serverThreshold: true,
|
||||
})
|
||||
.extend({
|
||||
webhookUrl: z.string().min(1),
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiUpdateTeams = apiCreateTeams.partial().extend({
|
||||
notificationId: z.string().min(1),
|
||||
teamsId: z.string().min(1),
|
||||
organizationId: z.string().optional(),
|
||||
});
|
||||
|
||||
export const apiTestTeamsConnection = apiCreateTeams.pick({
|
||||
webhookUrl: true,
|
||||
});
|
||||
|
||||
export const apiCreatePushover = notificationsSchema
|
||||
.pick({
|
||||
appBuildError: true,
|
||||
|
||||
@@ -101,7 +101,9 @@ export const findEnvironmentsByProjectId = async (projectId: string) => {
|
||||
return projectEnvironments;
|
||||
};
|
||||
|
||||
const environmentHasServices = (env: Awaited<ReturnType<typeof findEnvironmentById>>) => {
|
||||
const environmentHasServices = (
|
||||
env: Awaited<ReturnType<typeof findEnvironmentById>>,
|
||||
) => {
|
||||
return (
|
||||
(env.applications?.length ?? 0) > 0 ||
|
||||
(env.compose?.length ?? 0) > 0 ||
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
type apiCreatePushover,
|
||||
type apiCreateResend,
|
||||
type apiCreateSlack,
|
||||
type apiCreateTeams,
|
||||
type apiCreateTelegram,
|
||||
type apiUpdateCustom,
|
||||
type apiUpdateDiscord,
|
||||
@@ -19,6 +20,7 @@ import {
|
||||
type apiUpdatePushover,
|
||||
type apiUpdateResend,
|
||||
type apiUpdateSlack,
|
||||
type apiUpdateTeams,
|
||||
type apiUpdateTelegram,
|
||||
custom,
|
||||
discord,
|
||||
@@ -30,6 +32,7 @@ import {
|
||||
pushover,
|
||||
resend,
|
||||
slack,
|
||||
teams,
|
||||
telegram,
|
||||
} from "@dokploy/server/db/schema";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
@@ -796,6 +799,7 @@ export const findNotificationById = async (notificationId: string) => {
|
||||
custom: true,
|
||||
lark: true,
|
||||
pushover: true,
|
||||
teams: true,
|
||||
},
|
||||
});
|
||||
if (!notification) {
|
||||
@@ -905,6 +909,96 @@ export const updateLarkNotification = async (
|
||||
});
|
||||
};
|
||||
|
||||
export const createTeamsNotification = async (
|
||||
input: typeof apiCreateTeams._type,
|
||||
organizationId: string,
|
||||
) => {
|
||||
await db.transaction(async (tx) => {
|
||||
const newTeams = await tx
|
||||
.insert(teams)
|
||||
.values({
|
||||
webhookUrl: input.webhookUrl,
|
||||
})
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
|
||||
if (!newTeams) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error input: Inserting teams",
|
||||
});
|
||||
}
|
||||
|
||||
const newDestination = await tx
|
||||
.insert(notifications)
|
||||
.values({
|
||||
teamsId: newTeams.teamsId,
|
||||
name: input.name,
|
||||
appDeploy: input.appDeploy,
|
||||
appBuildError: input.appBuildError,
|
||||
databaseBackup: input.databaseBackup,
|
||||
volumeBackup: input.volumeBackup,
|
||||
dokployRestart: input.dokployRestart,
|
||||
dockerCleanup: input.dockerCleanup,
|
||||
notificationType: "teams",
|
||||
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 updateTeamsNotification = async (
|
||||
input: typeof apiUpdateTeams._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,
|
||||
volumeBackup: input.volumeBackup,
|
||||
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(teams)
|
||||
.set({
|
||||
webhookUrl: input.webhookUrl,
|
||||
})
|
||||
.where(eq(teams.teamsId, input.teamsId))
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
|
||||
return newDestination;
|
||||
});
|
||||
};
|
||||
|
||||
export const updateNotificationById = async (
|
||||
notificationId: string,
|
||||
notificationData: Partial<Notification>,
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
sendPushoverNotification,
|
||||
sendResendNotification,
|
||||
sendSlackNotification,
|
||||
sendTeamsNotification,
|
||||
sendTelegramNotification,
|
||||
} from "./utils";
|
||||
|
||||
@@ -52,6 +53,7 @@ export const sendBuildErrorNotifications = async ({
|
||||
custom: true,
|
||||
lark: true,
|
||||
pushover: true,
|
||||
teams: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -67,6 +69,7 @@ export const sendBuildErrorNotifications = async ({
|
||||
custom,
|
||||
lark,
|
||||
pushover,
|
||||
teams,
|
||||
} = notification;
|
||||
try {
|
||||
if (email || resend) {
|
||||
@@ -382,6 +385,26 @@ export const sendBuildErrorNotifications = async ({
|
||||
`Project: ${projectName}\nApplication: ${applicationName}\nType: ${applicationType}\nDate: ${date.toLocaleString()}\nError: ${errorMessage}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (teams) {
|
||||
const limitCharacter = 800;
|
||||
const truncatedErrorMessage = errorMessage.substring(0, limitCharacter);
|
||||
await sendTeamsNotification(teams, {
|
||||
title: "⚠️ Build Failed",
|
||||
facts: [
|
||||
{ name: "Project", value: projectName },
|
||||
{ name: "Application", value: applicationName },
|
||||
{ name: "Type", value: applicationType },
|
||||
{ name: "Date", value: format(date, "PP pp") },
|
||||
{ name: "Error Message", value: truncatedErrorMessage },
|
||||
],
|
||||
potentialAction: {
|
||||
type: "Action.OpenUrl",
|
||||
title: "View Build Details",
|
||||
url: buildLink,
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
sendPushoverNotification,
|
||||
sendResendNotification,
|
||||
sendSlackNotification,
|
||||
sendTeamsNotification,
|
||||
sendTelegramNotification,
|
||||
} from "./utils";
|
||||
|
||||
@@ -55,6 +56,7 @@ export const sendBuildSuccessNotifications = async ({
|
||||
custom: true,
|
||||
lark: true,
|
||||
pushover: true,
|
||||
teams: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -70,6 +72,7 @@ export const sendBuildSuccessNotifications = async ({
|
||||
custom,
|
||||
lark,
|
||||
pushover,
|
||||
teams,
|
||||
} = notification;
|
||||
try {
|
||||
if (email || resend) {
|
||||
@@ -396,6 +399,24 @@ export const sendBuildSuccessNotifications = async ({
|
||||
`Project: ${projectName}\nApplication: ${applicationName}\nEnvironment: ${environmentName}\nType: ${applicationType}\nDate: ${date.toLocaleString()}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (teams) {
|
||||
await sendTeamsNotification(teams, {
|
||||
title: "✅ Build Success",
|
||||
facts: [
|
||||
{ name: "Project", value: projectName },
|
||||
{ name: "Application", value: applicationName },
|
||||
{ name: "Environment", value: environmentName },
|
||||
{ name: "Type", value: applicationType },
|
||||
{ name: "Date", value: format(date, "PP pp") },
|
||||
],
|
||||
potentialAction: {
|
||||
type: "Action.OpenUrl",
|
||||
title: "View Build Details",
|
||||
url: buildLink,
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
sendPushoverNotification,
|
||||
sendResendNotification,
|
||||
sendSlackNotification,
|
||||
sendTeamsNotification,
|
||||
sendTelegramNotification,
|
||||
} from "./utils";
|
||||
|
||||
@@ -52,6 +53,7 @@ export const sendDatabaseBackupNotifications = async ({
|
||||
custom: true,
|
||||
lark: true,
|
||||
pushover: true,
|
||||
teams: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -67,6 +69,7 @@ export const sendDatabaseBackupNotifications = async ({
|
||||
custom,
|
||||
lark,
|
||||
pushover,
|
||||
teams,
|
||||
} = notification;
|
||||
try {
|
||||
if (email || resend) {
|
||||
@@ -410,6 +413,30 @@ export const sendDatabaseBackupNotifications = async ({
|
||||
`Project: ${projectName}\nApplication: ${applicationName}\nDatabase: ${databaseType}\nDatabase Name: ${databaseName}\nDate: ${date.toLocaleString()}${type === "error" && errorMessage ? `\nError: ${errorMessage}` : ""}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (teams) {
|
||||
const facts = [
|
||||
{ name: "Project", value: projectName },
|
||||
{ name: "Application", value: applicationName },
|
||||
{ name: "Database Type", value: databaseType },
|
||||
{ name: "Database Name", value: databaseName },
|
||||
{ name: "Date", value: format(date, "PP pp") },
|
||||
{
|
||||
name: "Status",
|
||||
value: type === "success" ? "Successful" : "Failed",
|
||||
},
|
||||
];
|
||||
if (type === "error" && errorMessage) {
|
||||
facts.push({ name: "Error", value: errorMessage.substring(0, 500) });
|
||||
}
|
||||
await sendTeamsNotification(teams, {
|
||||
title:
|
||||
type === "success"
|
||||
? "✅ Database Backup Successful"
|
||||
: "❌ Database Backup Failed",
|
||||
facts,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
sendPushoverNotification,
|
||||
sendResendNotification,
|
||||
sendSlackNotification,
|
||||
sendTeamsNotification,
|
||||
sendTelegramNotification,
|
||||
} from "./utils";
|
||||
|
||||
@@ -39,6 +40,7 @@ export const sendDockerCleanupNotifications = async (
|
||||
custom: true,
|
||||
lark: true,
|
||||
pushover: true,
|
||||
teams: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -54,6 +56,7 @@ export const sendDockerCleanupNotifications = async (
|
||||
custom,
|
||||
lark,
|
||||
pushover,
|
||||
teams,
|
||||
} = notification;
|
||||
try {
|
||||
if (email || resend) {
|
||||
@@ -262,6 +265,16 @@ export const sendDockerCleanupNotifications = async (
|
||||
`Date: ${date.toLocaleString()}\nMessage: ${message}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (teams) {
|
||||
await sendTeamsNotification(teams, {
|
||||
title: "✅ Docker Cleanup",
|
||||
facts: [
|
||||
{ name: "Date", value: format(date, "PP pp") },
|
||||
{ name: "Message", value: message },
|
||||
],
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
sendPushoverNotification,
|
||||
sendResendNotification,
|
||||
sendSlackNotification,
|
||||
sendTeamsNotification,
|
||||
sendTelegramNotification,
|
||||
} from "./utils";
|
||||
|
||||
@@ -33,6 +34,7 @@ export const sendDokployRestartNotifications = async () => {
|
||||
custom: true,
|
||||
lark: true,
|
||||
pushover: true,
|
||||
teams: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -48,6 +50,7 @@ export const sendDokployRestartNotifications = async () => {
|
||||
custom,
|
||||
lark,
|
||||
pushover,
|
||||
teams,
|
||||
} = notification;
|
||||
|
||||
try {
|
||||
@@ -251,6 +254,16 @@ export const sendDokployRestartNotifications = async () => {
|
||||
`Date: ${date.toLocaleString()}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (teams) {
|
||||
await sendTeamsNotification(teams, {
|
||||
title: "✅ Dokploy Server Restarted",
|
||||
facts: [
|
||||
{ name: "Status", value: "Successful" },
|
||||
{ name: "Restart Time", value: format(date, "PP pp") },
|
||||
],
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
sendLarkNotification,
|
||||
sendPushoverNotification,
|
||||
sendSlackNotification,
|
||||
sendTeamsNotification,
|
||||
sendTelegramNotification,
|
||||
} from "./utils";
|
||||
|
||||
@@ -40,6 +41,7 @@ export const sendServerThresholdNotifications = async (
|
||||
custom: true,
|
||||
lark: true,
|
||||
pushover: true,
|
||||
teams: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -47,7 +49,8 @@ export const sendServerThresholdNotifications = async (
|
||||
const typeColor = 0xff0000; // Rojo para indicar alerta
|
||||
|
||||
for (const notification of notificationList) {
|
||||
const { discord, telegram, slack, custom, lark, pushover } = notification;
|
||||
const { discord, telegram, slack, custom, lark, pushover, teams } =
|
||||
notification;
|
||||
|
||||
if (discord) {
|
||||
const decorate = (decoration: string, text: string) =>
|
||||
@@ -276,5 +279,19 @@ export const sendServerThresholdNotifications = async (
|
||||
`Server: ${payload.ServerName}\nType: ${payload.Type}\nCurrent: ${payload.Value.toFixed(2)}%\nThreshold: ${payload.Threshold.toFixed(2)}%\nMessage: ${payload.Message}\nTime: ${date.toLocaleString()}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (teams) {
|
||||
await sendTeamsNotification(teams, {
|
||||
title: `⚠️ Server ${payload.Type} Alert`,
|
||||
facts: [
|
||||
{ name: "Server Name", value: payload.ServerName },
|
||||
{ name: "Type", value: payload.Type },
|
||||
{ name: "Current Value", value: `${payload.Value.toFixed(2)}%` },
|
||||
{ name: "Threshold", value: `${payload.Threshold.toFixed(2)}%` },
|
||||
{ name: "Time", value: date.toLocaleString() },
|
||||
{ name: "Message", value: payload.Message },
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -8,6 +8,7 @@ import type {
|
||||
pushover,
|
||||
resend,
|
||||
slack,
|
||||
teams,
|
||||
telegram,
|
||||
} from "@dokploy/server/db/schema";
|
||||
import nodemailer from "nodemailer";
|
||||
@@ -253,6 +254,84 @@ export const sendLarkNotification = async (
|
||||
}
|
||||
};
|
||||
|
||||
export interface TeamsAdaptiveCardMessage {
|
||||
title: string;
|
||||
themeColor?: string;
|
||||
facts?: { name: string; value: string }[];
|
||||
potentialAction?: { type: "Action.OpenUrl"; title: string; url: string };
|
||||
}
|
||||
|
||||
export const sendTeamsNotification = async (
|
||||
connection: typeof teams.$inferInsert,
|
||||
message: TeamsAdaptiveCardMessage,
|
||||
) => {
|
||||
try {
|
||||
const bodyElements: Record<string, unknown>[] = [
|
||||
{
|
||||
type: "TextBlock",
|
||||
text: message.title,
|
||||
size: "Medium",
|
||||
weight: "Bolder",
|
||||
wrap: true,
|
||||
},
|
||||
];
|
||||
|
||||
if (message.facts && message.facts.length > 0) {
|
||||
bodyElements.push({
|
||||
type: "FactSet",
|
||||
facts: message.facts.map((f) => ({
|
||||
title: f.name,
|
||||
value: f.value,
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
const cardContent: Record<string, unknown> = {
|
||||
type: "AdaptiveCard",
|
||||
$schema: "http://adaptivecards.io/schemas/adaptive-card.json",
|
||||
version: "1.4",
|
||||
body: bodyElements,
|
||||
};
|
||||
|
||||
if (message.potentialAction) {
|
||||
cardContent.actions = [
|
||||
{
|
||||
type: "Action.OpenUrl",
|
||||
title: message.potentialAction.title,
|
||||
url: message.potentialAction.url,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
const payload = {
|
||||
type: "message",
|
||||
attachments: [
|
||||
{
|
||||
contentType: "application/vnd.microsoft.card.adaptive",
|
||||
content: cardContent,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const response = await fetch(connection.webhookUrl, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to send Teams notification: ${response.statusText}`,
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
throw new Error(
|
||||
`Failed to send Teams notification ${err instanceof Error ? err.message : "Unknown error"}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const sendPushoverNotification = async (
|
||||
connection: typeof pushover.$inferInsert,
|
||||
title: string,
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
sendPushoverNotification,
|
||||
sendResendNotification,
|
||||
sendSlackNotification,
|
||||
sendTeamsNotification,
|
||||
sendTelegramNotification,
|
||||
} from "./utils";
|
||||
|
||||
@@ -57,12 +58,22 @@ export const sendVolumeBackupNotifications = async ({
|
||||
gotify: true,
|
||||
ntfy: true,
|
||||
pushover: true,
|
||||
teams: true,
|
||||
},
|
||||
});
|
||||
|
||||
for (const notification of notificationList) {
|
||||
const { email, resend, discord, telegram, slack, gotify, ntfy, pushover } =
|
||||
notification;
|
||||
const {
|
||||
email,
|
||||
resend,
|
||||
discord,
|
||||
telegram,
|
||||
slack,
|
||||
gotify,
|
||||
ntfy,
|
||||
pushover,
|
||||
teams,
|
||||
} = notification;
|
||||
|
||||
if (email || resend) {
|
||||
const subject = `Volume Backup ${type === "success" ? "Successful" : "Failed"} - ${applicationName}`;
|
||||
@@ -288,5 +299,29 @@ export const sendVolumeBackupNotifications = async ({
|
||||
`Project: ${projectName}\nApplication: ${applicationName}\nVolume: ${volumeName}\nService Type: ${serviceType}${backupSize ? `\nBackup Size: ${backupSize}` : ""}\nDate: ${date.toLocaleString()}${type === "error" && errorMessage ? `\nError: ${errorMessage}` : ""}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (teams) {
|
||||
const facts = [
|
||||
{ name: "Project", value: projectName },
|
||||
{ name: "Application", value: applicationName },
|
||||
{ name: "Volume Name", value: volumeName },
|
||||
{ name: "Service Type", value: serviceType },
|
||||
{ name: "Date", value: format(date, "PP pp") },
|
||||
{ name: "Status", value: type === "success" ? "Successful" : "Failed" },
|
||||
];
|
||||
if (backupSize) {
|
||||
facts.push({ name: "Backup Size", value: backupSize });
|
||||
}
|
||||
if (type === "error" && errorMessage) {
|
||||
facts.push({ name: "Error", value: errorMessage.substring(0, 500) });
|
||||
}
|
||||
await sendTeamsNotification(teams, {
|
||||
title:
|
||||
type === "success"
|
||||
? "✅ Volume Backup Successful"
|
||||
: "❌ Volume Backup Failed",
|
||||
facts,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user