From 7430afd701f3db36e4a90b808fe5c81f709a4ae0 Mon Sep 17 00:00:00 2001 From: Yash Kumar Date: Fri, 16 Oct 2026 15:53:30 +0530 Subject: [PATCH] fix: escape HTML and truncate long messages in Telegram notifications --- .../src/utils/notifications/build-error.ts | 3 +- .../utils/notifications/database-backup.ts | 3 +- .../src/utils/notifications/docker-cleanup.ts | 3 +- .../src/utils/notifications/dokploy-backup.ts | 3 +- .../utils/notifications/server-threshold.ts | 3 +- .../server/src/utils/notifications/utils.ts | 35 ++++++++++++++++++- .../src/utils/notifications/volume-backup.ts | 3 +- 7 files changed, 46 insertions(+), 7 deletions(-) diff --git a/packages/server/src/utils/notifications/build-error.ts b/packages/server/src/utils/notifications/build-error.ts index 7c299809b..51c10232b 100644 --- a/packages/server/src/utils/notifications/build-error.ts +++ b/packages/server/src/utils/notifications/build-error.ts @@ -5,6 +5,7 @@ import { render } from "@react-email/components"; import { format } from "date-fns"; import { and, eq } from "drizzle-orm"; import { + formatTelegramErrorMessage, sendCustomNotification, sendDiscordNotification, sendEmailNotification, @@ -201,7 +202,7 @@ export const sendBuildErrorNotifications = async ({ await sendTelegramNotification( telegram, - `⚠️ Build Failed\n\nProject: ${projectName}\nApplication: ${applicationName}\nType: ${applicationType}\nDate: ${format(date, "PP")}\nTime: ${format(date, "pp")}\n\nError:\n
${errorMessage}
`, + `⚠️ Build Failed\n\nProject: ${projectName}\nApplication: ${applicationName}\nType: ${applicationType}\nDate: ${format(date, "PP")}\nTime: ${format(date, "pp")}\n\nError:\n
${formatTelegramErrorMessage(errorMessage)}
`, inlineButton, ); } diff --git a/packages/server/src/utils/notifications/database-backup.ts b/packages/server/src/utils/notifications/database-backup.ts index 7a838ff46..70c111947 100644 --- a/packages/server/src/utils/notifications/database-backup.ts +++ b/packages/server/src/utils/notifications/database-backup.ts @@ -5,6 +5,7 @@ import { render } from "@react-email/components"; import { format } from "date-fns"; import { and, eq } from "drizzle-orm"; import { + formatTelegramErrorMessage, sendCustomNotification, sendDiscordNotification, sendEmailNotification, @@ -208,7 +209,7 @@ export const sendDatabaseBackupNotifications = async ({ const statusEmoji = type === "success" ? "✅" : "❌"; const typeStatus = type === "success" ? "Successful" : "Failed"; const errorMsg = isError - ? `\n\nError:\n
${errorMessage}
` + ? `\n\nError:\n
${formatTelegramErrorMessage(errorMessage)}
` : ""; const messageText = `${statusEmoji} Database Backup ${typeStatus}\n\nProject: ${projectName}\nApplication: ${applicationName}\nType: ${databaseType}\nDatabase Name: ${databaseName}\nDate: ${format(date, "PP")}\nTime: ${format(date, "pp")}${isError ? errorMsg : ""}`; diff --git a/packages/server/src/utils/notifications/docker-cleanup.ts b/packages/server/src/utils/notifications/docker-cleanup.ts index 98d4cec53..38a3cc068 100644 --- a/packages/server/src/utils/notifications/docker-cleanup.ts +++ b/packages/server/src/utils/notifications/docker-cleanup.ts @@ -5,6 +5,7 @@ import { render } from "@react-email/components"; import { format } from "date-fns"; import { and, eq } from "drizzle-orm"; import { + escapeHtml, sendCustomNotification, sendDiscordNotification, sendEmailNotification, @@ -143,7 +144,7 @@ export const sendDockerCleanupNotifications = async ( if (telegram) { await sendTelegramNotification( telegram, - `✅ Docker Cleanup\n\nMessage: ${message}\nDate: ${format(date, "PP")}\nTime: ${format(date, "pp")}`, + `✅ Docker Cleanup\n\nMessage: ${escapeHtml(message)}\nDate: ${format(date, "PP")}\nTime: ${format(date, "pp")}`, ); } diff --git a/packages/server/src/utils/notifications/dokploy-backup.ts b/packages/server/src/utils/notifications/dokploy-backup.ts index e838534f2..ab5e8a22e 100644 --- a/packages/server/src/utils/notifications/dokploy-backup.ts +++ b/packages/server/src/utils/notifications/dokploy-backup.ts @@ -5,6 +5,7 @@ import { render } from "@react-email/components"; import { format } from "date-fns"; import { eq } from "drizzle-orm"; import { + formatTelegramErrorMessage, sendCustomNotification, sendDiscordNotification, sendEmailNotification, @@ -186,7 +187,7 @@ export const sendDokployBackupNotifications = async ({ const statusEmoji = type === "success" ? "✅" : "❌"; const typeStatus = type === "success" ? "Successful" : "Failed"; const errorMsg = isError - ? `\n\nError:\n
${errorMessage}
` + ? `\n\nError:\n
${formatTelegramErrorMessage(errorMessage)}
` : ""; const sizeInfo = backupSize ? `\nBackup Size: ${backupSize}` diff --git a/packages/server/src/utils/notifications/server-threshold.ts b/packages/server/src/utils/notifications/server-threshold.ts index a8749a349..a32217204 100644 --- a/packages/server/src/utils/notifications/server-threshold.ts +++ b/packages/server/src/utils/notifications/server-threshold.ts @@ -4,6 +4,7 @@ import { db } from "../../db"; import { notifications } from "../../db/schema"; import ServerThresholdEmail from "../../emails/emails/server-threshold"; import { + escapeHtml, sendCustomNotification, sendDiscordNotification, sendEmailNotification, @@ -193,7 +194,7 @@ export const sendServerThresholdNotifications = async ( Type: ${payload.Type} Current Value: ${payload.Value.toFixed(2)}% Threshold: ${payload.Threshold.toFixed(2)}% - Message: ${payload.Message} + Message: ${escapeHtml(payload.Message)} Time: ${date.toLocaleString()} `, ); diff --git a/packages/server/src/utils/notifications/utils.ts b/packages/server/src/utils/notifications/utils.ts index d65258862..506713352 100644 --- a/packages/server/src/utils/notifications/utils.ts +++ b/packages/server/src/utils/notifications/utils.ts @@ -101,6 +101,30 @@ export const sendDiscordNotification = async ( } }; +export const escapeHtml = (s: string) => + s.replace(/&/g, "&").replace(//g, ">"); + +export const formatTelegramErrorMessage = (errorMessage: string) => { + const escaped = escapeHtml(errorMessage); + const limit = 3000; + if (escaped.length <= limit) return escaped; + let end = limit; + const ampIdx = escaped.lastIndexOf("&", end); + if (ampIdx !== -1 && escaped.indexOf(";", ampIdx) > end) { + end = ampIdx; + } + if ( + end > 0 && + end < escaped.length && + isHighSurrogate(escaped.charCodeAt(end - 1)) + ) { + end--; + } + return `${escaped.substring(0, end)}…`; +}; + +const isHighSurrogate = (code: number) => code >= 0xd800 && code <= 0xdbff; + export const sendTelegramNotification = async ( connection: typeof telegram.$inferInsert, messageText: string, @@ -111,7 +135,7 @@ export const sendTelegramNotification = async ( ) => { try { const url = `https://api.telegram.org/bot${connection.botToken}/sendMessage`; - await fetch(url, { + const response = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ @@ -125,8 +149,17 @@ export const sendTelegramNotification = async ( }, }), }); + if (!response.ok) { + const body = await response.text(); + throw new Error( + `Failed to send telegram notification: ${response.status} ${body}`, + ); + } } catch (err) { console.log(err); + throw new Error( + `Failed to send telegram notification ${err instanceof Error ? err.message : "Unknown error"}`, + ); } }; diff --git a/packages/server/src/utils/notifications/volume-backup.ts b/packages/server/src/utils/notifications/volume-backup.ts index cdb1e3a96..2eb9c3c5f 100644 --- a/packages/server/src/utils/notifications/volume-backup.ts +++ b/packages/server/src/utils/notifications/volume-backup.ts @@ -5,6 +5,7 @@ import { render } from "@react-email/components"; import { format } from "date-fns"; import { and, eq } from "drizzle-orm"; import { + formatTelegramErrorMessage, sendCustomNotification, sendDiscordNotification, sendEmailNotification, @@ -223,7 +224,7 @@ export const sendVolumeBackupNotifications = async ({ const statusEmoji = type === "success" ? "✅" : "❌"; const typeStatus = type === "success" ? "Successful" : "Failed"; const errorMsg = isError - ? `\n\nError:\n
${errorMessage}
` + ? `\n\nError:\n
${formatTelegramErrorMessage(errorMessage)}
` : ""; const sizeInfo = backupSize ? `\nBackup Size: ${backupSize}`