diff --git a/packages/server/src/utils/notifications/build-error.ts b/packages/server/src/utils/notifications/build-error.ts index 7c299809b..465a7c006 100644 --- a/packages/server/src/utils/notifications/build-error.ts +++ b/packages/server/src/utils/notifications/build-error.ts @@ -5,6 +5,8 @@ import { render } from "@react-email/components"; import { format } from "date-fns"; import { and, eq } from "drizzle-orm"; import { + escapeHtml, + formatTelegramErrorMessage, sendCustomNotification, sendDiscordNotification, sendEmailNotification, @@ -201,7 +203,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: ${escapeHtml(projectName)}\nApplication: ${escapeHtml(applicationName)}\nType: ${escapeHtml(applicationType)}\nDate: ${format(date, "PP")}\nTime: ${format(date, "pp")}\n\nError:\n
${formatTelegramErrorMessage(errorMessage)}
`, inlineButton, ); } diff --git a/packages/server/src/utils/notifications/build-success.ts b/packages/server/src/utils/notifications/build-success.ts index 13ed68a77..658acdc9b 100644 --- a/packages/server/src/utils/notifications/build-success.ts +++ b/packages/server/src/utils/notifications/build-success.ts @@ -6,6 +6,7 @@ import { render } from "@react-email/components"; import { format } from "date-fns"; import { and, eq } from "drizzle-orm"; import { + escapeHtml, sendCustomNotification, sendDiscordNotification, sendEmailNotification, @@ -214,7 +215,7 @@ export const sendBuildSuccessNotifications = async ({ await sendTelegramNotification( telegram, - `✅ Build Success\n\nProject: ${projectName}\nApplication: ${applicationName}\nEnvironment: ${environmentName}\nType: ${applicationType}\nDate: ${format( + `✅ Build Success\n\nProject: ${escapeHtml(projectName)}\nApplication: ${escapeHtml(applicationName)}\nEnvironment: ${escapeHtml(environmentName)}\nType: ${escapeHtml(applicationType)}\nDate: ${format( date, "PP", )}\nTime: ${format(date, "pp")}`, diff --git a/packages/server/src/utils/notifications/database-backup.ts b/packages/server/src/utils/notifications/database-backup.ts index 7a838ff46..e6b8ffa60 100644 --- a/packages/server/src/utils/notifications/database-backup.ts +++ b/packages/server/src/utils/notifications/database-backup.ts @@ -5,6 +5,8 @@ import { render } from "@react-email/components"; import { format } from "date-fns"; import { and, eq } from "drizzle-orm"; import { + escapeHtml, + formatTelegramErrorMessage, sendCustomNotification, sendDiscordNotification, sendEmailNotification, @@ -208,10 +210,10 @@ 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 : ""}`; + const messageText = `${statusEmoji} Database Backup ${typeStatus}\n\nProject: ${escapeHtml(projectName)}\nApplication: ${escapeHtml(applicationName)}\nType: ${escapeHtml(databaseType)}\nDatabase Name: ${escapeHtml(databaseName)}\nDate: ${format(date, "PP")}\nTime: ${format(date, "pp")}${isError ? errorMsg : ""}`; await sendTelegramNotification(telegram, messageText); } 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..07048c8df 100644 --- a/packages/server/src/utils/notifications/dokploy-backup.ts +++ b/packages/server/src/utils/notifications/dokploy-backup.ts @@ -5,6 +5,8 @@ import { render } from "@react-email/components"; import { format } from "date-fns"; import { eq } from "drizzle-orm"; import { + escapeHtml, + formatTelegramErrorMessage, sendCustomNotification, sendDiscordNotification, sendEmailNotification, @@ -186,10 +188,10 @@ 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}` + ? `\nBackup Size: ${escapeHtml(backupSize)}` : ""; const messageText = `${statusEmoji} Dokploy Backup ${typeStatus}\n\nBackup Type: Complete Dokploy Instance${sizeInfo}\nDate: ${format(date, "PP")}\nTime: ${format(date, "pp")}${isError ? errorMsg : ""}`; diff --git a/packages/server/src/utils/notifications/server-threshold.ts b/packages/server/src/utils/notifications/server-threshold.ts index a8749a349..4e829f79e 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, @@ -189,11 +190,11 @@ export const sendServerThresholdNotifications = async ( telegram, ` ⚠️ Server ${payload.Type} Alert - Server Name: ${payload.ServerName} + Server Name: ${escapeHtml(payload.ServerName)} 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..a5603d975 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..54f9b97a4 100644 --- a/packages/server/src/utils/notifications/volume-backup.ts +++ b/packages/server/src/utils/notifications/volume-backup.ts @@ -5,6 +5,8 @@ import { render } from "@react-email/components"; import { format } from "date-fns"; import { and, eq } from "drizzle-orm"; import { + escapeHtml, + formatTelegramErrorMessage, sendCustomNotification, sendDiscordNotification, sendEmailNotification, @@ -223,13 +225,13 @@ 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}` + ? `\nBackup Size: ${escapeHtml(backupSize)}` : ""; - const messageText = `${statusEmoji} Volume Backup ${typeStatus}\n\nProject: ${projectName}\nApplication: ${applicationName}\nVolume Name: ${volumeName}\nService Type: ${serviceType}${sizeInfo}\nDate: ${format(date, "PP")}\nTime: ${format(date, "pp")}${isError ? errorMsg : ""}`; + const messageText = `${statusEmoji} Volume Backup ${typeStatus}\n\nProject: ${escapeHtml(projectName)}\nApplication: ${escapeHtml(applicationName)}\nVolume Name: ${escapeHtml(volumeName)}\nService Type: ${escapeHtml(serviceType)}${sizeInfo}\nDate: ${format(date, "PP")}\nTime: ${format(date, "pp")}${isError ? errorMsg : ""}`; await sendTelegramNotification(telegram, messageText); }