diff --git a/client/src/Hooks/useNotificationForm.ts b/client/src/Hooks/useNotificationForm.ts index dd882db67..c45773db7 100644 --- a/client/src/Hooks/useNotificationForm.ts +++ b/client/src/Hooks/useNotificationForm.ts @@ -1,28 +1,76 @@ import { useMemo } from "react"; import { notificationSchema } from "@/Validation/notifications"; +import type { NotificationFormData } from "@/Validation/notifications"; import type { Notification } from "@/Types/Notification"; interface UseNotificationFormOptions { data?: Notification | null; } +function buildDefaults(data: Notification | null): NotificationFormData { + if (data?.type === "matrix") { + return { + type: "matrix", + notificationName: data.notificationName || "", + homeserverUrl: data.homeserverUrl || "", + roomId: data.roomId || "", + accessToken: data.accessToken || "", + }; + } + if (data?.type === "telegram") { + return { + type: "telegram", + notificationName: data.notificationName || "", + address: data.address || "", + accessToken: data.accessToken || "", + }; + } + if (data?.type === "slack") { + return { + type: "slack", + notificationName: data.notificationName || "", + address: data.address || "", + }; + } + if (data?.type === "discord") { + return { + type: "discord", + notificationName: data.notificationName || "", + address: data.address || "", + }; + } + if (data?.type === "webhook") { + return { + type: "webhook", + notificationName: data.notificationName || "", + address: data.address || "", + }; + } + if (data?.type === "pager_duty") { + return { + type: "pager_duty", + notificationName: data.notificationName || "", + address: data.address || "", + }; + } + if (data?.type === "teams") { + return { + type: "teams", + notificationName: data.notificationName || "", + address: data.address || "", + }; + } + // Default: email (covers both data === null and data.type === "email") + return { + type: "email", + notificationName: data?.notificationName || "", + address: data?.address || "", + }; +} + export const useNotificationForm = ({ data = null }: UseNotificationFormOptions = {}) => { return useMemo(() => { - const defaults = - data?.type === "matrix" - ? { - type: "matrix" as const, - notificationName: data.notificationName || "", - homeserverUrl: data.homeserverUrl || "", - roomId: data.roomId || "", - accessToken: data.accessToken || "", - } - : { - type: (data?.type || "email") as Exclude, - notificationName: data?.notificationName || "", - address: data?.address || "", - }; - + const defaults = buildDefaults(data); return { schema: notificationSchema, defaults }; }, [data]); }; diff --git a/client/src/Pages/Notifications/create/index.tsx b/client/src/Pages/Notifications/create/index.tsx index 7aa7bc831..85568f8bd 100644 --- a/client/src/Pages/Notifications/create/index.tsx +++ b/client/src/Pages/Notifications/create/index.tsx @@ -147,7 +147,7 @@ const NotificationsCreatePage = () => { /> } /> - {watchedType !== "matrix" && ( + {watchedType !== "matrix" && watchedType !== "telegram" && ( { ( { } /> )} + {watchedType === "telegram" && ( + + ( + + )} + /> + ( + + )} + /> + + } + /> + )} {watchedType === "matrix" && ( { ( { ( { ( ; diff --git a/client/src/locales/en.json b/client/src/locales/en.json index 92a21939f..83ca4fa33 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -951,6 +951,14 @@ "description": "Select the type of notification channel to create.", "optionType": "Type", "title": "Channel type" + }, + "telegram": { + "title": "Telegram configuration", + "description": "Configure your Telegram bot to send notifications.", + "optionBotToken": "Bot token", + "placeholderBotToken": "123456789:ABCdefGhIJKlmNoPQRsTUVwxyZ", + "optionChatId": "Chat ID", + "placeholderChatId": "-1001234567890" } }, "table": { diff --git a/server/src/config/services.ts b/server/src/config/services.ts index c944e36c2..8ed72862a 100644 --- a/server/src/config/services.ts +++ b/server/src/config/services.ts @@ -28,6 +28,7 @@ import { PagerDutyProvider, MatrixProvider, TeamsProvider, + TelegramProvider, // Interfaces INetworkService, IEmailService, @@ -296,6 +297,7 @@ export const initializeServices = async ({ const pagerDutyProvider = new PagerDutyProvider(logger); const matrixProvider = new MatrixProvider(logger); const teamsProvider = new TeamsProvider(logger); + const telegramProvider = new TelegramProvider(logger); const notificationsService = new NotificationsService( notificationsRepository, @@ -307,6 +309,7 @@ export const initializeServices = async ({ pagerDutyProvider, matrixProvider, teamsProvider, + telegramProvider, settingsService, logger, notificationMessageBuilder diff --git a/server/src/db/models/Notification.ts b/server/src/db/models/Notification.ts index 9ea3f17bd..5f6d234dd 100755 --- a/server/src/db/models/Notification.ts +++ b/server/src/db/models/Notification.ts @@ -25,7 +25,7 @@ const NotificationSchema = new Schema( }, type: { type: String, - enum: ["email", "slack", "discord", "webhook", "pager_duty", "matrix", "teams"] as NotificationChannel[], + enum: ["email", "slack", "discord", "webhook", "pager_duty", "matrix", "teams", "telegram"] as NotificationChannel[], required: true, }, notificationName: { diff --git a/server/src/service/index.ts b/server/src/service/index.ts index 9686d2a13..1e3b20ba6 100644 --- a/server/src/service/index.ts +++ b/server/src/service/index.ts @@ -29,6 +29,7 @@ export * from "@/service/infrastructure/notificationProviders/pagerduty.js"; export * from "@/service/infrastructure/notificationProviders/slack.js"; export * from "@/service/infrastructure/notificationProviders/teams.js"; export * from "@/service/infrastructure/notificationProviders/webhook.js"; +export * from "@/service/infrastructure/notificationProviders/telegram.js"; // System services export * from "@/service/system/settingsService.js"; diff --git a/server/src/service/infrastructure/notificationProviders/telegram.ts b/server/src/service/infrastructure/notificationProviders/telegram.ts new file mode 100644 index 000000000..fcb7bdacc --- /dev/null +++ b/server/src/service/infrastructure/notificationProviders/telegram.ts @@ -0,0 +1,113 @@ +const SERVICE_NAME = "TelegramProvider"; +import type { Notification } from "@/types/index.js"; +import { INotificationProvider } from "@/service/index.js"; +import type { NotificationMessage } from "@/types/notificationMessage.js"; +import { getTestMessage } from "@/service/infrastructure/notificationProviders/utils.js"; +import got from "got"; +import { ILogger } from "@/utils/logger.js"; + +export class TelegramProvider implements INotificationProvider { + private logger: ILogger; + + constructor(logger: ILogger) { + this.logger = logger; + } + + async sendTestAlert(notification: Partial): Promise { + if (!notification.address || !notification.accessToken) { + return false; + } + + try { + await got.post(`https://api.telegram.org/bot${notification.accessToken}/sendMessage`, { + json: { + chat_id: notification.address, + text: getTestMessage(), + parse_mode: "HTML", + }, + }); + return true; + } catch (error) { + const errMsg = error instanceof Error ? error.message : "unknown error"; + const errStack = error instanceof Error ? error.stack : undefined; + this.logger.warn({ + message: "Telegram test alert failed", + service: SERVICE_NAME, + method: "sendTestAlert", + stack: errStack, + details: { error: errMsg }, + }); + return false; + } + } + + async sendMessage(notification: Notification, message: NotificationMessage): Promise { + if (!notification.address || !notification.accessToken) { + return false; + } + + const text = this.buildTelegramText(message); + + try { + await got.post(`https://api.telegram.org/bot${notification.accessToken}/sendMessage`, { + json: { + chat_id: notification.address, + text, + parse_mode: "HTML", + }, + }); + this.logger.info({ + message: "Telegram notification sent", + service: SERVICE_NAME, + method: "sendMessage", + }); + return true; + } catch (error) { + const errMsg = error instanceof Error ? error.message : "unknown error"; + const errStack = error instanceof Error ? error.stack : undefined; + this.logger.warn({ + message: "Telegram alert failed", + service: SERVICE_NAME, + method: "sendMessage", + stack: errStack, + details: { error: errMsg }, + }); + return false; + } + } + + private buildTelegramText(message: NotificationMessage): string { + const lines: string[] = []; + + lines.push(`${message.content.title}`); + lines.push(message.content.summary); + lines.push(""); + + lines.push("Monitor Details:"); + lines.push(`• Name: ${message.monitor.name}`); + lines.push(`• URL: ${message.monitor.url}`); + lines.push(`• Type: ${message.monitor.type}`); + lines.push(`• Status: ${message.monitor.status}`); + + if (message.content.details && message.content.details.length > 0) { + lines.push(""); + lines.push("Additional Information:"); + message.content.details.forEach((detail) => lines.push(`• ${detail}`)); + } + + if (message.content.thresholds && message.content.thresholds.length > 0) { + lines.push(""); + lines.push("Threshold Breaches:"); + message.content.thresholds.forEach((breach) => { + lines.push(`• ${breach.metric.toUpperCase()}: ${breach.formattedValue} (threshold: ${breach.threshold}${breach.unit})`); + }); + } + + if (message.content.incident) { + lines.push(""); + lines.push(`View Incident`); + } + + return lines.join("\n"); + } +} diff --git a/server/src/service/infrastructure/notificationsService.ts b/server/src/service/infrastructure/notificationsService.ts index c75477c88..b131c4ff9 100644 --- a/server/src/service/infrastructure/notificationsService.ts +++ b/server/src/service/infrastructure/notificationsService.ts @@ -33,6 +33,7 @@ export class NotificationsService implements INotificationsService { private pagerDutyProvider: INotificationProvider; private matrixProvider: INotificationProvider; private teamsProvider: INotificationProvider; + private telegramProvider: INotificationProvider; private logger: ILogger; private settingsService: ISettingsService; private notificationMessageBuilder: INotificationMessageBuilder; @@ -47,6 +48,7 @@ export class NotificationsService implements INotificationsService { pagerDutyProvider: INotificationProvider, matrixProvider: INotificationProvider, teamsProvider: INotificationProvider, + telegramProvider: INotificationProvider, settingsService: ISettingsService, logger: ILogger, notificationMessageBuilder: INotificationMessageBuilder @@ -60,6 +62,7 @@ export class NotificationsService implements INotificationsService { this.pagerDutyProvider = pagerDutyProvider; this.matrixProvider = matrixProvider; this.teamsProvider = teamsProvider; + this.telegramProvider = telegramProvider; this.settingsService = settingsService; this.logger = logger; this.notificationMessageBuilder = notificationMessageBuilder; @@ -97,6 +100,8 @@ export class NotificationsService implements INotificationsService { return await this.emailProvider.sendMessage!(notification, notificationMessage); case "teams": return await this.teamsProvider.sendMessage!(notification, notificationMessage); + case "telegram": + return await this.telegramProvider.sendMessage!(notification, notificationMessage); default: this.logger.warn({ message: `Unknown notification type: ${notification.type}`, @@ -157,6 +162,8 @@ export class NotificationsService implements INotificationsService { return await this.webhookProvider.sendTestAlert(notification); case "teams": return await this.teamsProvider.sendTestAlert(notification); + case "telegram": + return await this.telegramProvider.sendTestAlert(notification); default: return false; } diff --git a/server/src/types/notification.ts b/server/src/types/notification.ts index a121cd8e0..d4c47ae76 100644 --- a/server/src/types/notification.ts +++ b/server/src/types/notification.ts @@ -1,4 +1,4 @@ -export const NotificationChannels = ["email", "slack", "discord", "webhook", "pager_duty", "matrix", "teams"] as const; +export const NotificationChannels = ["email", "slack", "discord", "webhook", "pager_duty", "matrix", "teams", "telegram"] as const; export type NotificationChannel = (typeof NotificationChannels)[number]; export interface Notification { diff --git a/server/src/validation/notificationValidation.ts b/server/src/validation/notificationValidation.ts index 2f261d823..e9705c1ad 100644 --- a/server/src/validation/notificationValidation.ts +++ b/server/src/validation/notificationValidation.ts @@ -65,6 +65,13 @@ export const createNotificationBodyValidation = z.discriminatedUnion("type", [ type: z.literal("teams"), address: z.url({ message: "Please enter a valid Webhook URL" }), }), + // Telegram notification + z.object({ + notificationName: z.string().min(1, "Notification name is required"), + type: z.literal("telegram"), + address: z.string().min(1, "Chat ID is required"), + accessToken: z.string().min(1, "Bot token is required"), + }), ]); export const testNotificationBodyValidation = createNotificationBodyValidation;