diff --git a/server/notification-providers/slack.js b/server/notification-providers/slack.js index 3676d6df3..1f771cded 100644 --- a/server/notification-providers/slack.js +++ b/server/notification-providers/slack.js @@ -1,7 +1,8 @@ const NotificationProvider = require("./notification-provider"); const axios = require("axios"); -const { setSettings, setting } = require("../util-server"); -const { getMonitorRelativeURL, UP } = require("../../src/util"); +const { getMonitorRelativeURL, UP, DOWN } = require("../../src/util"); +const { Settings } = require("../settings"); +const { log } = require("../../src/util"); class Slack extends NotificationProvider { name = "slack"; @@ -14,13 +15,11 @@ class Slack extends NotificationProvider { * @returns {Promise} */ static async deprecateURL(url) { - let currentPrimaryBaseURL = await setting("primaryBaseURL"); + let currentPrimaryBaseURL = await Settings.get("primaryBaseURL"); if (!currentPrimaryBaseURL) { console.log("Move the url to be the primary base URL"); - await setSettings("general", { - primaryBaseURL: url, - }); + await Settings.set("primaryBaseURL", url, "general"); } else { console.log("Already there, no need to move the primary base URL"); } @@ -114,6 +113,167 @@ class Slack extends NotificationProvider { return blocks; } + /** + * Builds the message object to send to Slack + * @param {object} heartbeatJSON The heartbeat bean + * @param {object} monitorJSON The monitor bean + * @param {object} notification The notification config + * @param {string} title The message title + * @param {string} msg The textual message + * @returns {Promise} The message object + */ + static async buildMessage(heartbeatJSON, monitorJSON, notification, title, msg) { + + // check if the notification provider is being tested + if (heartbeatJSON == null) { + return { + "text": msg, + "channel": notification.slackchannel, + "username": notification.slackusername, + "icon_emoji": notification.slackiconemo, + "attachments": [], + }; + } + + const baseURL = await Settings.get("primaryBaseURL"); + + let data = { + "channel": notification.slackchannel, + "username": notification.slackusername, + "icon_emoji": notification.slackiconemo, + "attachments": [], + }; + + if (notification.slackrichmessage) { + data.attachments.push( + { + "color": (heartbeatJSON["status"] === UP) ? "#2eb886" : "#e01e5a", + "blocks": Slack.buildBlocks(baseURL, monitorJSON, heartbeatJSON, title, msg), + } + ); + } else { + data.text = `${title}\n${msg}`; + } + + return data; + + } + + static ENDPOINTS = { + postMessage: "https://slack.com/api/chat.postMessage", + getPermalink: "https://slack.com/api/chat.getPermalink", + update: "https://slack.com/api/chat.update", + }; + + // Keeps track of open alerts in order to update/close them + static openAlerts = {}; + + /** + * Delivers the message object to slack, through the chosen method + * @param {object} options The slack configuration + * @param {object} heartbeatJSON The heartbeat bean + * @param {object} message The message object to send to Slack + * @returns {Promise>} The response from axios + */ + static async deliverMessage(options, heartbeatJSON, message) { + + let response = null; + switch (options.mode) { + case "app": + response = Slack.deliverMessageViaAppApi(options, heartbeatJSON, message); + break; + + case "webhook": + default: + response = axios.post(options.slackwebhookURL, message); + + } + + return response; + } + + /** + * Track an open alert for a specific monitor + * @param {string} monitorId The monitor id + * @param {object} data The object representing the message + */ + static trackAlert(monitorId, data) { + Slack.openAlerts[monitorId] = Slack.openAlerts[monitorId] || []; + + Slack.openAlerts[monitorId].push(data); + + log.debug("notification.slack", `Monitor ${monitorId} now has ${Slack.openAlerts[monitorId].length} open alerts`); + + } + + /** + * Clears the open alerts for a specific monitor + * @param {string} monitorId The monitor id + */ + static clearAlerts(monitorId) { + Slack.openAlerts[monitorId] = []; + } + + /** + * Returns the alert(s) for the ongoing incident for a specific monitor + * @param {string} monitorId The monitor id + * @returns {Array} all open alerts + */ + static getAlerts(monitorId) { + return Slack.openAlerts[monitorId] || []; + } + + /** + * Delivers the message through the Slack App API + * @param {object} options Slack configuration + * @param {object} heartbeatJSON The heartbeat bean + * @param {object} message The message object to send + * @returns {Promise} The axios response + */ + static async deliverMessageViaAppApi(options, heartbeatJSON, message) { + + let response = null; + const token = options.botToken; + const monitorId = heartbeatJSON?.monitorId; + + const axiosConfig = { + headers: { + "Authorization": "Bearer " + token, + } + }; + + const existingAlerts = Slack.getAlerts(monitorId); + if (existingAlerts.length > 0 && heartbeatJSON?.status === UP) { + + log.info("slack", `Updating ${existingAlerts.length} message(s)`); + + //Update the messages in parallel + const responses = await Promise.all(existingAlerts.map(( { channel, ts } ) => { + message.channel = channel; + message.ts = ts; + return axios.post(Slack.ENDPOINTS.update, message, axiosConfig); + })); + + //get the last response + response = responses.pop(); + + } else { + response = await axios.post(Slack.ENDPOINTS.postMessage, message, axiosConfig); + } + + if (response.data.ok) { + + if (heartbeatJSON?.status === DOWN) { + Slack.trackAlert(monitorId, response.data); + } else if (heartbeatJSON?.status === UP) { + Slack.clearAlerts(monitorId); + } + + } + + return response; + } + /** * @inheritdoc */ @@ -125,43 +285,17 @@ class Slack extends NotificationProvider { } try { - if (heartbeatJSON == null) { - let data = { - "text": msg, - "channel": notification.slackchannel, - "username": notification.slackusername, - "icon_emoji": notification.slackiconemo, - }; - await axios.post(notification.slackwebhookURL, data); - return okMsg; - } - - const baseURL = await setting("primaryBaseURL"); - const title = "Uptime Kuma Alert"; - let data = { - "channel": notification.slackchannel, - "username": notification.slackusername, - "icon_emoji": notification.slackiconemo, - "attachments": [], - }; - if (notification.slackrichmessage) { - data.attachments.push( - { - "color": (heartbeatJSON["status"] === UP) ? "#2eb886" : "#e01e5a", - "blocks": Slack.buildBlocks(baseURL, monitorJSON, heartbeatJSON, title, msg), - } - ); - } else { - data.text = `${title}\n${msg}`; - } + const message = await Slack.buildMessage(heartbeatJSON, monitorJSON, notification, title, msg); + //not sure what this does, I think it can be safely removed if (notification.slackbutton) { await Slack.deprecateURL(notification.slackbutton); } - await axios.post(notification.slackwebhookURL, data); + await Slack.deliverMessage(notification, heartbeatJSON, message); + return okMsg; } catch (error) { this.throwGeneralAxiosError(error); diff --git a/src/components/notifications/Slack.vue b/src/components/notifications/Slack.vue index dc07bf373..f74b6479c 100644 --- a/src/components/notifications/Slack.vue +++ b/src/components/notifications/Slack.vue @@ -1,11 +1,25 @@