feat(notifications): add Telegram notification channel (#2753)
Add Telegram as a new notification type, allowing users to receive monitor alerts via a Telegram bot. Configuration requires: - Bot token (obtained from @BotFather) - Chat ID (user, group, or channel) Changes: - TelegramProvider: sends HTML-formatted messages via Telegram Bot API - NotificationsService: route telegram type to TelegramProvider - Notification model/types: add 'telegram' to NotificationChannel enum - Server validation: telegram schema (accessToken + address/chat_id) - Client Types/Validation: telegram schema with accessToken + address - useNotificationForm: handle telegram defaults - Notifications create page: render Telegram config UI - en.json: add telegram i18n keys
This commit is contained in:
1 parent
c24b41ecd9
commit
eacbb227c3
12 files changed
+1521
-1329
No files matched your search
@@ -17,11 +17,18 @@ export const useNotificationForm = ({ data = null }: UseNotificationFormOptions
|
||||
roomId: data.roomId || "",
|
||||
accessToken: data.accessToken || "",
|
||||
}
|
||||
: {
|
||||
type: (data?.type || "email") as Exclude<Notification["type"], "matrix">,
|
||||
notificationName: data?.notificationName || "",
|
||||
address: data?.address || "",
|
||||
};
|
||||
: data?.type === "telegram"
|
||||
? {
|
||||
type: "telegram" as const,
|
||||
notificationName: data.notificationName || "",
|
||||
address: data.address || "",
|
||||
accessToken: data.accessToken || "",
|
||||
}
|
||||
: {
|
||||
type: (data?.type || "email") as Exclude<Notification["type"], "matrix" | "telegram">,
|
||||
notificationName: data?.notificationName || "",
|
||||
address: data?.address || "",
|
||||
};
|
||||
|
||||
return { schema: notificationSchema, defaults };
|
||||
}, [data]);
|
||||
|
||||
@@ -147,7 +147,7 @@ const NotificationsCreatePage = () => {
|
||||
/>
|
||||
}
|
||||
/>
|
||||
{watchedType !== "matrix" && (
|
||||
{watchedType !== "matrix" && watchedType !== "telegram" && (
|
||||
<ConfigBox
|
||||
title={addressConfig.title}
|
||||
subtitle={addressConfig.description}
|
||||
@@ -171,6 +171,48 @@ const NotificationsCreatePage = () => {
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{watchedType === "telegram" && (
|
||||
<ConfigBox
|
||||
title={t("pages.notifications.form.telegram.title")}
|
||||
subtitle={t("pages.notifications.form.telegram.description")}
|
||||
rightContent={
|
||||
<Stack spacing={theme.spacing(8)}>
|
||||
<Controller
|
||||
name="accessToken"
|
||||
control={control}
|
||||
defaultValue={defaults.accessToken}
|
||||
render={({ field, fieldState }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
type="text"
|
||||
fieldLabel={t("pages.notifications.form.telegram.optionBotToken")}
|
||||
placeholder={t("pages.notifications.form.telegram.placeholderBotToken")}
|
||||
fullWidth
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message ?? ""}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="address"
|
||||
control={control}
|
||||
defaultValue={defaults.address}
|
||||
render={({ field, fieldState }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
type="text"
|
||||
fieldLabel={t("pages.notifications.form.telegram.optionChatId")}
|
||||
placeholder={t("pages.notifications.form.telegram.placeholderChatId")}
|
||||
fullWidth
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message ?? ""}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Stack>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{watchedType === "matrix" && (
|
||||
<ConfigBox
|
||||
title={t("pages.notifications.form.matrix.title")}
|
||||
|
||||
@@ -6,6 +6,7 @@ export const NotificationChannels = [
|
||||
"pager_duty",
|
||||
"matrix",
|
||||
"teams",
|
||||
"telegram",
|
||||
] as const;
|
||||
export type NotificationChannel = (typeof NotificationChannels)[number];
|
||||
|
||||
|
||||
@@ -50,6 +50,12 @@ const teamsSchema = baseSchema.extend({
|
||||
address: z.string().min(1, "Webhook URL is required").url("Please enter a valid URL"),
|
||||
});
|
||||
|
||||
const telegramSchema = baseSchema.extend({
|
||||
type: z.literal("telegram"),
|
||||
address: z.string().min(1, "Chat ID is required"),
|
||||
accessToken: z.string().min(1, "Bot token is required"),
|
||||
});
|
||||
|
||||
export const notificationSchema = z.discriminatedUnion("type", [
|
||||
emailSchema,
|
||||
slackSchema,
|
||||
@@ -58,6 +64,7 @@ export const notificationSchema = z.discriminatedUnion("type", [
|
||||
pagerDutySchema,
|
||||
matrixSchema,
|
||||
teamsSchema,
|
||||
telegramSchema,
|
||||
]);
|
||||
|
||||
export type NotificationFormData = z.infer<typeof notificationSchema>;
|
||||
+1329
-1321
File diff suppressed because it is too large.
Load diff
@@ -28,6 +28,7 @@ import {
|
||||
PagerDutyProvider,
|
||||
MatrixProvider,
|
||||
TeamsProvider,
|
||||
TelegramProvider,
|
||||
// Interfaces
|
||||
INetworkService,
|
||||
IEmailService,
|
||||
@@ -230,6 +231,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,
|
||||
@@ -241,6 +243,7 @@ export const initializeServices = async ({
|
||||
pagerDutyProvider,
|
||||
matrixProvider,
|
||||
teamsProvider,
|
||||
telegramProvider,
|
||||
settingsService,
|
||||
logger,
|
||||
notificationMessageBuilder
|
||||
|
||||
@@ -25,7 +25,7 @@ const NotificationSchema = new Schema<NotificationDocument>(
|
||||
},
|
||||
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: {
|
||||
|
||||
@@ -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";
|
||||
@@ -0,0 +1,109 @@
|
||||
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, { HTTPError } 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<Notification>): Promise<boolean> {
|
||||
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 err = error as HTTPError;
|
||||
this.logger.warn({
|
||||
message: "Telegram test alert failed",
|
||||
service: SERVICE_NAME,
|
||||
method: "sendTestAlert",
|
||||
stack: err?.stack,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async sendMessage(notification: Notification, message: NotificationMessage): Promise<boolean> {
|
||||
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 err = error as HTTPError;
|
||||
this.logger.warn({
|
||||
message: "Telegram alert failed",
|
||||
service: SERVICE_NAME,
|
||||
method: "sendMessage",
|
||||
stack: err?.stack,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private buildTelegramText(message: NotificationMessage): string {
|
||||
const lines: string[] = [];
|
||||
|
||||
lines.push(`<b>${message.content.title}</b>`);
|
||||
lines.push(message.content.summary);
|
||||
lines.push("");
|
||||
|
||||
lines.push("<b>Monitor Details:</b>");
|
||||
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("<b>Additional Information:</b>");
|
||||
message.content.details.forEach((detail) => lines.push(`• ${detail}`));
|
||||
}
|
||||
|
||||
if (message.content.thresholds && message.content.thresholds.length > 0) {
|
||||
lines.push("");
|
||||
lines.push("<b>Threshold Breaches:</b>");
|
||||
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(`<a href="${message.clientHost}/infrastructure/${message.monitor.id}">View Incident</a>`);
|
||||
}
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user