From 6675ce50868d5379bedbd6923337935529e21d32 Mon Sep 17 00:00:00 2001 From: GivenBY Date: Sat, 3 Jan 2026 20:52:39 +0530 Subject: [PATCH 1/2] Fix: escape Telegram MarkdownV2 after template rendering --- server/notification-providers/telegram.js | 78 ++++++++++++++++++++++- 1 file changed, 77 insertions(+), 1 deletion(-) diff --git a/server/notification-providers/telegram.js b/server/notification-providers/telegram.js index a98c326d7..5ba971d76 100644 --- a/server/notification-providers/telegram.js +++ b/server/notification-providers/telegram.js @@ -1,9 +1,84 @@ const NotificationProvider = require("./notification-provider"); const axios = require("axios"); +const { Liquid } = require("liquidjs"); +const { DOWN } = require("../../src/util"); class Telegram extends NotificationProvider { name = "telegram"; + /** + * Escapes special characters for Telegram MarkdownV2 format + * @param {string} text Text to escape + * @returns {string} Escaped text + */ + escapeMarkdownV2(text) { + if (!text) { + return text; + } + + // Characters that need to be escaped in MarkdownV2 + // https://core.telegram.org/bots/api#markdownv2-style + return String(text).replace(/[_*[\]()~>#+\-=|{}.!\\]/g, "\\$&"); + } + + /** + * Renders template with optional MarkdownV2 escaping + * @param {string} template The template + * @param {string} msg Base message + * @param {?object} monitorJSON Monitor details + * @param {?object} heartbeatJSON Heartbeat details + * @param {boolean} escapeMarkdown Whether to escape for MarkdownV2 + * @returns {Promise} Rendered template + */ + async renderTemplate(template, msg, monitorJSON, heartbeatJSON, escapeMarkdown = false) { + const engine = new Liquid({ + root: "./no-such-directory-uptime-kuma", + relativeReference: false, + dynamicPartials: false, + }); + + const parsedTpl = engine.parse(template); + + // Defaults + let monitorName = "Monitor Name not available"; + let monitorHostnameOrURL = "testing.hostname"; + + if (monitorJSON !== null) { + monitorName = monitorJSON.name; + monitorHostnameOrURL = this.extractAddress(monitorJSON); + } + + let serviceStatus = "⚠️ Test"; + if (heartbeatJSON !== null) { + serviceStatus = heartbeatJSON.status === DOWN ? "🔴 Down" : "✅ Up"; + } + + // Escape values only when MarkdownV2 is enabled + if (escapeMarkdown) { + msg = this.escapeMarkdownV2(msg); + monitorName = this.escapeMarkdownV2(monitorName); + monitorHostnameOrURL = this.escapeMarkdownV2(monitorHostnameOrURL); + serviceStatus = this.escapeMarkdownV2(serviceStatus); + } + + const context = { + // v1 compatibility (remove in v3) + STATUS: serviceStatus, + NAME: monitorName, + HOSTNAME_OR_URL: monitorHostnameOrURL, + + // Official variables + status: serviceStatus, + name: monitorName, + hostnameOrURL: monitorHostnameOrURL, + monitorJSON, + heartbeatJSON, + msg, + }; + + return engine.render(parsedTpl, context); + } + /** * @inheritdoc */ @@ -24,7 +99,8 @@ class Telegram extends NotificationProvider { } if (notification.telegramUseTemplate) { - params.text = await this.renderTemplate(notification.telegramTemplate, msg, monitorJSON, heartbeatJSON); + const escapeMarkdown = notification.telegramTemplateParseMode === "MarkdownV2"; + params.text = await this.renderTemplate(notification.telegramTemplate, msg, monitorJSON, heartbeatJSON, escapeMarkdown); if (notification.telegramTemplateParseMode !== "plain") { params.parse_mode = notification.telegramTemplateParseMode; From e83039f30bd66307ddeb2c9a8f77eb3e4e5cf128 Mon Sep 17 00:00:00 2001 From: GivenBY Date: Sat, 3 Jan 2026 23:05:36 +0530 Subject: [PATCH 2/2] Fix: reuse base template renderer and escape MarkdownV2 output --- server/notification-providers/telegram.js | 97 ++++++++++------------- 1 file changed, 42 insertions(+), 55 deletions(-) diff --git a/server/notification-providers/telegram.js b/server/notification-providers/telegram.js index 5ba971d76..790f06a69 100644 --- a/server/notification-providers/telegram.js +++ b/server/notification-providers/telegram.js @@ -1,7 +1,5 @@ const NotificationProvider = require("./notification-provider"); const axios = require("axios"); -const { Liquid } = require("liquidjs"); -const { DOWN } = require("../../src/util"); class Telegram extends NotificationProvider { name = "telegram"; @@ -22,61 +20,29 @@ class Telegram extends NotificationProvider { } /** - * Renders template with optional MarkdownV2 escaping - * @param {string} template The template - * @param {string} msg Base message - * @param {?object} monitorJSON Monitor details - * @param {?object} heartbeatJSON Heartbeat details - * @param {boolean} escapeMarkdown Whether to escape for MarkdownV2 - * @returns {Promise} Rendered template + * Recursively escapes string properties of an object for Telegram MarkdownV2 + * @param {object|string} obj Object or string to escape + * @returns {object|string} Escaped object or string */ - async renderTemplate(template, msg, monitorJSON, heartbeatJSON, escapeMarkdown = false) { - const engine = new Liquid({ - root: "./no-such-directory-uptime-kuma", - relativeReference: false, - dynamicPartials: false, - }); - - const parsedTpl = engine.parse(template); - - // Defaults - let monitorName = "Monitor Name not available"; - let monitorHostnameOrURL = "testing.hostname"; - - if (monitorJSON !== null) { - monitorName = monitorJSON.name; - monitorHostnameOrURL = this.extractAddress(monitorJSON); + escapeObjectRecursive(obj) { + if (typeof obj === "string") { + return this.escapeMarkdownV2(obj); } + if (typeof obj === "object" && obj !== null) { + // Check if array + if (Array.isArray(obj)) { + return obj.map(item => this.escapeObjectRecursive(item)); + } - let serviceStatus = "⚠️ Test"; - if (heartbeatJSON !== null) { - serviceStatus = heartbeatJSON.status === DOWN ? "🔴 Down" : "✅ Up"; + const newObj = {}; + for (const key in obj) { + if (Object.prototype.hasOwnProperty.call(obj, key)) { + newObj[key] = this.escapeObjectRecursive(obj[key]); + } + } + return newObj; } - - // Escape values only when MarkdownV2 is enabled - if (escapeMarkdown) { - msg = this.escapeMarkdownV2(msg); - monitorName = this.escapeMarkdownV2(monitorName); - monitorHostnameOrURL = this.escapeMarkdownV2(monitorHostnameOrURL); - serviceStatus = this.escapeMarkdownV2(serviceStatus); - } - - const context = { - // v1 compatibility (remove in v3) - STATUS: serviceStatus, - NAME: monitorName, - HOSTNAME_OR_URL: monitorHostnameOrURL, - - // Official variables - status: serviceStatus, - name: monitorName, - hostnameOrURL: monitorHostnameOrURL, - monitorJSON, - heartbeatJSON, - msg, - }; - - return engine.render(parsedTpl, context); + return obj; } /** @@ -99,8 +65,29 @@ class Telegram extends NotificationProvider { } if (notification.telegramUseTemplate) { - const escapeMarkdown = notification.telegramTemplateParseMode === "MarkdownV2"; - params.text = await this.renderTemplate(notification.telegramTemplate, msg, monitorJSON, heartbeatJSON, escapeMarkdown); + let monitorJSONCopy = monitorJSON; + let heartbeatJSONCopy = heartbeatJSON; + + if (notification.telegramTemplateParseMode === "MarkdownV2") { + msg = this.escapeMarkdownV2(msg); + + if (monitorJSONCopy) { + monitorJSONCopy = this.escapeObjectRecursive(monitorJSONCopy); + } else { + // for testing monitorJSON is null, provide escaped defaults + monitorJSONCopy = { + name: this.escapeMarkdownV2("Monitor Name not available"), + hostname: this.escapeMarkdownV2("testing.hostname"), + url: this.escapeMarkdownV2("testing.hostname"), + }; + } + + if (heartbeatJSONCopy) { + heartbeatJSONCopy = this.escapeObjectRecursive(heartbeatJSONCopy); + } + } + + params.text = await this.renderTemplate(notification.telegramTemplate, msg, monitorJSONCopy, heartbeatJSONCopy); if (notification.telegramTemplateParseMode !== "plain") { params.parse_mode = notification.telegramTemplateParseMode;