From 7756070c57d081cadc1a17661d54f4c8bb45632f Mon Sep 17 00:00:00 2001 From: taschenuhr Date: Wed, 28 Feb 2024 09:47:14 +0100 Subject: [PATCH 1/6] refactor MS-Teams notification to use adaptive cards --- server/notification-providers/teams.js | 119 +++++++++++++++++++------ 1 file changed, 92 insertions(+), 27 deletions(-) diff --git a/server/notification-providers/teams.js b/server/notification-providers/teams.js index 30976cf5b..3568acade 100644 --- a/server/notification-providers/teams.js +++ b/server/notification-providers/teams.js @@ -21,18 +21,32 @@ class Teams extends NotificationProvider { }; /** - * Select theme color to use based on status + * Select the style to use based on status * @param {const} status The status constant - * @returns {string} Selected color in hex RGB format + * @returns {string} Selected style for adaptive cards */ - _getThemeColor = (status) => { + _getStyle = (status) => { if (status === DOWN) { - return "ff0000"; + return "attention"; } if (status === UP) { - return "00e804"; + return "good"; + } + return "emphasis"; + }; + + /** + * Format an URL in the markdown format + * @param {string} url An absolute URL + * @param {string} linkName Optional name of the link + * @returns {string} The URL formatted as markdown link + */ + _formatAsMarkdownLink = (url, linkName) => { + if (linkName) { + return `[${linkName}](${url})`; + } else { + return `[${url}](${url})`; } - return "008cff"; }; /** @@ -57,40 +71,91 @@ class Teams extends NotificationProvider { const facts = []; + if (monitorMessage) { + facts.push({ + title: "Description", + value: monitorMessage, + }); + } + if (monitorName) { facts.push({ - name: "Monitor", + title: "Monitor", value: monitorName, }); } if (monitorUrl && monitorUrl !== "https://") { facts.push({ - name: "URL", - value: monitorUrl, + title: "URL", + value: this._formatAsMarkdownLink(monitorUrl), }); } return { - "@context": "https://schema.org/extensions", - "@type": "MessageCard", - themeColor: this._getThemeColor(status), - summary: notificationMessage, - sections: [ + "type": "message", + "attachments": [ { - activityImage: - "https://raw.githubusercontent.com/louislam/uptime-kuma/master/public/icon.png", - activityTitle: "**Uptime Kuma**", - }, - { - activityTitle: notificationMessage, - }, - { - activityTitle: "**Description**", - text: monitorMessage, - facts, - }, - ], + "contentType": "application/vnd.microsoft.card.adaptive", + "contentUrl": "", + "content": { + "type": "AdaptiveCard", + "body": [ + { + "type": "Container", + "verticalContentAlignment": "Center", + "items": [ + { + "type": "ColumnSet", + "style": this._getStyle(status), + "columns": [ + { + "type": "Column", + "width": "auto", + "verticalContentAlignment": "Center", + "items": [ + { + "type": "Image", + "width": "32px", + "style": "Person", + "url": "https://raw.githubusercontent.com/louislam/uptime-kuma/master/public/icon.png", + "altText": "Uptime Kuma Logo" + } + ] + }, + { + "type": "Column", + "width": "stretch", + "items": [ + { + "type": "TextBlock", + "size": "Large", + "weight": "Bolder", + "text": "**Uptime Kuma Alert**" + } + ] + } + ] + } + ] + }, + { + "type": "TextBlock", + "weight": "Bolder", + "text": notificationMessage, + "separator": true, + "wrap": true + }, + { + "type": "FactSet", + "facts": facts + } + ], + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "version": "1.5" + } + } + ] }; }; From 5b87da94b3834383d4c83c04cc0c20b4e8f77e7e Mon Sep 17 00:00:00 2001 From: taschenuhr Date: Wed, 28 Feb 2024 14:19:32 +0100 Subject: [PATCH 2/6] push notificationMessage to card header and add action button to dashboard --- server/notification-providers/teams.js | 88 ++++++++++++++++---------- 1 file changed, 53 insertions(+), 35 deletions(-) diff --git a/server/notification-providers/teams.js b/server/notification-providers/teams.js index 3568acade..16ce6435b 100644 --- a/server/notification-providers/teams.js +++ b/server/notification-providers/teams.js @@ -1,6 +1,7 @@ const NotificationProvider = require("./notification-provider"); const axios = require("axios"); -const { DOWN, UP } = require("../../src/util"); +const { setting } = require("../util-server"); +const { DOWN, UP, getMonitorRelativeURL } = require("../../src/util"); class Teams extends NotificationProvider { name = "teams"; @@ -13,9 +14,9 @@ class Teams extends NotificationProvider { */ _statusMessageFactory = (status, monitorName) => { if (status === DOWN) { - return `🔴 Application [${monitorName}] went down`; + return `🔴 [${monitorName}] went down`; } else if (status === UP) { - return `✅ Application [${monitorName}] is back online`; + return `✅ [${monitorName}] is back online`; } return "Notification"; }; @@ -35,27 +36,14 @@ class Teams extends NotificationProvider { return "emphasis"; }; - /** - * Format an URL in the markdown format - * @param {string} url An absolute URL - * @param {string} linkName Optional name of the link - * @returns {string} The URL formatted as markdown link - */ - _formatAsMarkdownLink = (url, linkName) => { - if (linkName) { - return `[${linkName}](${url})`; - } else { - return `[${url}](${url})`; - } - }; - /** * Generate payload for notification * @param {object} args Method arguments * @param {const} args.status The status of the monitor * @param {string} args.monitorMessage Message to send - * @param {string} args.monitorName Name of monitor affected - * @param {string} args.monitorUrl URL of monitor affected + * @param {string} args.monitorName Name of the monitor affected + * @param {string} args.monitorUrl URL of the monitor affected + * @param {string} args.dashboardUrl URL of the dashboard affected * @returns {object} Notification payload */ _notificationPayloadFactory = ({ @@ -63,6 +51,7 @@ class Teams extends NotificationProvider { monitorMessage, monitorName, monitorUrl, + dashboardUrl, }) => { const notificationMessage = this._statusMessageFactory( status, @@ -88,11 +77,14 @@ class Teams extends NotificationProvider { if (monitorUrl && monitorUrl !== "https://") { facts.push({ title: "URL", - value: this._formatAsMarkdownLink(monitorUrl), + // format URL as markdown syntax, to be clickable + value: `[${monitorUrl}](${monitorUrl})`, }); } - return { + const headerMessage = `**${notificationMessage}**`; + + const payload = { "type": "message", "attachments": [ { @@ -129,9 +121,17 @@ class Teams extends NotificationProvider { "items": [ { "type": "TextBlock", - "size": "Large", + "size": "Medium", "weight": "Bolder", - "text": "**Uptime Kuma Alert**" + "text": headerMessage, + }, + { + "type": "TextBlock", + "size": "Small", + "weight": "Default", + "text": "Uptime Kuma Alert", + "isSubtle": true, + "spacing": "None" } ] } @@ -139,15 +139,9 @@ class Teams extends NotificationProvider { } ] }, - { - "type": "TextBlock", - "weight": "Bolder", - "text": notificationMessage, - "separator": true, - "wrap": true - }, { "type": "FactSet", + "separator": true, "facts": facts } ], @@ -157,6 +151,23 @@ class Teams extends NotificationProvider { } ] }; + + if (dashboardUrl) { + payload.attachments.forEach(element => { + element.content.push({ + "type": "ActionSet", + "actions": [ + { + "type": "Action.OpenUrl", + "title": "Visit Uptime Kuma", + "url": dashboardUrl + } + ] + }); + }); + } + + return payload; }; /** @@ -195,26 +206,33 @@ class Teams extends NotificationProvider { return okMsg; } - let url; + let monitorUrl; switch (monitorJSON["type"]) { case "http": case "keywork": - url = monitorJSON["url"]; + monitorUrl = monitorJSON["url"]; break; case "docker": - url = monitorJSON["docker_host"]; + monitorUrl = monitorJSON["docker_host"]; break; default: - url = monitorJSON["hostname"]; + monitorUrl = monitorJSON["hostname"]; break; } + const baseURL = await setting("primaryBaseURL"); + let dashboardUrl; + if (baseURL) { + dashboardUrl = baseURL + getMonitorRelativeURL(monitorJSON.id); + } + const payload = this._notificationPayloadFactory({ monitorMessage: heartbeatJSON.msg, monitorName: monitorJSON.name, - monitorUrl: url, + monitorUrl: monitorUrl, status: heartbeatJSON.status, + dashboardUrl: dashboardUrl, }); await this._sendNotification(notification.webhookUrl, payload); From b8941403d16cbadadcef0f3f21e7a0af1600089e Mon Sep 17 00:00:00 2001 From: taschenuhr Date: Wed, 28 Feb 2024 16:30:48 +0100 Subject: [PATCH 3/6] code cleanup --- server/notification-providers/teams.js | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/server/notification-providers/teams.js b/server/notification-providers/teams.js index 16ce6435b..4435d2ddb 100644 --- a/server/notification-providers/teams.js +++ b/server/notification-providers/teams.js @@ -14,9 +14,9 @@ class Teams extends NotificationProvider { */ _statusMessageFactory = (status, monitorName) => { if (status === DOWN) { - return `🔴 [${monitorName}] went down`; + return `[${monitorName}] went down`; } else if (status === UP) { - return `✅ [${monitorName}] is back online`; + return `[${monitorName}] is back online`; } return "Notification"; }; @@ -82,8 +82,6 @@ class Teams extends NotificationProvider { }); } - const headerMessage = `**${notificationMessage}**`; - const payload = { "type": "message", "attachments": [ @@ -123,7 +121,7 @@ class Teams extends NotificationProvider { "type": "TextBlock", "size": "Medium", "weight": "Bolder", - "text": headerMessage, + "text": `**${notificationMessage}**`, }, { "type": "TextBlock", @@ -141,7 +139,7 @@ class Teams extends NotificationProvider { }, { "type": "FactSet", - "separator": true, + "separator": false, "facts": facts } ], From cc09147b4bbd831fd614ed5c770dc7496d7638f3 Mon Sep 17 00:00:00 2001 From: taschenuhr Date: Wed, 28 Feb 2024 16:31:47 +0100 Subject: [PATCH 4/6] fix adding optional ActionSet --- server/notification-providers/teams.js | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/server/notification-providers/teams.js b/server/notification-providers/teams.js index 4435d2ddb..e46bc9e32 100644 --- a/server/notification-providers/teams.js +++ b/server/notification-providers/teams.js @@ -151,17 +151,15 @@ class Teams extends NotificationProvider { }; if (dashboardUrl) { - payload.attachments.forEach(element => { - element.content.push({ - "type": "ActionSet", - "actions": [ - { - "type": "Action.OpenUrl", - "title": "Visit Uptime Kuma", - "url": dashboardUrl - } - ] - }); + payload.attachments[0].content.body.push({ + "type": "ActionSet", + "actions": [ + { + "type": "Action.OpenUrl", + "title": "Visit Uptime Kuma", + "url": dashboardUrl + } + ] }); } From 8bd69d78a84937849748371053b3676513b6ff8b Mon Sep 17 00:00:00 2001 From: taschenuhr Date: Wed, 28 Feb 2024 17:41:00 +0100 Subject: [PATCH 5/6] some more tweaking of the card content and notification summary --- server/notification-providers/teams.js | 61 +++++++++++++++----------- 1 file changed, 36 insertions(+), 25 deletions(-) diff --git a/server/notification-providers/teams.js b/server/notification-providers/teams.js index e46bc9e32..e8aab1a4a 100644 --- a/server/notification-providers/teams.js +++ b/server/notification-providers/teams.js @@ -10,13 +10,14 @@ class Teams extends NotificationProvider { * Generate the message to send * @param {const} status The status constant * @param {string} monitorName Name of monitor + * @param {boolean} withStatusSymbol If the status should be prepended as symbol * @returns {string} Status message */ - _statusMessageFactory = (status, monitorName) => { + _statusMessageFactory = (status, monitorName, withStatusSymbol) => { if (status === DOWN) { - return `[${monitorName}] went down`; + return (withStatusSymbol ? "🔴 " : "") + `[${monitorName}] went down`; } else if (status === UP) { - return `[${monitorName}] is back online`; + return (withStatusSymbol ? "✅ " : "") + `[${monitorName}] is back online`; } return "Notification"; }; @@ -39,31 +40,34 @@ class Teams extends NotificationProvider { /** * Generate payload for notification * @param {object} args Method arguments - * @param {const} args.status The status of the monitor - * @param {string} args.monitorMessage Message to send + * @param {object} args.heartbeatJSON Heartbeat details * @param {string} args.monitorName Name of the monitor affected * @param {string} args.monitorUrl URL of the monitor affected * @param {string} args.dashboardUrl URL of the dashboard affected * @returns {object} Notification payload */ _notificationPayloadFactory = ({ - status, - monitorMessage, + heartbeatJSON, monitorName, monitorUrl, dashboardUrl, }) => { - const notificationMessage = this._statusMessageFactory( - status, - monitorName - ); - + const status = heartbeatJSON.status; const facts = []; + const actions = []; - if (monitorMessage) { + if (dashboardUrl) { + actions.push({ + "type": "Action.OpenUrl", + "title": "Visit Uptime Kuma", + "url": dashboardUrl + }); + } + + if (heartbeatJSON?.msg) { facts.push({ title: "Description", - value: monitorMessage, + value: heartbeatJSON.msg, }); } @@ -80,10 +84,24 @@ class Teams extends NotificationProvider { // format URL as markdown syntax, to be clickable value: `[${monitorUrl}](${monitorUrl})`, }); + actions.push({ + "type": "Action.OpenUrl", + "title": "Visit Monitor URL", + "url": monitorUrl + }); + } + + if (heartbeatJSON?.localDateTime) { + facts.push({ + title: "Time", + value: heartbeatJSON.localDateTime + (heartbeatJSON.timezone ? ` (${heartbeatJSON.timezone})` : ""), + }); } const payload = { "type": "message", + // message with status prefix as notification text + "summary": this._statusMessageFactory(status, monitorName, true), "attachments": [ { "contentType": "application/vnd.microsoft.card.adaptive", @@ -121,7 +139,7 @@ class Teams extends NotificationProvider { "type": "TextBlock", "size": "Medium", "weight": "Bolder", - "text": `**${notificationMessage}**`, + "text": `**${this._statusMessageFactory(status, monitorName, false)}**`, }, { "type": "TextBlock", @@ -150,16 +168,10 @@ class Teams extends NotificationProvider { ] }; - if (dashboardUrl) { + if (actions) { payload.attachments[0].content.body.push({ "type": "ActionSet", - "actions": [ - { - "type": "Action.OpenUrl", - "title": "Visit Uptime Kuma", - "url": dashboardUrl - } - ] + "actions": actions, }); } @@ -224,10 +236,9 @@ class Teams extends NotificationProvider { } const payload = this._notificationPayloadFactory({ - monitorMessage: heartbeatJSON.msg, + heartbeatJSON: heartbeatJSON, monitorName: monitorJSON.name, monitorUrl: monitorUrl, - status: heartbeatJSON.status, dashboardUrl: dashboardUrl, }); From 4aaa0b92fb96ff6c47d50e42451c085dfb493f16 Mon Sep 17 00:00:00 2001 From: taschenuhr Date: Thu, 7 Mar 2024 15:11:19 +0100 Subject: [PATCH 6/6] fix test notification after method refactoring --- server/notification-providers/teams.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/server/notification-providers/teams.js b/server/notification-providers/teams.js index e8aab1a4a..bf2cd7647 100644 --- a/server/notification-providers/teams.js +++ b/server/notification-providers/teams.js @@ -52,7 +52,7 @@ class Teams extends NotificationProvider { monitorUrl, dashboardUrl, }) => { - const status = heartbeatJSON.status; + const status = heartbeatJSON?.status; const facts = []; const actions = []; @@ -196,7 +196,9 @@ class Teams extends NotificationProvider { */ _handleGeneralNotification = (webhookUrl, msg) => { const payload = this._notificationPayloadFactory({ - monitorMessage: msg + heartbeatJSON: { + msg: msg + } }); return this._sendNotification(webhookUrl, payload);