Merge pull request #3729 from Dokploy/feat/add-teams-notification-provider

feat(notifications): add Microsoft Teams integration for notifications
This commit is contained in:
Mauricio Siu
2026-02-16 21:23:09 -06:00
committed by GitHub
18 changed files with 7894 additions and 5 deletions

View File

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

View File

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

View File

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

View 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;

File diff suppressed because it is too large Load Diff

View File

@@ -1009,6 +1009,13 @@
"when": 1770961667210,
"tag": "0143_brown_ultron",
"breakpoints": true
},
{
"idx": 144,
"version": "7",
"when": 1771297084611,
"tag": "0144_odd_gunslinger",
"breakpoints": true
}
]
}

View File

@@ -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 }) => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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