From b64d0020cbb8662f2ba7855599ae56d73e52536f Mon Sep 17 00:00:00 2001 From: Sergio Conde <skgsergio@gmail.com> Date: Wed, 12 Mar 2025 18:42:56 +0100 Subject: [PATCH] feat: create templated components and unify notification templating code --- .../notification-provider.js | 47 +++++++++++ server/notification-providers/smtp.js | 47 +---------- server/notification-providers/telegram.js | 13 +-- server/notification-providers/webhook.js | 13 +-- src/components/TemplatedInput.vue | 75 +++++++++++++++++ src/components/TemplatedTextarea.vue | 80 +++++++++++++++++++ src/components/notifications/SMTP.vue | 20 ++--- src/components/notifications/Telegram.vue | 37 +++------ src/components/notifications/Webhook.vue | 20 ++--- src/lang/en.json | 11 ++- 10 files changed, 233 insertions(+), 130 deletions(-) create mode 100644 src/components/TemplatedInput.vue create mode 100644 src/components/TemplatedTextarea.vue diff --git a/server/notification-providers/notification-provider.js b/server/notification-providers/notification-provider.js index b9fb3d863..42e8e616d 100644 --- a/server/notification-providers/notification-provider.js +++ b/server/notification-providers/notification-provider.js @@ -1,3 +1,6 @@ +const { Liquid } = require("liquidjs"); +const { DOWN } = require("../../src/util"); + class NotificationProvider { /** @@ -49,6 +52,50 @@ class NotificationProvider { } } + /** + * Renders a message template with notification context + * @param {string} template the template + * @param {string} msg the message that will be included in the context + * @param {?object} monitorJSON Monitor details (For Up/Down/Cert-Expiry only) + * @param {?object} heartbeatJSON Heartbeat details (For Up/Down only) + * @returns {Promise<string>} rendered template + */ + async renderTemplate(template, msg, monitorJSON, heartbeatJSON) { + const engine = new Liquid(); + const parsedTpl = engine.parse(template); + + // Let's start with dummy values to simplify code + 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"; + } + + const context = { + // for v1 compatibility, to be removed in v3 + "STATUS": serviceStatus, + "NAME": monitorName, + "HOSTNAME_OR_URL": monitorHostnameOrURL, + + // variables which are officially supported + "status": serviceStatus, + "name": monitorName, + "hostnameOrURL": monitorHostnameOrURL, + monitorJSON, + heartbeatJSON, + msg, + }; + + return engine.render(parsedTpl, context); + } + /** * Throws an error * @param {any} error The error to throw diff --git a/server/notification-providers/smtp.js b/server/notification-providers/smtp.js index 9f3defa5e..980c7dfd3 100644 --- a/server/notification-providers/smtp.js +++ b/server/notification-providers/smtp.js @@ -1,7 +1,5 @@ const nodemailer = require("nodemailer"); const NotificationProvider = require("./notification-provider"); -const { DOWN } = require("../../src/util"); -const { Liquid } = require("liquidjs"); class SMTP extends NotificationProvider { name = "smtp"; @@ -53,15 +51,11 @@ class SMTP extends NotificationProvider { const customSubject = notification.customSubject?.trim() || ""; const customBody = notification.customBody?.trim() || ""; - const context = this.generateContext(msg, monitorJSON, heartbeatJSON); - const engine = new Liquid(); if (customSubject !== "") { - const tpl = engine.parse(customSubject); - subject = await engine.render(tpl, context); + subject = await this.renderTemplate(customSubject, msg, monitorJSON, heartbeatJSON); } if (customBody !== "") { - const tpl = engine.parse(customBody); - body = await engine.render(tpl, context); + body = await this.renderTemplate(customBody, msg, monitorJSON, heartbeatJSON); } } @@ -78,43 +72,6 @@ class SMTP extends NotificationProvider { return okMsg; } - - /** - * Generate context for LiquidJS - * @param {string} msg the message that will be included in the context - * @param {?object} monitorJSON Monitor details (For Up/Down/Cert-Expiry only) - * @param {?object} heartbeatJSON Heartbeat details (For Up/Down only) - * @returns {{STATUS: string, status: string, HOSTNAME_OR_URL: string, hostnameOrUrl: string, NAME: string, name: string, monitorJSON: ?object, heartbeatJSON: ?object, msg: string}} the context - */ - generateContext(msg, monitorJSON, heartbeatJSON) { - // Let's start with dummy values to simplify code - 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"; - } - return { - // for v1 compatibility, to be removed in v3 - "STATUS": serviceStatus, - "NAME": monitorName, - "HOSTNAME_OR_URL": monitorHostnameOrURL, - - // variables which are officially supported - "status": serviceStatus, - "name": monitorName, - "hostnameOrURL": monitorHostnameOrURL, - monitorJSON, - heartbeatJSON, - msg, - }; - } } module.exports = SMTP; diff --git a/server/notification-providers/telegram.js b/server/notification-providers/telegram.js index a8f15d107..62263db07 100644 --- a/server/notification-providers/telegram.js +++ b/server/notification-providers/telegram.js @@ -1,6 +1,5 @@ const NotificationProvider = require("./notification-provider"); const axios = require("axios"); -const { Liquid } = require("liquidjs"); class Telegram extends NotificationProvider { name = "telegram"; @@ -24,17 +23,7 @@ class Telegram extends NotificationProvider { } if (notification.telegramUseTemplate) { - const engine = new Liquid(); - const tpl = engine.parse(notification.telegramTemplate); - - params.text = await engine.render( - tpl, - { - msg, - heartbeatJSON, - monitorJSON - } - ); + params.text = await this.renderTemplate(notification.telegramTemplate, msg, monitorJSON, heartbeatJSON); if (notification.telegramTemplateParseMode !== "plain") { params.parse_mode = notification.telegramTemplateParseMode; diff --git a/server/notification-providers/webhook.js b/server/notification-providers/webhook.js index 986986d44..537f94bd5 100644 --- a/server/notification-providers/webhook.js +++ b/server/notification-providers/webhook.js @@ -1,7 +1,6 @@ const NotificationProvider = require("./notification-provider"); const axios = require("axios"); const FormData = require("form-data"); -const { Liquid } = require("liquidjs"); class Webhook extends NotificationProvider { name = "webhook"; @@ -28,17 +27,7 @@ class Webhook extends NotificationProvider { config.headers = formData.getHeaders(); data = formData; } else if (notification.webhookContentType === "custom") { - // Initialize LiquidJS and parse the custom Body Template - const engine = new Liquid(); - const tpl = engine.parse(notification.webhookCustomBody); - - // Insert templated values into Body - data = await engine.render(tpl, - { - msg, - heartbeatJSON, - monitorJSON - }); + data = await this.renderTemplate(notification.webhookCustomBody, msg, monitorJSON, heartbeatJSON); } if (notification.webhookAdditionalHeaders) { diff --git a/src/components/TemplatedInput.vue b/src/components/TemplatedInput.vue new file mode 100644 index 000000000..43c5382e0 --- /dev/null +++ b/src/components/TemplatedInput.vue @@ -0,0 +1,75 @@ +<template> + <div class="form-text mb-2"> + <i18n-t tag="div" keypath="liquidIntroduction"> + <a href="https://liquidjs.com/" target="_blank">{{ $t("documentation") }}</a> + </i18n-t> + + <code v-pre>{{ msg }}</code>: {{ $t("templateMsg") }}<br /> + <code v-pre>{{ name }}</code>: {{ $t("templateServiceName") }}<br /> + <code v-pre>{{ status }}</code>: {{ $t("templateStatus") }}<br /> + <code v-pre>{{ hostnameOrURL }}</code>: {{ $t("templateHostnameOrURL") }}<br /> + <code v-pre>{{ heartbeatJSON }}</code>: {{ $t("templateHeartbeatJSON") }} <b>({{ $t("templateLimitedToUpDownNotifications") }})</b><br /> + <code v-pre>{{ monitorJSON }}</code>: {{ $t("templateMonitorJSON") }} <b>({{ $t("templateLimitedToUpDownCertNotifications") }})</b><br /> + </div> + + <input + :id="id" + ref="templatedInput" + v-model="model" + type="text" + class="form-control" + :placeholder="placeholder" + :required="required" + autocomplete="false" + > +</template> + +<script> +export default { + props: { + /** + * The value of the templated input. + */ + modelValue: { + type: String, + default: "" + }, + /** + * id for the templated input. + */ + id: { + type: String, + required: true, + }, + /** + * Whether the templated input is required. + * @example true + */ + required: { + type: Boolean, + required: true, + }, + /** + * Placeholder text for the templated input. + */ + placeholder: { + type: String, + default: "" + }, + }, + emits: [ "update:modelValue" ], + computed: { + /** + * Send value update to parent on change. + */ + model: { + get() { + return this.modelValue; + }, + set(value) { + this.$emit("update:modelValue", value); + } + } + }, +}; +</script> diff --git a/src/components/TemplatedTextarea.vue b/src/components/TemplatedTextarea.vue new file mode 100644 index 000000000..ff0c0f9fb --- /dev/null +++ b/src/components/TemplatedTextarea.vue @@ -0,0 +1,80 @@ +<template> + <div class="form-text mb-2"> + <i18n-t tag="div" keypath="liquidIntroduction"> + <a href="https://liquidjs.com/" target="_blank">{{ $t("documentation") }}</a> + </i18n-t> + + <code v-pre>{{ msg }}</code>: {{ $t("templateMsg") }}<br /> + <code v-pre>{{ name }}</code>: {{ $t("templateServiceName") }}<br /> + <code v-pre>{{ status }}</code>: {{ $t("templateStatus") }}<br /> + <code v-pre>{{ hostnameOrURL }}</code>: {{ $t("templateHostnameOrURL") }}<br /> + <code v-pre>{{ heartbeatJSON }}</code>: {{ $t("templateHeartbeatJSON") }} <b>({{ $t("templateLimitedToUpDownNotifications") }})</b><br /> + <code v-pre>{{ monitorJSON }}</code>: {{ $t("templateMonitorJSON") }} <b>({{ $t("templateLimitedToUpDownCertNotifications") }})</b><br /> + </div> + + <textarea + :id="id" + ref="templatedTextarea" + v-model="model" + class="form-control" + :placeholder="placeholder" + :required="required" + autocomplete="false" + ></textarea> +</template> + +<script> +export default { + props: { + /** + * The value of the templated textarea. + */ + modelValue: { + type: String, + default: "" + }, + /** + * id for the templated textarea. + */ + id: { + type: String, + required: true, + }, + /** + * Whether the templated textarea is required. + * @example true + */ + required: { + type: Boolean, + required: true, + }, + /** + * Placeholder text for the templated textarea. + */ + placeholder: { + type: String, + default: "" + }, + }, + emits: [ "update:modelValue" ], + computed: { + /** + * Send value update to parent on change. + */ + model: { + get() { + return this.modelValue; + }, + set(value) { + this.$emit("update:modelValue", value); + } + } + }, +}; +</script> + +<style lang="scss" scoped> +textarea { + min-height: 150px; +} +</style> diff --git a/src/components/notifications/SMTP.vue b/src/components/notifications/SMTP.vue index 003f90556..4e0fb4b57 100644 --- a/src/components/notifications/SMTP.vue +++ b/src/components/notifications/SMTP.vue @@ -67,25 +67,15 @@ <input id="to-bcc" v-model="$parent.notification.smtpBCC" type="text" class="form-control" autocomplete="false" :required="!hasRecipient"> </div> - <p class="form-text"> - <i18n-t tag="div" keypath="smtpLiquidIntroduction" class="form-text mb-3"> - <a href="https://liquidjs.com/" target="_blank">{{ $t("documentation") }}</a> - </i18n-t> - <code v-pre>{{name}}</code>: {{ $t("emailTemplateServiceName") }}<br /> - <code v-pre>{{msg}}</code>: {{ $t("emailTemplateMsg") }}<br /> - <code v-pre>{{status}}</code>: {{ $t("emailTemplateStatus") }}<br /> - <code v-pre>{{heartbeatJSON}}</code>: {{ $t("emailTemplateHeartbeatJSON") }}<b>{{ $t("emailTemplateLimitedToUpDownNotification") }}</b><br /> - <code v-pre>{{monitorJSON}}</code>: {{ $t("emailTemplateMonitorJSON") }} <b>{{ $t("emailTemplateLimitedToUpDownNotification") }}</b><br /> - <code v-pre>{{hostnameOrURL}}</code>: {{ $t("emailTemplateHostnameOrURL") }}<br /> - </p> <div class="mb-3"> <label for="subject-email" class="form-label">{{ $t("emailCustomSubject") }}</label> - <input id="subject-email" v-model="$parent.notification.customSubject" type="text" class="form-control" autocomplete="false" placeholder=""> + <TemplatedInput id="subject-email" v-model="$parent.notification.customSubject" :required="false" placeholder=""></TemplatedInput> <div class="form-text">{{ $t("leave blank for default subject") }}</div> </div> + <div class="mb-3"> <label for="body-email" class="form-label">{{ $t("emailCustomBody") }}</label> - <textarea id="body-email" v-model="$parent.notification.customBody" type="text" class="form-control" autocomplete="false" placeholder=""></textarea> + <TemplatedTextarea id="body-email" v-model="$parent.notification.customBody" :required="false" placeholder=""></TemplatedTextarea> <div class="form-text">{{ $t("leave blank for default body") }}</div> </div> @@ -124,11 +114,15 @@ <script> import HiddenInput from "../HiddenInput.vue"; +import TemplatedInput from "../TemplatedInput.vue"; +import TemplatedTextarea from "../TemplatedTextarea.vue"; import ToggleSection from "../ToggleSection.vue"; export default { components: { HiddenInput, + TemplatedInput, + TemplatedTextarea, ToggleSection, }, computed: { diff --git a/src/components/notifications/Telegram.vue b/src/components/notifications/Telegram.vue index 04b49ed00..7f04e44c8 100644 --- a/src/components/notifications/Telegram.vue +++ b/src/components/notifications/Telegram.vue @@ -47,12 +47,7 @@ <template v-if="$parent.notification.telegramUseTemplate"> <div class="mb-3"> - <label class="form-label" for="message_parse_mode">{{ $t("Template Format") }}</label> - - <i18n-t tag="div" keypath="telegramTemplateFormatDescription" class="form-text mb-3"> - <a href="https://core.telegram.org/bots/api#formatting-options" target="_blank">{{ $t("documentation") }}</a> - </i18n-t> - + <label class="form-label" for="message_parse_mode">{{ $t("Message Format") }}</label> <select id="message_parse_mode" v-model="$parent.notification.telegramTemplateParseMode" @@ -63,28 +58,12 @@ <option value="HTML">HTML</option> <option value="MarkdownV2">MarkdownV2</option> </select> - </div> + <i18n-t tag="p" keypath="telegramTemplateFormatDescription" class="form-text"> + <a href="https://core.telegram.org/bots/api#formatting-options" target="_blank">{{ $t("documentation") }}</a> + </i18n-t> - <div class="mb-3"> - <label class="form-label" for="message_parse_mode">{{ $t("Template") }}</label> - - <div class="form-text mb-3"> - <i18n-t tag="div" keypath="liquidIntroduction"> - <a href="https://liquidjs.com/" target="_blank">{{ $t("documentation") }}</a> - </i18n-t> - - <code v-pre>{{ msg }}</code>: {{ $t("templateMsg") }}<br /> - <code v-pre>{{ heartbeatJSON }}</code>: {{ $t("templateHeartbeatJSON") }} <b>({{ $t("templateLimitedToUpDownNotifications") }})</b><br /> - <code v-pre>{{ monitorJSON }}</code>: {{ $t("templateMonitorJSON") }} <b>({{ $t("templateLimitedToUpDownCertNotifications") }})</b><br /> - </div> - - <textarea - id="message_template" - v-model="$parent.notification.telegramTemplate" - class="form-control mb-3" - :placeholder="telegramMessageTemplatePlaceholder" - required - ></textarea> + <label class="form-label" for="message_template">{{ $t('Message Template') }}</label> + <TemplatedTextarea id="message_template" v-model="$parent.notification.telegramTemplate" :required="true" :placeholder="telegramTemplatedTextareaPlaceholder"></TemplatedTextarea> </div> </template> @@ -113,14 +92,16 @@ <script> import HiddenInput from "../HiddenInput.vue"; +import TemplatedTextarea from "../TemplatedTextarea.vue"; import axios from "axios"; export default { components: { HiddenInput, + TemplatedTextarea, }, computed: { - telegramMessageTemplatePlaceholder() { + telegramTemplatedTextareaPlaceholder() { return this.$t("Example:", [ ` Uptime Kuma Alert{% if monitorJSON %} - {{ monitorJSON['name'] }}{% endif %} diff --git a/src/components/notifications/Webhook.vue b/src/components/notifications/Webhook.vue index 8c67a2745..7775a3fdd 100644 --- a/src/components/notifications/Webhook.vue +++ b/src/components/notifications/Webhook.vue @@ -32,20 +32,7 @@ </template> </i18n-t> <template v-else-if="$parent.notification.webhookContentType == 'custom'"> - <i18n-t tag="div" keypath="liquidIntroduction" class="form-text"> - <a href="https://liquidjs.com/" target="_blank">{{ $t("documentation") }}</a> - </i18n-t> - <code v-pre>{{msg}}</code>: {{ $t("templateMsg") }}<br /> - <code v-pre>{{heartbeatJSON}}</code>: {{ $t("templateHeartbeatJSON") }} <b>({{ $t("templateLimitedToUpDownNotifications") }})</b><br /> - <code v-pre>{{monitorJSON}}</code>: {{ $t("templateMonitorJSON") }} <b>({{ $t("templateLimitedToUpDownCertNotifications") }})</b><br /> - - <textarea - id="customBody" - v-model="$parent.notification.webhookCustomBody" - class="form-control" - :placeholder="customBodyPlaceholder" - required - ></textarea> + <TemplatedTextarea id="customBody" v-model="$parent.notification.webhookCustomBody" :required="true" :placeholder="customBodyPlaceholder"></TemplatedTextarea> </template> </div> @@ -67,7 +54,12 @@ </template> <script> +import TemplatedTextarea from "../TemplatedTextarea.vue"; + export default { + components: { + TemplatedTextarea, + }, data() { return { showAdditionalHeadersField: this.$parent.notification.webhookAdditionalHeaders != null, diff --git a/src/lang/en.json b/src/lang/en.json index 86e24d348..118dd7fc8 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -231,6 +231,9 @@ "templateMonitorJSON": "object describing the monitor", "templateLimitedToUpDownCertNotifications": "only available for UP/DOWN/Certificate expiry notifications", "templateLimitedToUpDownNotifications": "only available for UP/DOWN notifications", + "templateServiceName": "service name", + "templateHostnameOrURL": "hostname or URL", + "templateStatus": "status", "webhookAdditionalHeadersTitle": "Additional Headers", "webhookAdditionalHeadersDesc": "Sets additional headers sent with the webhook. Each header should be defined as a JSON key/value.", "webhookBodyPresetOption": "Preset - {0}", @@ -423,7 +426,7 @@ "telegramProtectContentDescription": "If enabled, the bot messages in Telegram will be protected from forwarding and saving.", "telegramUseTemplate": "Use custom message template", "telegramUseTemplateDescription": "If enabled, the message will be sent using a custom template.", - "telegramTemplateFormatDescription": "See Telegram {0} for more details on formatting.", + "telegramTemplateFormatDescription": "Telegram allows using different markup languages for messages, see Telegram {0} for specifc details.", "supportTelegramChatID": "Support Direct Chat / Group / Channel's Chat ID", "wayToGetTelegramChatID": "You can get your chat ID by sending a message to the bot and going to this URL to view the chat_id:", "YOUR BOT TOKEN HERE": "YOUR BOT TOKEN HERE", @@ -522,9 +525,6 @@ "leave blank for default subject": "leave blank for default subject", "emailCustomBody": "Custom Body", "leave blank for default body": "leave blank for default body", - "emailTemplateServiceName": "Service Name", - "emailTemplateHostnameOrURL": "Hostname or URL", - "emailTemplateStatus": "Status", "emailTemplateMonitorJSON": "object describing the monitor", "emailTemplateHeartbeatJSON": "object describing the heartbeat", "emailTemplateMsg": "message of the notification", @@ -1057,8 +1057,7 @@ "Separate multiple email addresses with commas": "Separate multiple email addresses with commas", "YZJ Webhook URL": "YZJ Webhook URL", "YZJ Robot Token": "YZJ Robot token", - "Separate multiple email addresses with commas": "Separate multiple email addresses with commas", "Plain Text": "Plain Text", - "Template": "Template", + "Message Template": "Message Template", "Template Format": "Template Format" }