diff options
Diffstat (limited to 'server/notification-providers')
69 files changed, 4557 insertions, 0 deletions
diff --git a/server/notification-providers/46elks.js b/server/notification-providers/46elks.js new file mode 100644 index 0000000..4b15e9f --- /dev/null +++ b/server/notification-providers/46elks.js @@ -0,0 +1,35 @@ +const NotificationProvider = require("./notification-provider"); +const axios = require("axios"); + +class Elks extends NotificationProvider { + name = "Elks"; + + /** + * @inheritdoc + */ + async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { + const okMsg = "Sent Successfully."; + const url = "https://api.46elks.com/a1/sms"; + + try { + let data = new URLSearchParams(); + data.append("from", notification.elksFromNumber); + data.append("to", notification.elksToNumber ); + data.append("message", msg); + + const config = { + headers: { + "Authorization": "Basic " + Buffer.from(`${notification.elksUsername}:${notification.elksAuthToken}`).toString("base64") + } + }; + + await axios.post(url, data, config); + + return okMsg; + } catch (error) { + this.throwGeneralAxiosError(error); + } + } +} + +module.exports = Elks; diff --git a/server/notification-providers/alerta.js b/server/notification-providers/alerta.js new file mode 100644 index 0000000..f9a273b --- /dev/null +++ b/server/notification-providers/alerta.js @@ -0,0 +1,68 @@ +const NotificationProvider = require("./notification-provider"); +const { DOWN, UP } = require("../../src/util"); +const axios = require("axios"); + +class Alerta extends NotificationProvider { + name = "alerta"; + + /** + * @inheritdoc + */ + async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { + const okMsg = "Sent Successfully."; + + try { + let config = { + headers: { + "Content-Type": "application/json;charset=UTF-8", + "Authorization": "Key " + notification.alertaApiKey, + } + }; + let data = { + environment: notification.alertaEnvironment, + severity: "critical", + correlate: [], + service: [ "UptimeKuma" ], + value: "Timeout", + tags: [ "uptimekuma" ], + attributes: {}, + origin: "uptimekuma", + type: "exceptionAlert", + }; + + if (heartbeatJSON == null) { + let postData = Object.assign({ + event: "msg", + text: msg, + group: "uptimekuma-msg", + resource: "Message", + }, data); + + await axios.post(notification.alertaApiEndpoint, postData, config); + } else { + let datadup = Object.assign( { + correlate: [ "service_up", "service_down" ], + event: monitorJSON["type"], + group: "uptimekuma-" + monitorJSON["type"], + resource: monitorJSON["name"], + }, data ); + + if (heartbeatJSON["status"] === DOWN) { + datadup.severity = notification.alertaAlertState; // critical + datadup.text = "Service " + monitorJSON["type"] + " is down."; + await axios.post(notification.alertaApiEndpoint, datadup, config); + } else if (heartbeatJSON["status"] === UP) { + datadup.severity = notification.alertaRecoverState; // cleaned + datadup.text = "Service " + monitorJSON["type"] + " is up."; + await axios.post(notification.alertaApiEndpoint, datadup, config); + } + } + return okMsg; + } catch (error) { + this.throwGeneralAxiosError(error); + } + + } +} + +module.exports = Alerta; diff --git a/server/notification-providers/alertnow.js b/server/notification-providers/alertnow.js new file mode 100644 index 0000000..4257ca9 --- /dev/null +++ b/server/notification-providers/alertnow.js @@ -0,0 +1,53 @@ +const NotificationProvider = require("./notification-provider"); +const axios = require("axios"); +const { setting } = require("../util-server"); +const { getMonitorRelativeURL, UP, DOWN } = require("../../src/util"); + +class AlertNow extends NotificationProvider { + name = "AlertNow"; + + /** + * @inheritdoc + */ + async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { + const okMsg = "Sent Successfully."; + + try { + let textMsg = ""; + let status = "open"; + let eventType = "ERROR"; + let eventId = new Date().toISOString().slice(0, 10).replace(/-/g, ""); + + if (heartbeatJSON && heartbeatJSON.status === UP) { + textMsg = `[${heartbeatJSON.name}] ✅ Application is back online`; + status = "close"; + eventType = "INFO"; + eventId += `_${heartbeatJSON.name.replace(/\s/g, "")}`; + } else if (heartbeatJSON && heartbeatJSON.status === DOWN) { + textMsg = `[${heartbeatJSON.name}] 🔴 Application went down`; + } + + textMsg += ` - ${msg}`; + + const baseURL = await setting("primaryBaseURL"); + if (baseURL && monitorJSON) { + textMsg += ` >> ${baseURL + getMonitorRelativeURL(monitorJSON.id)}`; + } + + const data = { + "summary": textMsg, + "status": status, + "event_type": eventType, + "event_id": eventId, + }; + + await axios.post(notification.alertNowWebhookURL, data); + return okMsg; + } catch (error) { + this.throwGeneralAxiosError(error); + } + + } +} + +module.exports = AlertNow; diff --git a/server/notification-providers/aliyun-sms.js b/server/notification-providers/aliyun-sms.js new file mode 100644 index 0000000..ff38bd0 --- /dev/null +++ b/server/notification-providers/aliyun-sms.js @@ -0,0 +1,143 @@ +const NotificationProvider = require("./notification-provider"); +const { DOWN, UP } = require("../../src/util"); +const { default: axios } = require("axios"); +const Crypto = require("crypto"); +const qs = require("qs"); + +class AliyunSMS extends NotificationProvider { + name = "AliyunSMS"; + + /** + * @inheritdoc + */ + async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { + const okMsg = "Sent Successfully."; + + try { + if (heartbeatJSON != null) { + let msgBody = JSON.stringify({ + name: monitorJSON["name"], + time: heartbeatJSON["time"], + status: this.statusToString(heartbeatJSON["status"]), + msg: heartbeatJSON["msg"], + }); + if (await this.sendSms(notification, msgBody)) { + return okMsg; + } + } else { + let msgBody = JSON.stringify({ + name: "", + time: "", + status: "", + msg: msg, + }); + if (await this.sendSms(notification, msgBody)) { + return okMsg; + } + } + } catch (error) { + this.throwGeneralAxiosError(error); + } + } + + /** + * Send the SMS notification + * @param {BeanModel} notification Notification details + * @param {string} msgbody Message template + * @returns {Promise<boolean>} True if successful else false + */ + async sendSms(notification, msgbody) { + let params = { + PhoneNumbers: notification.phonenumber, + TemplateCode: notification.templateCode, + SignName: notification.signName, + TemplateParam: msgbody, + AccessKeyId: notification.accessKeyId, + Format: "JSON", + SignatureMethod: "HMAC-SHA1", + SignatureVersion: "1.0", + SignatureNonce: Math.random().toString(), + Timestamp: new Date().toISOString(), + Action: "SendSms", + Version: "2017-05-25", + }; + + params.Signature = this.sign(params, notification.secretAccessKey); + let config = { + method: "POST", + url: "http://dysmsapi.aliyuncs.com/", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + data: qs.stringify(params), + }; + + let result = await axios(config); + if (result.data.Message === "OK") { + return true; + } + + throw new Error(result.data.Message); + } + + /** + * Aliyun request sign + * @param {object} param Parameters object to sign + * @param {string} AccessKeySecret Secret key to sign parameters with + * @returns {string} Base64 encoded request + */ + sign(param, AccessKeySecret) { + let param2 = {}; + let data = []; + + let oa = Object.keys(param).sort(); + + for (let i = 0; i < oa.length; i++) { + let key = oa[i]; + param2[key] = param[key]; + } + + // Escape more characters than encodeURIComponent does. + // For generating Aliyun signature, all characters except A-Za-z0-9~-._ are encoded. + // See https://help.aliyun.com/document_detail/315526.html + // This encoding methods as known as RFC 3986 (https://tools.ietf.org/html/rfc3986) + let moreEscapesTable = function (m) { + return { + "!": "%21", + "*": "%2A", + "'": "%27", + "(": "%28", + ")": "%29" + }[m]; + }; + + for (let key in param2) { + let value = encodeURIComponent(param2[key]).replace(/[!*'()]/g, moreEscapesTable); + data.push(`${encodeURIComponent(key)}=${value}`); + } + + let StringToSign = `POST&${encodeURIComponent("/")}&${encodeURIComponent(data.join("&"))}`; + return Crypto + .createHmac("sha1", `${AccessKeySecret}&`) + .update(Buffer.from(StringToSign)) + .digest("base64"); + } + + /** + * Convert status constant to string + * @param {const} status The status constant + * @returns {string} Status + */ + statusToString(status) { + switch (status) { + case DOWN: + return "DOWN"; + case UP: + return "UP"; + default: + return status; + } + } +} + +module.exports = AliyunSMS; diff --git a/server/notification-providers/apprise.js b/server/notification-providers/apprise.js new file mode 100644 index 0000000..0f69821 --- /dev/null +++ b/server/notification-providers/apprise.js @@ -0,0 +1,37 @@ +const NotificationProvider = require("./notification-provider"); +const childProcessAsync = require("promisify-child-process"); + +class Apprise extends NotificationProvider { + name = "apprise"; + + /** + * @inheritdoc + */ + async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { + const okMsg = "Sent Successfully."; + + const args = [ "-vv", "-b", msg, notification.appriseURL ]; + if (notification.title) { + args.push("-t"); + args.push(notification.title); + } + const s = await childProcessAsync.spawn("apprise", args, { + encoding: "utf8", + }); + + const output = (s.stdout) ? s.stdout.toString() : "ERROR: maybe apprise not found"; + + if (output) { + + if (! output.includes("ERROR")) { + return okMsg; + } + + throw new Error(output); + } else { + return "No output from apprise"; + } + } +} + +module.exports = Apprise; diff --git a/server/notification-providers/bitrix24.js b/server/notification-providers/bitrix24.js new file mode 100644 index 0000000..ba12126 --- /dev/null +++ b/server/notification-providers/bitrix24.js @@ -0,0 +1,31 @@ +const NotificationProvider = require("./notification-provider"); +const axios = require("axios"); +const { UP } = require("../../src/util"); + +class Bitrix24 extends NotificationProvider { + name = "Bitrix24"; + + /** + * @inheritdoc + */ + async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { + const okMsg = "Sent Successfully."; + + try { + const params = { + user_id: notification.bitrix24UserID, + message: "[B]Uptime Kuma[/B]", + "ATTACH[COLOR]": (heartbeatJSON ?? {})["status"] === UP ? "#b73419" : "#67b518", + "ATTACH[BLOCKS][0][MESSAGE]": msg + }; + + await axios.get(`${notification.bitrix24WebhookURL}/im.notify.system.add.json`, { params }); + return okMsg; + + } catch (error) { + this.throwGeneralAxiosError(error); + } + } +} + +module.exports = Bitrix24; diff --git a/server/notification-providers/call-me-bot.js b/server/notification-providers/call-me-bot.js new file mode 100644 index 0000000..daa9ccd --- /dev/null +++ b/server/notification-providers/call-me-bot.js @@ -0,0 +1,23 @@ +const NotificationProvider = require("./notification-provider"); +const axios = require("axios"); + +class CallMeBot extends NotificationProvider { + name = "CallMeBot"; + + /** + * @inheritdoc + */ + async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { + const okMsg = "Sent Successfully."; + try { + const url = new URL(notification.callMeBotEndpoint); + url.searchParams.set("text", msg); + await axios.get(url.toString()); + return okMsg; + } catch (error) { + this.throwGeneralAxiosError(error); + } + } +} + +module.exports = CallMeBot; diff --git a/server/notification-providers/cellsynt.js b/server/notification-providers/cellsynt.js new file mode 100644 index 0000000..e842237 --- /dev/null +++ b/server/notification-providers/cellsynt.js @@ -0,0 +1,39 @@ +const NotificationProvider = require("./notification-provider"); +const axios = require("axios"); + +class Cellsynt extends NotificationProvider { + name = "Cellsynt"; + + /** + * @inheritdoc + */ + async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { + const okMsg = "Sent Successfully."; + const data = { + // docs at https://www.cellsynt.com/en/sms/api-integration + params: { + "username": notification.cellsyntLogin, + "password": notification.cellsyntPassword, + "destination": notification.cellsyntDestination, + "text": msg.replace(/[^\x00-\x7F]/g, ""), + "originatortype": notification.cellsyntOriginatortype, + "originator": notification.cellsyntOriginator, + "allowconcat": notification.cellsyntAllowLongSMS ? 6 : 1 + } + }; + try { + const resp = await axios.post("https://se-1.cellsynt.net/sms.php", null, data); + if (resp.data == null ) { + throw new Error("Could not connect to Cellsynt, please try again."); + } else if (resp.data.includes("Error:")) { + resp.data = resp.data.replaceAll("Error:", ""); + throw new Error(resp.data); + } + return okMsg; + } catch (error) { + this.throwGeneralAxiosError(error); + } + } +} + +module.exports = Cellsynt; diff --git a/server/notification-providers/clicksendsms.js b/server/notification-providers/clicksendsms.js new file mode 100644 index 0000000..c090b7f --- /dev/null +++ b/server/notification-providers/clicksendsms.js @@ -0,0 +1,45 @@ +const NotificationProvider = require("./notification-provider"); +const axios = require("axios"); + +class ClickSendSMS extends NotificationProvider { + name = "clicksendsms"; + + /** + * @inheritdoc + */ + async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { + const okMsg = "Sent Successfully."; + const url = "https://rest.clicksend.com/v3/sms/send"; + + try { + let config = { + headers: { + "Content-Type": "application/json", + "Authorization": "Basic " + Buffer.from(notification.clicksendsmsLogin + ":" + notification.clicksendsmsPassword).toString("base64"), + "Accept": "text/json", + } + }; + let data = { + messages: [ + { + "body": msg.replace(/[^\x00-\x7F]/g, ""), + "to": notification.clicksendsmsToNumber, + "source": "uptime-kuma", + "from": notification.clicksendsmsSenderName, + } + ] + }; + let resp = await axios.post(url, data, config); + if (resp.data.data.messages[0].status !== "SUCCESS") { + let error = "Something gone wrong. Api returned " + resp.data.data.messages[0].status + "."; + this.throwGeneralAxiosError(error); + } + + return okMsg; + } catch (error) { + this.throwGeneralAxiosError(error); + } + } +} + +module.exports = ClickSendSMS; diff --git a/server/notification-providers/dingding.js b/server/notification-providers/dingding.js new file mode 100644 index 0000000..c66f270 --- /dev/null +++ b/server/notification-providers/dingding.js @@ -0,0 +1,101 @@ +const NotificationProvider = require("./notification-provider"); +const { DOWN, UP } = require("../../src/util"); +const { default: axios } = require("axios"); +const Crypto = require("crypto"); + +class DingDing extends NotificationProvider { + name = "DingDing"; + + /** + * @inheritdoc + */ + async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { + const okMsg = "Sent Successfully."; + + try { + if (heartbeatJSON != null) { + let params = { + msgtype: "markdown", + markdown: { + title: `[${this.statusToString(heartbeatJSON["status"])}] ${monitorJSON["name"]}`, + text: `## [${this.statusToString(heartbeatJSON["status"])}] ${monitorJSON["name"]} \n> ${heartbeatJSON["msg"]}\n> Time (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`, + }, + "at": { + "isAtAll": notification.mentioning === "everyone" + } + }; + if (await this.sendToDingDing(notification, params)) { + return okMsg; + } + } else { + let params = { + msgtype: "text", + text: { + content: msg + } + }; + if (await this.sendToDingDing(notification, params)) { + return okMsg; + } + } + } catch (error) { + this.throwGeneralAxiosError(error); + } + } + + /** + * Send message to DingDing + * @param {BeanModel} notification Notification to send + * @param {object} params Parameters of message + * @returns {Promise<boolean>} True if successful else false + */ + async sendToDingDing(notification, params) { + let timestamp = Date.now(); + + let config = { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + url: `${notification.webHookUrl}×tamp=${timestamp}&sign=${encodeURIComponent(this.sign(timestamp, notification.secretKey))}`, + data: JSON.stringify(params), + }; + + let result = await axios(config); + if (result.data.errmsg === "ok") { + return true; + } + throw new Error(result.data.errmsg); + } + + /** + * DingDing sign + * @param {Date} timestamp Timestamp of message + * @param {string} secretKey Secret key to sign data with + * @returns {string} Base64 encoded signature + */ + sign(timestamp, secretKey) { + return Crypto + .createHmac("sha256", Buffer.from(secretKey, "utf8")) + .update(Buffer.from(`${timestamp}\n${secretKey}`, "utf8")) + .digest("base64"); + } + + /** + * Convert status constant to string + * @param {const} status The status constant + * @returns {string} Status + */ + statusToString(status) { + switch (status) { + case DOWN: + return "DOWN"; + case UP: + return "UP"; + default: + return status; + } + } +} + +module.exports = DingDing; diff --git a/server/notification-providers/discord.js b/server/notification-providers/discord.js new file mode 100644 index 0000000..6a52f8f --- /dev/null +++ b/server/notification-providers/discord.js @@ -0,0 +1,120 @@ +const NotificationProvider = require("./notification-provider"); +const axios = require("axios"); +const { DOWN, UP } = require("../../src/util"); + +class Discord extends NotificationProvider { + name = "discord"; + + /** + * @inheritdoc + */ + async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { + const okMsg = "Sent Successfully."; + + try { + const discordDisplayName = notification.discordUsername || "Uptime Kuma"; + const webhookUrl = new URL(notification.discordWebhookUrl); + if (notification.discordChannelType === "postToThread") { + webhookUrl.searchParams.append("thread_id", notification.threadId); + } + + // If heartbeatJSON is null, assume we're testing. + if (heartbeatJSON == null) { + let discordtestdata = { + username: discordDisplayName, + content: msg, + }; + + if (notification.discordChannelType === "createNewForumPost") { + discordtestdata.thread_name = notification.postName; + } + + await axios.post(webhookUrl.toString(), discordtestdata); + return okMsg; + } + + // If heartbeatJSON is not null, we go into the normal alerting loop. + if (heartbeatJSON["status"] === DOWN) { + let discorddowndata = { + username: discordDisplayName, + embeds: [{ + title: "❌ Your service " + monitorJSON["name"] + " went down. ❌", + color: 16711680, + timestamp: heartbeatJSON["time"], + fields: [ + { + name: "Service Name", + value: monitorJSON["name"], + }, + { + name: monitorJSON["type"] === "push" ? "Service Type" : "Service URL", + value: this.extractAddress(monitorJSON), + }, + { + name: `Time (${heartbeatJSON["timezone"]})`, + value: heartbeatJSON["localDateTime"], + }, + { + name: "Error", + value: heartbeatJSON["msg"] == null ? "N/A" : heartbeatJSON["msg"], + }, + ], + }], + }; + if (notification.discordChannelType === "createNewForumPost") { + discorddowndata.thread_name = notification.postName; + } + if (notification.discordPrefixMessage) { + discorddowndata.content = notification.discordPrefixMessage; + } + + await axios.post(webhookUrl.toString(), discorddowndata); + return okMsg; + + } else if (heartbeatJSON["status"] === UP) { + let discordupdata = { + username: discordDisplayName, + embeds: [{ + title: "✅ Your service " + monitorJSON["name"] + " is up! ✅", + color: 65280, + timestamp: heartbeatJSON["time"], + fields: [ + { + name: "Service Name", + value: monitorJSON["name"], + }, + { + name: monitorJSON["type"] === "push" ? "Service Type" : "Service URL", + value: this.extractAddress(monitorJSON), + }, + { + name: `Time (${heartbeatJSON["timezone"]})`, + value: heartbeatJSON["localDateTime"], + }, + { + name: "Ping", + value: heartbeatJSON["ping"] == null ? "N/A" : heartbeatJSON["ping"] + " ms", + }, + ], + }], + }; + + if (notification.discordChannelType === "createNewForumPost") { + discordupdata.thread_name = notification.postName; + } + + if (notification.discordPrefixMessage) { + discordupdata.content = notification.discordPrefixMessage; + } + + await axios.post(webhookUrl.toString(), discordupdata); + return okMsg; + } + } catch (error) { + this.throwGeneralAxiosError(error); + } + } + +} + +module.exports = Discord; diff --git a/server/notification-providers/feishu.js b/server/notification-providers/feishu.js new file mode 100644 index 0000000..cd5331d --- /dev/null +++ b/server/notification-providers/feishu.js @@ -0,0 +1,104 @@ +const NotificationProvider = require("./notification-provider"); +const axios = require("axios"); +const { DOWN, UP } = require("../../src/util"); + +class Feishu extends NotificationProvider { + name = "Feishu"; + + /** + * @inheritdoc + */ + async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { + const okMsg = "Sent Successfully."; + + try { + if (heartbeatJSON == null) { + let testdata = { + msg_type: "text", + content: { + text: msg, + }, + }; + await axios.post(notification.feishuWebHookUrl, testdata); + return okMsg; + } + + if (heartbeatJSON["status"] === DOWN) { + let downdata = { + msg_type: "interactive", + card: { + config: { + update_multi: false, + wide_screen_mode: true, + }, + header: { + title: { + tag: "plain_text", + content: "UptimeKuma Alert: [Down] " + monitorJSON["name"], + }, + template: "red", + }, + elements: [ + { + tag: "div", + text: { + tag: "lark_md", + content: getContent(heartbeatJSON), + }, + } + ] + } + }; + await axios.post(notification.feishuWebHookUrl, downdata); + return okMsg; + } + + if (heartbeatJSON["status"] === UP) { + let updata = { + msg_type: "interactive", + card: { + config: { + update_multi: false, + wide_screen_mode: true, + }, + header: { + title: { + tag: "plain_text", + content: "UptimeKuma Alert: [UP] " + monitorJSON["name"], + }, + template: "green", + }, + elements: [ + { + tag: "div", + text: { + tag: "lark_md", + content: getContent(heartbeatJSON), + }, + }, + ] + } + }; + await axios.post(notification.feishuWebHookUrl, updata); + return okMsg; + } + } catch (error) { + this.throwGeneralAxiosError(error); + } + } +} + +/** + * Get content + * @param {?object} heartbeatJSON Heartbeat details (For Up/Down only) + * @returns {string} Return Successful Message + */ +function getContent(heartbeatJSON) { + return [ + "**Message**: " + heartbeatJSON["msg"], + "**Ping**: " + (heartbeatJSON["ping"] == null ? "N/A" : heartbeatJSON["ping"] + " ms"), + `**Time (${heartbeatJSON["timezone"]})**: ${heartbeatJSON["localDateTime"]}` + ].join("\n"); +} + +module.exports = Feishu; diff --git a/server/notification-providers/flashduty.js b/server/notification-providers/flashduty.js new file mode 100644 index 0000000..c340ed0 --- /dev/null +++ b/server/notification-providers/flashduty.js @@ -0,0 +1,108 @@ +const NotificationProvider = require("./notification-provider"); +const axios = require("axios"); +const { UP, DOWN, getMonitorRelativeURL } = require("../../src/util"); +const { setting } = require("../util-server"); +const successMessage = "Sent Successfully."; + +class FlashDuty extends NotificationProvider { + name = "FlashDuty"; + + /** + * @inheritdoc + */ + async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { + try { + if (heartbeatJSON == null) { + const title = "Uptime Kuma Alert"; + const monitor = { + type: "ping", + url: msg, + name: "https://flashcat.cloud" + }; + return this.postNotification(notification, title, msg, monitor); + } + + if (heartbeatJSON.status === UP) { + const title = "Uptime Kuma Monitor ✅ Up"; + + return this.postNotification(notification, title, heartbeatJSON.msg, monitorJSON, "Ok"); + } + + if (heartbeatJSON.status === DOWN) { + const title = "Uptime Kuma Monitor 🔴 Down"; + return this.postNotification(notification, title, heartbeatJSON.msg, monitorJSON, notification.flashdutySeverity); + } + } catch (error) { + this.throwGeneralAxiosError(error); + } + } + + /** + * Generate a monitor url from the monitors infomation + * @param {object} monitorInfo Monitor details + * @returns {string|undefined} Monitor URL + */ + genMonitorUrl(monitorInfo) { + if (monitorInfo.type === "port" && monitorInfo.port) { + return monitorInfo.hostname + ":" + monitorInfo.port; + } + if (monitorInfo.hostname != null) { + return monitorInfo.hostname; + } + return monitorInfo.url; + } + + /** + * Send the message + * @param {BeanModel} notification Message title + * @param {string} title Message + * @param {string} body Message + * @param {object} monitorInfo Monitor details + * @param {string} eventStatus Monitor status (Info, Warning, Critical, Ok) + * @returns {string} Success message + */ + async postNotification(notification, title, body, monitorInfo, eventStatus) { + let labels = { + resource: this.genMonitorUrl(monitorInfo), + check: monitorInfo.name, + }; + if (monitorInfo.tags && monitorInfo.tags.length > 0) { + for (let tag of monitorInfo.tags) { + labels[tag.name] = tag.value; + } + } + const options = { + method: "POST", + url: "https://api.flashcat.cloud/event/push/alert/standard?integration_key=" + notification.flashdutyIntegrationKey, + headers: { "Content-Type": "application/json" }, + data: { + description: `[${title}] [${monitorInfo.name}] ${body}`, + title, + event_status: eventStatus || "Info", + alert_key: String(monitorInfo.id) || Math.random().toString(36).substring(7), + labels, + } + }; + + const baseURL = await setting("primaryBaseURL"); + if (baseURL && monitorInfo) { + options.client = "Uptime Kuma"; + options.client_url = baseURL + getMonitorRelativeURL(monitorInfo.id); + } + + let result = await axios.request(options); + if (result.status == null) { + throw new Error("FlashDuty notification failed with invalid response!"); + } + if (result.status < 200 || result.status >= 300) { + throw new Error("FlashDuty notification failed with status code " + result.status); + } + if (result.statusText != null) { + return "FlashDuty notification succeed: " + result.statusText; + } + + return successMessage; + } +} + +module.exports = FlashDuty; diff --git a/server/notification-providers/freemobile.js b/server/notification-providers/freemobile.js new file mode 100644 index 0000000..4de45ac --- /dev/null +++ b/server/notification-providers/freemobile.js @@ -0,0 +1,27 @@ +const NotificationProvider = require("./notification-provider"); +const axios = require("axios"); + +class FreeMobile extends NotificationProvider { + name = "FreeMobile"; + + /** + * @inheritdoc + */ + async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { + const okMsg = "Sent Successfully."; + + try { + await axios.post(`https://smsapi.free-mobile.fr/sendmsg?msg=${encodeURIComponent(msg.replace("🔴", "⛔️"))}`, { + "user": notification.freemobileUser, + "pass": notification.freemobilePass, + }); + + return okMsg; + + } catch (error) { + this.throwGeneralAxiosError(error); + } + } +} + +module.exports = FreeMobile; diff --git a/server/notification-providers/goalert.js b/server/notification-providers/goalert.js new file mode 100644 index 0000000..847c6a0 --- /dev/null +++ b/server/notification-providers/goalert.js @@ -0,0 +1,36 @@ +const NotificationProvider = require("./notification-provider"); +const axios = require("axios"); +const { UP } = require("../../src/util"); + +class GoAlert extends NotificationProvider { + name = "GoAlert"; + + /** + * @inheritdoc + */ + async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { + const okMsg = "Sent Successfully."; + + try { + let data = { + summary: msg, + }; + if (heartbeatJSON != null && heartbeatJSON["status"] === UP) { + data["action"] = "close"; + } + let headers = { + "Content-Type": "multipart/form-data", + }; + let config = { + headers: headers + }; + await axios.post(`${notification.goAlertBaseURL}/api/v2/generic/incoming?token=${notification.goAlertToken}`, data, config); + return okMsg; + } catch (error) { + let msg = (error.response.data) ? error.response.data : "Error without response"; + throw new Error(msg); + } + } +} + +module.exports = GoAlert; diff --git a/server/notification-providers/google-chat.js b/server/notification-providers/google-chat.js new file mode 100644 index 0000000..0b72fea --- /dev/null +++ b/server/notification-providers/google-chat.js @@ -0,0 +1,94 @@ +const NotificationProvider = require("./notification-provider"); +const axios = require("axios"); +const { setting } = require("../util-server"); +const { getMonitorRelativeURL, UP } = require("../../src/util"); + +class GoogleChat extends NotificationProvider { + name = "GoogleChat"; + + /** + * @inheritdoc + */ + async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { + const okMsg = "Sent Successfully."; + + try { + // Google Chat message formatting: https://developers.google.com/chat/api/guides/message-formats/basic + + let chatHeader = { + title: "Uptime Kuma Alert", + }; + + if (monitorJSON && heartbeatJSON) { + chatHeader["title"] = + heartbeatJSON["status"] === UP + ? `✅ ${monitorJSON["name"]} is back online` + : `🔴 ${monitorJSON["name"]} went down`; + } + + // always show msg + let sectionWidgets = [ + { + textParagraph: { + text: `<b>Message:</b>\n${msg}`, + }, + }, + ]; + + // add time if available + if (heartbeatJSON) { + sectionWidgets.push({ + textParagraph: { + text: `<b>Time (${heartbeatJSON["timezone"]}):</b>\n${heartbeatJSON["localDateTime"]}`, + }, + }); + } + + // add button for monitor link if available + const baseURL = await setting("primaryBaseURL"); + if (baseURL) { + const urlPath = monitorJSON ? getMonitorRelativeURL(monitorJSON.id) : "/"; + sectionWidgets.push({ + buttonList: { + buttons: [ + { + text: "Visit Uptime Kuma", + onClick: { + openLink: { + url: baseURL + urlPath, + }, + }, + }, + ], + }, + }); + } + + let chatSections = [ + { + widgets: sectionWidgets, + }, + ]; + + // construct json data + let data = { + cardsV2: [ + { + card: { + header: chatHeader, + sections: chatSections, + }, + }, + ], + }; + + await axios.post(notification.googleChatWebhookURL, data); + return okMsg; + } catch (error) { + this.throwGeneralAxiosError(error); + } + + } +} + +module.exports = GoogleChat; diff --git a/server/notification-providers/gorush.js b/server/notification-providers/gorush.js new file mode 100644 index 0000000..ba9d470 --- /dev/null +++ b/server/notification-providers/gorush.js @@ -0,0 +1,44 @@ +const NotificationProvider = require("./notification-provider"); +const axios = require("axios"); + +class Gorush extends NotificationProvider { + name = "gorush"; + + /** + * @inheritdoc + */ + async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { + const okMsg = "Sent Successfully."; + + let platformMapping = { + "ios": 1, + "android": 2, + "huawei": 3, + }; + + try { + let data = { + "notifications": [ + { + "tokens": [ notification.gorushDeviceToken ], + "platform": platformMapping[notification.gorushPlatform], + "message": msg, + // Optional + "title": notification.gorushTitle, + "priority": notification.gorushPriority, + "retry": parseInt(notification.gorushRetry) || 0, + "topic": notification.gorushTopic, + } + ] + }; + let config = {}; + + await axios.post(`${notification.gorushServerURL}/api/push`, data, config); + return okMsg; + } catch (error) { + this.throwGeneralAxiosError(error); + } + } +} + +module.exports = Gorush; diff --git a/server/notification-providers/gotify.js b/server/notification-providers/gotify.js new file mode 100644 index 0000000..a52ef51 --- /dev/null +++ b/server/notification-providers/gotify.js @@ -0,0 +1,31 @@ +const NotificationProvider = require("./notification-provider"); +const axios = require("axios"); + +class Gotify extends NotificationProvider { + name = "gotify"; + + /** + * @inheritdoc + */ + async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { + const okMsg = "Sent Successfully."; + + try { + if (notification.gotifyserverurl && notification.gotifyserverurl.endsWith("/")) { + notification.gotifyserverurl = notification.gotifyserverurl.slice(0, -1); + } + await axios.post(`${notification.gotifyserverurl}/message?token=${notification.gotifyapplicationToken}`, { + "message": msg, + "priority": notification.gotifyPriority || 8, + "title": "Uptime-Kuma", + }); + + return okMsg; + + } catch (error) { + this.throwGeneralAxiosError(error); + } + } +} + +module.exports = Gotify; diff --git a/server/notification-providers/grafana-oncall.js b/server/notification-providers/grafana-oncall.js new file mode 100644 index 0000000..e93c77c --- /dev/null +++ b/server/notification-providers/grafana-oncall.js @@ -0,0 +1,51 @@ +const NotificationProvider = require("./notification-provider"); +const axios = require("axios"); +const { DOWN, UP } = require("../../src/util"); + +class GrafanaOncall extends NotificationProvider { + name = "GrafanaOncall"; + + /** + * @inheritdoc + */ + async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { + const okMsg = "Sent Successfully."; + + if (!notification.GrafanaOncallURL) { + throw new Error("GrafanaOncallURL cannot be empty"); + } + + try { + if (heartbeatJSON === null) { + let grafanaupdata = { + title: "General notification", + message: msg, + state: "alerting", + }; + await axios.post(notification.GrafanaOncallURL, grafanaupdata); + return okMsg; + } else if (heartbeatJSON["status"] === DOWN) { + let grafanadowndata = { + title: monitorJSON["name"] + " is down", + message: heartbeatJSON["msg"], + state: "alerting", + }; + await axios.post(notification.GrafanaOncallURL, grafanadowndata); + return okMsg; + } else if (heartbeatJSON["status"] === UP) { + let grafanaupdata = { + title: monitorJSON["name"] + " is up", + message: heartbeatJSON["msg"], + state: "ok", + }; + await axios.post(notification.GrafanaOncallURL, grafanaupdata); + return okMsg; + } + } catch (error) { + this.throwGeneralAxiosError(error); + } + + } +} + +module.exports = GrafanaOncall; diff --git a/server/notification-providers/gtx-messaging.js b/server/notification-providers/gtx-messaging.js new file mode 100644 index 0000000..1ff97d1 --- /dev/null +++ b/server/notification-providers/gtx-messaging.js @@ -0,0 +1,33 @@ +const NotificationProvider = require("./notification-provider"); +const axios = require("axios"); + +class GtxMessaging extends NotificationProvider { + name = "gtxmessaging"; + + /** + * @inheritDoc + */ + async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { + const okMsg = "Sent Successfully."; + + // The UP/DOWN symbols will be replaced with `???` by gtx-messaging + const text = msg.replaceAll("🔴 ", "").replaceAll("✅ ", ""); + + try { + const data = new URLSearchParams(); + data.append("from", notification.gtxMessagingFrom.trim()); + data.append("to", notification.gtxMessagingTo.trim()); + data.append("text", text); + + const url = `https://rest.gtx-messaging.net/smsc/sendsms/${notification.gtxMessagingApiKey}/json`; + + await axios.post(url, data); + + return okMsg; + } catch (error) { + this.throwGeneralAxiosError(error); + } + } +} + +module.exports = GtxMessaging; diff --git a/server/notification-providers/heii-oncall.js b/server/notification-providers/heii-oncall.js new file mode 100644 index 0000000..20b53e6 --- /dev/null +++ b/server/notification-providers/heii-oncall.js @@ -0,0 +1,52 @@ +const { UP, DOWN, getMonitorRelativeURL } = require("../../src/util"); +const { setting } = require("../util-server"); + +const NotificationProvider = require("./notification-provider"); +const axios = require("axios"); +class HeiiOnCall extends NotificationProvider { + name = "HeiiOnCall"; + + /** + * @inheritdoc + */ + async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { + const okMsg = "Sent Successfully."; + const payload = heartbeatJSON || {}; + + const baseURL = await setting("primaryBaseURL"); + if (baseURL && monitorJSON) { + payload["url"] = baseURL + getMonitorRelativeURL(monitorJSON.id); + } + + const config = { + headers: { + Accept: "application/json", + "Content-Type": "application/json", + Authorization: "Bearer " + notification.heiiOnCallApiKey, + }, + }; + const heiiUrl = `https://heiioncall.com/triggers/${notification.heiiOnCallTriggerId}/`; + // docs https://heiioncall.com/docs#manual-triggers + try { + if (!heartbeatJSON) { + // Testing or general notification like certificate expiry + payload["msg"] = msg; + await axios.post(heiiUrl + "alert", payload, config); + return okMsg; + } + + if (heartbeatJSON.status === DOWN) { + await axios.post(heiiUrl + "alert", payload, config); + return okMsg; + } + if (heartbeatJSON.status === UP) { + await axios.post(heiiUrl + "resolve", payload, config); + return okMsg; + } + } catch (error) { + this.throwGeneralAxiosError(error); + } + } +} + +module.exports = HeiiOnCall; diff --git a/server/notification-providers/home-assistant.js b/server/notification-providers/home-assistant.js new file mode 100644 index 0000000..4536b2a --- /dev/null +++ b/server/notification-providers/home-assistant.js @@ -0,0 +1,45 @@ +const NotificationProvider = require("./notification-provider"); +const axios = require("axios"); + +const defaultNotificationService = "notify"; + +class HomeAssistant extends NotificationProvider { + name = "HomeAssistant"; + + /** + * @inheritdoc + */ + async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { + const okMsg = "Sent Successfully."; + + const notificationService = notification?.notificationService || defaultNotificationService; + + try { + await axios.post( + `${notification.homeAssistantUrl.trim().replace(/\/*$/, "")}/api/services/notify/${notificationService}`, + { + title: "Uptime Kuma", + message: msg, + ...(notificationService !== "persistent_notification" && { data: { + name: monitorJSON?.name, + status: heartbeatJSON?.status, + channel: "Uptime Kuma", + icon_url: "https://github.com/louislam/uptime-kuma/blob/master/public/icon.png?raw=true", + } }), + }, + { + headers: { + Authorization: `Bearer ${notification.longLivedAccessToken}`, + "Content-Type": "application/json", + }, + } + ); + + return okMsg; + } catch (error) { + this.throwGeneralAxiosError(error); + } + } +} + +module.exports = HomeAssistant; diff --git a/server/notification-providers/keep.js b/server/notification-providers/keep.js new file mode 100644 index 0000000..aa65a86 --- /dev/null +++ b/server/notification-providers/keep.js @@ -0,0 +1,42 @@ +const NotificationProvider = require("./notification-provider"); +const axios = require("axios"); + +class Keep extends NotificationProvider { + name = "Keep"; + + /** + * @inheritdoc + */ + async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { + const okMsg = "Sent Successfully."; + + try { + let data = { + heartbeat: heartbeatJSON, + monitor: monitorJSON, + msg, + }; + let config = { + headers: { + "x-api-key": notification.webhookAPIKey, + "content-type": "application/json", + }, + }; + + let url = notification.webhookURL; + + if (url.endsWith("/")) { + url = url.slice(0, -1); + } + + let webhookURL = url + "/alerts/event/uptimekuma"; + + await axios.post(webhookURL, data, config); + return okMsg; + } catch (error) { + this.throwGeneralAxiosError(error); + } + } +} + +module.exports = Keep; diff --git a/server/notification-providers/kook.js b/server/notification-providers/kook.js new file mode 100644 index 0000000..dab1951 --- /dev/null +++ b/server/notification-providers/kook.js @@ -0,0 +1,34 @@ +const NotificationProvider = require("./notification-provider"); +const axios = require("axios"); + +class Kook extends NotificationProvider { + name = "Kook"; + + /** + * @inheritdoc + */ + async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { + const okMsg = "Sent Successfully."; + const url = "https://www.kookapp.cn/api/v3/message/create"; + + let data = { + target_id: notification.kookGuildID, + content: msg, + }; + let config = { + headers: { + "Authorization": "Bot " + notification.kookBotToken, + "Content-Type": "application/json", + }, + }; + try { + await axios.post(url, data, config); + return okMsg; + + } catch (error) { + this.throwGeneralAxiosError(error); + } + } +} + +module.exports = Kook; diff --git a/server/notification-providers/line.js b/server/notification-providers/line.js new file mode 100644 index 0000000..57dc87e --- /dev/null +++ b/server/notification-providers/line.js @@ -0,0 +1,69 @@ +const NotificationProvider = require("./notification-provider"); +const axios = require("axios"); +const { DOWN, UP } = require("../../src/util"); + +class Line extends NotificationProvider { + name = "line"; + + /** + * @inheritdoc + */ + async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { + const okMsg = "Sent Successfully."; + const url = "https://api.line.me/v2/bot/message/push"; + + try { + let config = { + headers: { + "Content-Type": "application/json", + "Authorization": "Bearer " + notification.lineChannelAccessToken + } + }; + if (heartbeatJSON == null) { + let testMessage = { + "to": notification.lineUserID, + "messages": [ + { + "type": "text", + "text": "Test Successful!" + } + ] + }; + await axios.post(url, testMessage, config); + } else if (heartbeatJSON["status"] === DOWN) { + let downMessage = { + "to": notification.lineUserID, + "messages": [ + { + "type": "text", + "text": "UptimeKuma Alert: [🔴 Down]\n" + + "Name: " + monitorJSON["name"] + " \n" + + heartbeatJSON["msg"] + + `\nTime (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}` + } + ] + }; + await axios.post(url, downMessage, config); + } else if (heartbeatJSON["status"] === UP) { + let upMessage = { + "to": notification.lineUserID, + "messages": [ + { + "type": "text", + "text": "UptimeKuma Alert: [✅ Up]\n" + + "Name: " + monitorJSON["name"] + " \n" + + heartbeatJSON["msg"] + + `\nTime (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}` + } + ] + }; + await axios.post(url, upMessage, config); + } + return okMsg; + } catch (error) { + this.throwGeneralAxiosError(error); + } + } +} + +module.exports = Line; diff --git a/server/notification-providers/linenotify.js b/server/notification-providers/linenotify.js new file mode 100644 index 0000000..2622e3f --- /dev/null +++ b/server/notification-providers/linenotify.js @@ -0,0 +1,52 @@ +const NotificationProvider = require("./notification-provider"); +const axios = require("axios"); +const qs = require("qs"); +const { DOWN, UP } = require("../../src/util"); + +class LineNotify extends NotificationProvider { + name = "LineNotify"; + + /** + * @inheritdoc + */ + async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { + const okMsg = "Sent Successfully."; + const url = "https://notify-api.line.me/api/notify"; + + try { + let config = { + headers: { + "Content-Type": "application/x-www-form-urlencoded", + "Authorization": "Bearer " + notification.lineNotifyAccessToken + } + }; + if (heartbeatJSON == null) { + let testMessage = { + "message": msg, + }; + await axios.post(url, qs.stringify(testMessage), config); + } else if (heartbeatJSON["status"] === DOWN) { + let downMessage = { + "message": "\n[🔴 Down]\n" + + "Name: " + monitorJSON["name"] + " \n" + + heartbeatJSON["msg"] + "\n" + + `Time (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}` + }; + await axios.post(url, qs.stringify(downMessage), config); + } else if (heartbeatJSON["status"] === UP) { + let upMessage = { + "message": "\n[✅ Up]\n" + + "Name: " + monitorJSON["name"] + " \n" + + heartbeatJSON["msg"] + "\n" + + `Time (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}` + }; + await axios.post(url, qs.stringify(upMessage), config); + } + return okMsg; + } catch (error) { + this.throwGeneralAxiosError(error); + } + } +} + +module.exports = LineNotify; diff --git a/server/notification-providers/lunasea.js b/server/notification-providers/lunasea.js new file mode 100644 index 0000000..787a704 --- /dev/null +++ b/server/notification-providers/lunasea.js @@ -0,0 +1,67 @@ +const NotificationProvider = require("./notification-provider"); +const axios = require("axios"); +const { DOWN, UP } = require("../../src/util"); + +class LunaSea extends NotificationProvider { + name = "lunasea"; + + /** + * @inheritdoc + */ + async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { + const okMsg = "Sent Successfully."; + const url = "https://notify.lunasea.app/v1"; + + try { + const target = this.getTarget(notification); + if (heartbeatJSON == null) { + let testdata = { + "title": "Uptime Kuma Alert", + "body": msg, + }; + await axios.post(`${url}/custom/${target}`, testdata); + return okMsg; + } + + if (heartbeatJSON["status"] === DOWN) { + let downdata = { + "title": "UptimeKuma Alert: " + monitorJSON["name"], + "body": "[🔴 Down] " + + heartbeatJSON["msg"] + + `\nTime (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}` + }; + await axios.post(`${url}/custom/${target}`, downdata); + return okMsg; + } + + if (heartbeatJSON["status"] === UP) { + let updata = { + "title": "UptimeKuma Alert: " + monitorJSON["name"], + "body": "[✅ Up] " + + heartbeatJSON["msg"] + + `\nTime (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}` + }; + await axios.post(`${url}/custom/${target}`, updata); + return okMsg; + } + + } catch (error) { + this.throwGeneralAxiosError(error); + } + } + + /** + * Generates the lunasea target to send the notification to + * @param {BeanModel} notification Notification details + * @returns {string} The target to send the notification to + */ + getTarget(notification) { + if (notification.lunaseaTarget === "user") { + return "user/" + notification.lunaseaUserID; + } + return "device/" + notification.lunaseaDevice; + + } +} + +module.exports = LunaSea; diff --git a/server/notification-providers/matrix.js b/server/notification-providers/matrix.js new file mode 100644 index 0000000..805a494 --- /dev/null +++ b/server/notification-providers/matrix.js @@ -0,0 +1,48 @@ +const NotificationProvider = require("./notification-provider"); +const axios = require("axios"); +const Crypto = require("crypto"); +const { log } = require("../../src/util"); + +class Matrix extends NotificationProvider { + name = "matrix"; + + /** + * @inheritdoc + */ + async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { + const okMsg = "Sent Successfully."; + + const size = 20; + const randomString = encodeURIComponent( + Crypto + .randomBytes(size) + .toString("base64") + .slice(0, size) + ); + + log.debug("notification", "Random String: " + randomString); + + const roomId = encodeURIComponent(notification.internalRoomId); + + log.debug("notification", "Matrix Room ID: " + roomId); + + try { + let config = { + headers: { + "Authorization": `Bearer ${notification.accessToken}`, + } + }; + let data = { + "msgtype": "m.text", + "body": msg + }; + + await axios.put(`${notification.homeserverUrl}/_matrix/client/r0/rooms/${roomId}/send/m.room.message/${randomString}`, data, config); + return okMsg; + } catch (error) { + this.throwGeneralAxiosError(error); + } + } +} + +module.exports = Matrix; diff --git a/server/notification-providers/mattermost.js b/server/notification-providers/mattermost.js new file mode 100644 index 0000000..9946d02 --- /dev/null +++ b/server/notification-providers/mattermost.js @@ -0,0 +1,110 @@ +const NotificationProvider = require("./notification-provider"); +const axios = require("axios"); +const { DOWN, UP } = require("../../src/util"); + +class Mattermost extends NotificationProvider { + name = "mattermost"; + + /** + * @inheritdoc + */ + async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { + const okMsg = "Sent Successfully."; + + try { + const mattermostUserName = notification.mattermostusername || "Uptime Kuma"; + // If heartbeatJSON is null, assume non monitoring notification (Certificate warning) or testing. + if (heartbeatJSON == null) { + let mattermostTestData = { + username: mattermostUserName, + text: msg, + }; + await axios.post(notification.mattermostWebhookUrl, mattermostTestData); + return okMsg; + } + + let mattermostChannel; + + if (typeof notification.mattermostchannel === "string") { + mattermostChannel = notification.mattermostchannel.toLowerCase(); + } + + const mattermostIconEmoji = notification.mattermosticonemo; + let mattermostIconEmojiOnline = ""; + let mattermostIconEmojiOffline = ""; + + if (mattermostIconEmoji && typeof mattermostIconEmoji === "string") { + const emojiArray = mattermostIconEmoji.split(" "); + if (emojiArray.length >= 2) { + mattermostIconEmojiOnline = emojiArray[0]; + mattermostIconEmojiOffline = emojiArray[1]; + } + } + const mattermostIconUrl = notification.mattermosticonurl; + let iconEmoji = mattermostIconEmoji; + let statusField = { + short: false, + title: "Error", + value: heartbeatJSON.msg, + }; + let statusText = "unknown"; + let color = "#000000"; + if (heartbeatJSON.status === DOWN) { + iconEmoji = mattermostIconEmojiOffline || mattermostIconEmoji; + statusField = { + short: false, + title: "Error", + value: heartbeatJSON.msg, + }; + statusText = "down."; + color = "#FF0000"; + } else if (heartbeatJSON.status === UP) { + iconEmoji = mattermostIconEmojiOnline || mattermostIconEmoji; + statusField = { + short: false, + title: "Ping", + value: heartbeatJSON.ping + "ms", + }; + statusText = "up!"; + color = "#32CD32"; + } + + let mattermostdata = { + username: monitorJSON.name + " " + mattermostUserName, + channel: mattermostChannel, + icon_emoji: iconEmoji, + icon_url: mattermostIconUrl, + attachments: [ + { + fallback: + "Your " + + monitorJSON.pathName + + " service went " + + statusText, + color: color, + title: + monitorJSON.pathName + + " service went " + + statusText, + title_link: monitorJSON.url, + fields: [ + statusField, + { + short: true, + title: `Time (${heartbeatJSON["timezone"]})`, + value: heartbeatJSON.localDateTime, + }, + ], + }, + ], + }; + await axios.post(notification.mattermostWebhookUrl, mattermostdata); + return okMsg; + } catch (error) { + this.throwGeneralAxiosError(error); + } + + } +} + +module.exports = Mattermost; diff --git a/server/notification-providers/nostr.js b/server/notification-providers/nostr.js new file mode 100644 index 0000000..8784738 --- /dev/null +++ b/server/notification-providers/nostr.js @@ -0,0 +1,122 @@ +const NotificationProvider = require("./notification-provider"); +const { + relayInit, + getPublicKey, + getEventHash, + getSignature, + nip04, + nip19 +} = require("nostr-tools"); + +// polyfills for node versions +const semver = require("semver"); +const nodeVersion = process.version; +if (semver.lt(nodeVersion, "20.0.0")) { + // polyfills for node 18 + global.crypto = require("crypto"); + global.WebSocket = require("isomorphic-ws"); +} else { + // polyfills for node 20 + global.WebSocket = require("isomorphic-ws"); +} + +class Nostr extends NotificationProvider { + name = "nostr"; + + /** + * @inheritdoc + */ + async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { + // All DMs should have same timestamp + const createdAt = Math.floor(Date.now() / 1000); + + const senderPrivateKey = await this.getPrivateKey(notification.sender); + const senderPublicKey = getPublicKey(senderPrivateKey); + const recipientsPublicKeys = await this.getPublicKeys(notification.recipients); + + // Create NIP-04 encrypted direct message event for each recipient + const events = []; + for (const recipientPublicKey of recipientsPublicKeys) { + const ciphertext = await nip04.encrypt(senderPrivateKey, recipientPublicKey, msg); + let event = { + kind: 4, + pubkey: senderPublicKey, + created_at: createdAt, + tags: [[ "p", recipientPublicKey ]], + content: ciphertext, + }; + event.id = getEventHash(event); + event.sig = getSignature(event, senderPrivateKey); + events.push(event); + } + + // Publish events to each relay + const relays = notification.relays.split("\n"); + let successfulRelays = 0; + + // Connect to each relay + for (const relayUrl of relays) { + const relay = relayInit(relayUrl); + try { + await relay.connect(); + successfulRelays++; + + // Publish events + for (const event of events) { + relay.publish(event); + } + } catch (error) { + continue; + } finally { + relay.close(); + } + } + + // Report success or failure + if (successfulRelays === 0) { + throw Error("Failed to connect to any relays."); + } + return `${successfulRelays}/${relays.length} relays connected.`; + } + + /** + * Get the private key for the sender + * @param {string} sender Sender to retrieve key for + * @returns {nip19.DecodeResult} Private key + */ + async getPrivateKey(sender) { + try { + const senderDecodeResult = await nip19.decode(sender); + const { data } = senderDecodeResult; + return data; + } catch (error) { + throw new Error(`Failed to get private key: ${error.message}`); + } + } + + /** + * Get public keys for recipients + * @param {string} recipients Newline delimited list of recipients + * @returns {Promise<nip19.DecodeResult[]>} Public keys + */ + async getPublicKeys(recipients) { + const recipientsList = recipients.split("\n"); + const publicKeys = []; + for (const recipient of recipientsList) { + try { + const recipientDecodeResult = await nip19.decode(recipient); + const { type, data } = recipientDecodeResult; + if (type === "npub") { + publicKeys.push(data); + } else { + throw new Error("not an npub"); + } + } catch (error) { + throw new Error(`Error decoding recipient: ${error}`); + } + } + return publicKeys; + } +} + +module.exports = Nostr; diff --git a/server/notification-providers/notification-provider.js b/server/notification-providers/notification-provider.js new file mode 100644 index 0000000..b9fb3d8 --- /dev/null +++ b/server/notification-providers/notification-provider.js @@ -0,0 +1,73 @@ +class NotificationProvider { + + /** + * Notification Provider Name + * @type {string} + */ + name = undefined; + + /** + * Send a notification + * @param {BeanModel} notification Notification to send + * @param {string} msg General Message + * @param {?object} monitorJSON Monitor details (For Up/Down only) + * @param {?object} heartbeatJSON Heartbeat details (For Up/Down only) + * @returns {Promise<string>} Return Successful Message + * @throws Error with fail msg + */ + async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { + throw new Error("Have to override Notification.send(...)"); + } + + /** + * Extracts the address from a monitor JSON object based on its type. + * @param {?object} monitorJSON Monitor details (For Up/Down only) + * @returns {string} The extracted address based on the monitor type. + */ + extractAddress(monitorJSON) { + if (!monitorJSON) { + return ""; + } + switch (monitorJSON["type"]) { + case "push": + return "Heartbeat"; + case "ping": + return monitorJSON["hostname"]; + case "port": + case "dns": + case "gamedig": + case "steam": + if (monitorJSON["port"]) { + return monitorJSON["hostname"] + ":" + monitorJSON["port"]; + } + return monitorJSON["hostname"]; + default: + if (![ "https://", "http://", "" ].includes(monitorJSON["url"])) { + return monitorJSON["url"]; + } + return ""; + } + } + + /** + * Throws an error + * @param {any} error The error to throw + * @returns {void} + * @throws {any} The error specified + */ + throwGeneralAxiosError(error) { + let msg = "Error: " + error + " "; + + if (error.response && error.response.data) { + if (typeof error.response.data === "string") { + msg += error.response.data; + } else { + msg += JSON.stringify(error.response.data); + } + } + + throw new Error(msg); + } +} + +module.exports = NotificationProvider; diff --git a/server/notification-providers/ntfy.js b/server/notification-providers/ntfy.js new file mode 100644 index 0000000..ad1d39f --- /dev/null +++ b/server/notification-providers/ntfy.js @@ -0,0 +1,83 @@ +const NotificationProvider = require("./notification-provider"); +const axios = require("axios"); +const { DOWN, UP } = require("../../src/util"); + +class Ntfy extends NotificationProvider { + name = "ntfy"; + + /** + * @inheritdoc + */ + async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { + const okMsg = "Sent Successfully."; + + try { + let headers = {}; + if (notification.ntfyAuthenticationMethod === "usernamePassword") { + headers = { + "Authorization": "Basic " + Buffer.from(notification.ntfyusername + ":" + notification.ntfypassword).toString("base64"), + }; + } else if (notification.ntfyAuthenticationMethod === "accessToken") { + headers = { + "Authorization": "Bearer " + notification.ntfyaccesstoken, + }; + } + // If heartbeatJSON is null, assume non monitoring notification (Certificate warning) or testing. + if (heartbeatJSON == null) { + let ntfyTestData = { + "topic": notification.ntfytopic, + "title": (monitorJSON?.name || notification.ntfytopic) + " [Uptime-Kuma]", + "message": msg, + "priority": notification.ntfyPriority, + "tags": [ "test_tube" ], + }; + await axios.post(notification.ntfyserverurl, ntfyTestData, { headers: headers }); + return okMsg; + } + let tags = []; + let status = "unknown"; + let priority = notification.ntfyPriority || 4; + if ("status" in heartbeatJSON) { + if (heartbeatJSON.status === DOWN) { + tags = [ "red_circle" ]; + status = "Down"; + // if priority is not 5, increase priority for down alerts + priority = priority === 5 ? priority : priority + 1; + } else if (heartbeatJSON["status"] === UP) { + tags = [ "green_circle" ]; + status = "Up"; + } + } + let data = { + "topic": notification.ntfytopic, + "message": heartbeatJSON.msg, + "priority": priority, + "title": monitorJSON.name + " " + status + " [Uptime-Kuma]", + "tags": tags, + }; + + if (monitorJSON.url && monitorJSON.url !== "https://") { + data.actions = [ + { + "action": "view", + "label": "Open " + monitorJSON.name, + "url": monitorJSON.url, + }, + ]; + } + + if (notification.ntfyIcon) { + data.icon = notification.ntfyIcon; + } + + await axios.post(notification.ntfyserverurl, data, { headers: headers }); + + return okMsg; + + } catch (error) { + this.throwGeneralAxiosError(error); + } + } +} + +module.exports = Ntfy; diff --git a/server/notification-providers/octopush.js b/server/notification-providers/octopush.js new file mode 100644 index 0000000..7576e0a --- /dev/null +++ b/server/notification-providers/octopush.js @@ -0,0 +1,76 @@ +const NotificationProvider = require("./notification-provider"); +const axios = require("axios"); + +class Octopush extends NotificationProvider { + name = "octopush"; + + /** + * @inheritdoc + */ + async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { + const okMsg = "Sent Successfully."; + const urlV2 = "https://api.octopush.com/v1/public/sms-campaign/send"; + const urlV1 = "https://www.octopush-dm.com/api/sms/json"; + + try { + // Default - V2 + if (notification.octopushVersion === "2" || !notification.octopushVersion) { + let config = { + headers: { + "api-key": notification.octopushAPIKey, + "api-login": notification.octopushLogin, + "cache-control": "no-cache" + } + }; + let data = { + "recipients": [ + { + "phone_number": notification.octopushPhoneNumber + } + ], + //octopush not supporting non ascii char + "text": msg.replace(/[^\x00-\x7F]/g, ""), + "type": notification.octopushSMSType, + "purpose": "alert", + "sender": notification.octopushSenderName + }; + await axios.post(urlV2, data, config); + } else if (notification.octopushVersion === "1") { + let data = { + "user_login": notification.octopushDMLogin, + "api_key": notification.octopushDMAPIKey, + "sms_recipients": notification.octopushDMPhoneNumber, + "sms_sender": notification.octopushDMSenderName, + "sms_type": (notification.octopushDMSMSType === "sms_premium") ? "FR" : "XXX", + "transactional": "1", + //octopush not supporting non ascii char + "sms_text": msg.replace(/[^\x00-\x7F]/g, ""), + }; + + let config = { + headers: { + "cache-control": "no-cache" + }, + params: data + }; + + // V1 API returns 200 even on error so we must check + // response data + let response = await axios.post(urlV1, {}, config); + if ("error_code" in response.data) { + if (response.data.error_code !== "000") { + this.throwGeneralAxiosError(`Octopush error ${JSON.stringify(response.data)}`); + } + } + } else { + throw new Error("Unknown Octopush version!"); + } + + return okMsg; + } catch (error) { + this.throwGeneralAxiosError(error); + } + } +} + +module.exports = Octopush; diff --git a/server/notification-providers/onebot.js b/server/notification-providers/onebot.js new file mode 100644 index 0000000..b04794d --- /dev/null +++ b/server/notification-providers/onebot.js @@ -0,0 +1,48 @@ +const NotificationProvider = require("./notification-provider"); +const axios = require("axios"); + +class OneBot extends NotificationProvider { + name = "OneBot"; + + /** + * @inheritdoc + */ + async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { + const okMsg = "Sent Successfully."; + + try { + let url = notification.httpAddr; + if (!url.startsWith("http")) { + url = "http://" + url; + } + if (!url.endsWith("/")) { + url += "/"; + } + url += "send_msg"; + let config = { + headers: { + "Content-Type": "application/json", + "Authorization": "Bearer " + notification.accessToken, + } + }; + let pushText = "UptimeKuma Alert: " + msg; + let data = { + "auto_escape": true, + "message": pushText, + }; + if (notification.msgType === "group") { + data["message_type"] = "group"; + data["group_id"] = notification.recieverId; + } else { + data["message_type"] = "private"; + data["user_id"] = notification.recieverId; + } + await axios.post(url, data, config); + return okMsg; + } catch (error) { + this.throwGeneralAxiosError(error); + } + } +} + +module.exports = OneBot; diff --git a/server/notification-providers/onesender.js b/server/notification-providers/onesender.js new file mode 100644 index 0000000..4a33931 --- /dev/null +++ b/server/notification-providers/onesender.js @@ -0,0 +1,47 @@ +const NotificationProvider = require("./notification-provider"); +const axios = require("axios"); + +class Onesender extends NotificationProvider { + name = "Onesender"; + + /** + * @inheritdoc + */ + async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { + const okMsg = "Sent Successfully."; + + try { + let data = { + heartbeat: heartbeatJSON, + monitor: monitorJSON, + msg, + to: notification.onesenderReceiver, + type: "text", + recipient_type: "individual", + text: { + body: msg + } + }; + if (notification.onesenderTypeReceiver === "private") { + data.to = notification.onesenderReceiver + "@s.whatsapp.net"; + } else { + data.recipient_type = "group"; + data.to = notification.onesenderReceiver + "@g.us"; + } + let config = { + headers: { + "Authorization": "Bearer " + notification.onesenderToken, + } + }; + await axios.post(notification.onesenderURL, data, config); + return okMsg; + + } catch (error) { + this.throwGeneralAxiosError(error); + } + + } + +} + +module.exports = Onesender; diff --git a/server/notification-providers/opsgenie.js b/server/notification-providers/opsgenie.js new file mode 100644 index 0000000..59a7970 --- /dev/null +++ b/server/notification-providers/opsgenie.js @@ -0,0 +1,96 @@ +const NotificationProvider = require("./notification-provider"); +const axios = require("axios"); +const { UP, DOWN } = require("../../src/util"); + +const opsgenieAlertsUrlEU = "https://api.eu.opsgenie.com/v2/alerts"; +const opsgenieAlertsUrlUS = "https://api.opsgenie.com/v2/alerts"; +const okMsg = "Sent Successfully."; + +class Opsgenie extends NotificationProvider { + name = "Opsgenie"; + + /** + * @inheritdoc + */ + async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { + let opsgenieAlertsUrl; + let priority = (!notification.opsgeniePriority) ? 3 : notification.opsgeniePriority; + const textMsg = "Uptime Kuma Alert"; + + try { + switch (notification.opsgenieRegion) { + case "us": + opsgenieAlertsUrl = opsgenieAlertsUrlUS; + break; + case "eu": + opsgenieAlertsUrl = opsgenieAlertsUrlEU; + break; + default: + opsgenieAlertsUrl = opsgenieAlertsUrlUS; + } + + if (heartbeatJSON == null) { + let notificationTestAlias = "uptime-kuma-notification-test"; + let data = { + "message": msg, + "alias": notificationTestAlias, + "source": "Uptime Kuma", + "priority": "P5" + }; + + return this.post(notification, opsgenieAlertsUrl, data); + } + + if (heartbeatJSON.status === DOWN) { + let data = { + "message": monitorJSON ? textMsg + `: ${monitorJSON.name}` : textMsg, + "alias": monitorJSON.name, + "description": msg, + "source": "Uptime Kuma", + "priority": `P${priority}` + }; + + return this.post(notification, opsgenieAlertsUrl, data); + } + + if (heartbeatJSON.status === UP) { + let opsgenieAlertsCloseUrl = `${opsgenieAlertsUrl}/${encodeURIComponent(monitorJSON.name)}/close?identifierType=alias`; + let data = { + "source": "Uptime Kuma", + }; + + return this.post(notification, opsgenieAlertsCloseUrl, data); + } + } catch (error) { + this.throwGeneralAxiosError(error); + } + } + + /** + * Make POST request to Opsgenie + * @param {BeanModel} notification Notification to send + * @param {string} url Request url + * @param {object} data Request body + * @returns {Promise<string>} Success message + */ + async post(notification, url, data) { + let config = { + headers: { + "Content-Type": "application/json", + "Authorization": `GenieKey ${notification.opsgenieApiKey}`, + } + }; + + let res = await axios.post(url, data, config); + if (res.status == null) { + return "Opsgenie notification failed with invalid response!"; + } + if (res.status < 200 || res.status >= 300) { + return `Opsgenie notification failed with status code ${res.status}`; + } + + return okMsg; + } +} + +module.exports = Opsgenie; diff --git a/server/notification-providers/pagerduty.js b/server/notification-providers/pagerduty.js new file mode 100644 index 0000000..c60d782 --- /dev/null +++ b/server/notification-providers/pagerduty.js @@ -0,0 +1,114 @@ +const NotificationProvider = require("./notification-provider"); +const axios = require("axios"); +const { UP, DOWN, getMonitorRelativeURL } = require("../../src/util"); +const { setting } = require("../util-server"); +let successMessage = "Sent Successfully."; + +class PagerDuty extends NotificationProvider { + name = "PagerDuty"; + + /** + * @inheritdoc + */ + async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { + try { + if (heartbeatJSON == null) { + const title = "Uptime Kuma Alert"; + const monitor = { + type: "ping", + url: "Uptime Kuma Test Button", + }; + return this.postNotification(notification, title, msg, monitor); + } + + if (heartbeatJSON.status === UP) { + const title = "Uptime Kuma Monitor ✅ Up"; + const eventAction = notification.pagerdutyAutoResolve || null; + + return this.postNotification(notification, title, heartbeatJSON.msg, monitorJSON, eventAction); + } + + if (heartbeatJSON.status === DOWN) { + const title = "Uptime Kuma Monitor 🔴 Down"; + return this.postNotification(notification, title, heartbeatJSON.msg, monitorJSON, "trigger"); + } + } catch (error) { + this.throwGeneralAxiosError(error); + } + } + + /** + * Check if result is successful, result code should be in range 2xx + * @param {object} result Axios response object + * @returns {void} + * @throws {Error} The status code is not in range 2xx + */ + checkResult(result) { + if (result.status == null) { + throw new Error("PagerDuty notification failed with invalid response!"); + } + if (result.status < 200 || result.status >= 300) { + throw new Error("PagerDuty notification failed with status code " + result.status); + } + } + + /** + * Send the message + * @param {BeanModel} notification Message title + * @param {string} title Message title + * @param {string} body Message + * @param {object} monitorInfo Monitor details (For Up/Down only) + * @param {?string} eventAction Action event for PagerDuty (trigger, acknowledge, resolve) + * @returns {Promise<string>} Success message + */ + async postNotification(notification, title, body, monitorInfo, eventAction = "trigger") { + + if (eventAction == null) { + return "No action required"; + } + + let monitorUrl; + if (monitorInfo.type === "port") { + monitorUrl = monitorInfo.hostname; + if (monitorInfo.port) { + monitorUrl += ":" + monitorInfo.port; + } + } else if (monitorInfo.hostname != null) { + monitorUrl = monitorInfo.hostname; + } else { + monitorUrl = monitorInfo.url; + } + + const options = { + method: "POST", + url: notification.pagerdutyIntegrationUrl, + headers: { "Content-Type": "application/json" }, + data: { + payload: { + summary: `[${title}] [${monitorInfo.name}] ${body}`, + severity: notification.pagerdutyPriority || "warning", + source: monitorUrl, + }, + routing_key: notification.pagerdutyIntegrationKey, + event_action: eventAction, + dedup_key: "Uptime Kuma/" + monitorInfo.id, + } + }; + + const baseURL = await setting("primaryBaseURL"); + if (baseURL && monitorInfo) { + options.client = "Uptime Kuma"; + options.client_url = baseURL + getMonitorRelativeURL(monitorInfo.id); + } + + let result = await axios.request(options); + this.checkResult(result); + if (result.statusText != null) { + return "PagerDuty notification succeed: " + result.statusText; + } + + return successMessage; + } +} + +module.exports = PagerDuty; diff --git a/server/notification-providers/pagertree.js b/server/notification-providers/pagertree.js new file mode 100644 index 0000000..c7a5338 --- /dev/null +++ b/server/notification-providers/pagertree.js @@ -0,0 +1,93 @@ +const NotificationProvider = require("./notification-provider"); +const axios = require("axios"); +const { UP, DOWN, getMonitorRelativeURL } = require("../../src/util"); +const { setting } = require("../util-server"); +let successMessage = "Sent Successfully."; + +class PagerTree extends NotificationProvider { + name = "PagerTree"; + + /** + * @inheritdoc + */ + async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { + try { + if (heartbeatJSON == null) { + // general messages + return this.postNotification(notification, msg, monitorJSON, heartbeatJSON); + } + + if (heartbeatJSON.status === UP && notification.pagertreeAutoResolve === "resolve") { + return this.postNotification(notification, null, monitorJSON, heartbeatJSON, notification.pagertreeAutoResolve); + } + + if (heartbeatJSON.status === DOWN) { + const title = `Uptime Kuma Monitor "${monitorJSON.name}" is DOWN`; + return this.postNotification(notification, title, monitorJSON, heartbeatJSON); + } + } catch (error) { + this.throwGeneralAxiosError(error); + } + } + + /** + * Check if result is successful, result code should be in range 2xx + * @param {object} result Axios response object + * @returns {void} + * @throws {Error} The status code is not in range 2xx + */ + checkResult(result) { + if (result.status == null) { + throw new Error("PagerTree notification failed with invalid response!"); + } + if (result.status < 200 || result.status >= 300) { + throw new Error("PagerTree notification failed with status code " + result.status); + } + } + + /** + * Send the message + * @param {BeanModel} notification Message title + * @param {string} title Message title + * @param {object} monitorJSON Monitor details (For Up/Down only) + * @param {object} heartbeatJSON Heartbeat details (For Up/Down only) + * @param {?string} eventAction Action event for PagerTree (create, resolve) + * @returns {Promise<string>} Success state + */ + async postNotification(notification, title, monitorJSON, heartbeatJSON, eventAction = "create") { + + if (eventAction == null) { + return "No action required"; + } + + const options = { + method: "POST", + url: notification.pagertreeIntegrationUrl, + headers: { "Content-Type": "application/json" }, + data: { + event_type: eventAction, + id: heartbeatJSON?.monitorID || "uptime-kuma", + title: title, + urgency: notification.pagertreeUrgency, + heartbeat: heartbeatJSON, + monitor: monitorJSON + } + }; + + const baseURL = await setting("primaryBaseURL"); + if (baseURL && monitorJSON) { + options.client = "Uptime Kuma"; + options.client_url = baseURL + getMonitorRelativeURL(monitorJSON.id); + } + + let result = await axios.request(options); + this.checkResult(result); + if (result.statusText != null) { + return "PagerTree notification succeed: " + result.statusText; + } + + return successMessage; + } +} + +module.exports = PagerTree; diff --git a/server/notification-providers/promosms.js b/server/notification-providers/promosms.js new file mode 100644 index 0000000..05334e9 --- /dev/null +++ b/server/notification-providers/promosms.js @@ -0,0 +1,53 @@ +const NotificationProvider = require("./notification-provider"); +const axios = require("axios"); + +class PromoSMS extends NotificationProvider { + name = "promosms"; + + /** + * @inheritdoc + */ + async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { + const okMsg = "Sent Successfully."; + const url = "https://promosms.com/api/rest/v3_2/sms"; + + if (notification.promosmsAllowLongSMS === undefined) { + notification.promosmsAllowLongSMS = false; + } + + //TODO: Add option for enabling special characters. It will decrese message max length from 160 to 70 chars. + //Lets remove non ascii char + let cleanMsg = msg.replace(/[^\x00-\x7F]/g, ""); + + try { + let config = { + headers: { + "Content-Type": "application/json", + "Authorization": "Basic " + Buffer.from(notification.promosmsLogin + ":" + notification.promosmsPassword).toString("base64"), + "Accept": "text/json", + } + }; + let data = { + "recipients": [ notification.promosmsPhoneNumber ], + //Trim message to maximum length of 1 SMS or 4 if we allowed long messages + "text": notification.promosmsAllowLongSMS ? cleanMsg.substring(0, 639) : cleanMsg.substring(0, 159), + "long-sms": notification.promosmsAllowLongSMS, + "type": Number(notification.promosmsSMSType), + "sender": notification.promosmsSenderName + }; + + let resp = await axios.post(url, data, config); + + if (resp.data.response.status !== 0) { + let error = "Something gone wrong. Api returned " + resp.data.response.status + "."; + this.throwGeneralAxiosError(error); + } + + return okMsg; + } catch (error) { + this.throwGeneralAxiosError(error); + } + } +} + +module.exports = PromoSMS; diff --git a/server/notification-providers/pushbullet.js b/server/notification-providers/pushbullet.js new file mode 100644 index 0000000..0b73031 --- /dev/null +++ b/server/notification-providers/pushbullet.js @@ -0,0 +1,56 @@ +const NotificationProvider = require("./notification-provider"); +const axios = require("axios"); + +const { DOWN, UP } = require("../../src/util"); + +class Pushbullet extends NotificationProvider { + name = "pushbullet"; + + /** + * @inheritdoc + */ + async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { + const okMsg = "Sent Successfully."; + const url = "https://api.pushbullet.com/v2/pushes"; + + try { + let config = { + headers: { + "Access-Token": notification.pushbulletAccessToken, + "Content-Type": "application/json" + } + }; + if (heartbeatJSON == null) { + let data = { + "type": "note", + "title": "Uptime Kuma Alert", + "body": msg, + }; + await axios.post(url, data, config); + } else if (heartbeatJSON["status"] === DOWN) { + let downData = { + "type": "note", + "title": "UptimeKuma Alert: " + monitorJSON["name"], + "body": "[🔴 Down] " + + heartbeatJSON["msg"] + + `\nTime (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`, + }; + await axios.post(url, downData, config); + } else if (heartbeatJSON["status"] === UP) { + let upData = { + "type": "note", + "title": "UptimeKuma Alert: " + monitorJSON["name"], + "body": "[✅ Up] " + + heartbeatJSON["msg"] + + `\nTime (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`, + }; + await axios.post(url, upData, config); + } + return okMsg; + } catch (error) { + this.throwGeneralAxiosError(error); + } + } +} + +module.exports = Pushbullet; diff --git a/server/notification-providers/pushdeer.js b/server/notification-providers/pushdeer.js new file mode 100644 index 0000000..276c2f4 --- /dev/null +++ b/server/notification-providers/pushdeer.js @@ -0,0 +1,55 @@ +const NotificationProvider = require("./notification-provider"); +const axios = require("axios"); +const { DOWN, UP } = require("../../src/util"); + +class PushDeer extends NotificationProvider { + name = "PushDeer"; + + /** + * @inheritdoc + */ + async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { + const okMsg = "Sent Successfully."; + const serverUrl = notification.pushdeerServer || "https://api2.pushdeer.com"; + const url = `${serverUrl.trim().replace(/\/*$/, "")}/message/push`; + + let valid = msg != null && monitorJSON != null && heartbeatJSON != null; + + let title; + if (valid && heartbeatJSON.status === UP) { + title = "## Uptime Kuma: " + monitorJSON.name + " up"; + } else if (valid && heartbeatJSON.status === DOWN) { + title = "## Uptime Kuma: " + monitorJSON.name + " down"; + } else { + title = "## Uptime Kuma Message"; + } + + let data = { + "pushkey": notification.pushdeerKey, + "text": title, + "desp": msg.replace(/\n/g, "\n\n"), + "type": "markdown", + }; + + try { + let res = await axios.post(url, data); + + if ("error" in res.data) { + let error = res.data.error; + this.throwGeneralAxiosError(error); + } + if (res.data.content.result.length === 0) { + let error = "Invalid PushDeer key"; + this.throwGeneralAxiosError(error); + } else if (JSON.parse(res.data.content.result[0]).success !== "ok") { + let error = "Unknown error"; + this.throwGeneralAxiosError(error); + } + return okMsg; + } catch (error) { + this.throwGeneralAxiosError(error); + } + } +} + +module.exports = PushDeer; diff --git a/server/notification-providers/pushover.js b/server/notification-providers/pushover.js new file mode 100644 index 0000000..8422b64 --- /dev/null +++ b/server/notification-providers/pushover.js @@ -0,0 +1,58 @@ +const { getMonitorRelativeURL } = require("../../src/util"); +const { setting } = require("../util-server"); + +const NotificationProvider = require("./notification-provider"); +const axios = require("axios"); + +class Pushover extends NotificationProvider { + name = "pushover"; + + /** + * @inheritdoc + */ + async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { + const okMsg = "Sent Successfully."; + const url = "https://api.pushover.net/1/messages.json"; + + let data = { + "message": msg, + "user": notification.pushoveruserkey, + "token": notification.pushoverapptoken, + "sound": notification.pushoversounds, + "priority": notification.pushoverpriority, + "title": notification.pushovertitle, + "retry": "30", + "expire": "3600", + "html": 1, + }; + + const baseURL = await setting("primaryBaseURL"); + if (baseURL && monitorJSON) { + data["url"] = baseURL + getMonitorRelativeURL(monitorJSON.id); + data["url_title"] = "Link to Monitor"; + } + + if (notification.pushoverdevice) { + data.device = notification.pushoverdevice; + } + if (notification.pushoverttl) { + data.ttl = notification.pushoverttl; + } + + try { + if (heartbeatJSON == null) { + await axios.post(url, data); + return okMsg; + } else { + data.message += `\n<b>Time (${heartbeatJSON["timezone"]})</b>:${heartbeatJSON["localDateTime"]}`; + await axios.post(url, data); + return okMsg; + } + } catch (error) { + this.throwGeneralAxiosError(error); + } + + } +} + +module.exports = Pushover; diff --git a/server/notification-providers/pushy.js b/server/notification-providers/pushy.js new file mode 100644 index 0000000..cb70022 --- /dev/null +++ b/server/notification-providers/pushy.js @@ -0,0 +1,32 @@ +const NotificationProvider = require("./notification-provider"); +const axios = require("axios"); + +class Pushy extends NotificationProvider { + name = "pushy"; + + /** + * @inheritdoc + */ + async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { + const okMsg = "Sent Successfully."; + + try { + await axios.post(`https://api.pushy.me/push?api_key=${notification.pushyAPIKey}`, { + "to": notification.pushyToken, + "data": { + "message": "Uptime-Kuma" + }, + "notification": { + "body": msg, + "badge": 1, + "sound": "ping.aiff" + } + }); + return okMsg; + } catch (error) { + this.throwGeneralAxiosError(error); + } + } +} + +module.exports = Pushy; diff --git a/server/notification-providers/rocket-chat.js b/server/notification-providers/rocket-chat.js new file mode 100644 index 0000000..690e33a --- /dev/null +++ b/server/notification-providers/rocket-chat.js @@ -0,0 +1,67 @@ +const NotificationProvider = require("./notification-provider"); +const axios = require("axios"); +const Slack = require("./slack"); +const { setting } = require("../util-server"); +const { getMonitorRelativeURL, DOWN } = require("../../src/util"); + +class RocketChat extends NotificationProvider { + name = "rocket.chat"; + + /** + * @inheritdoc + */ + async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { + const okMsg = "Sent Successfully."; + + try { + if (heartbeatJSON == null) { + let data = { + "text": msg, + "channel": notification.rocketchannel, + "username": notification.rocketusername, + "icon_emoji": notification.rocketiconemo, + }; + await axios.post(notification.rocketwebhookURL, data); + return okMsg; + } + + let data = { + "text": "Uptime Kuma Alert", + "channel": notification.rocketchannel, + "username": notification.rocketusername, + "icon_emoji": notification.rocketiconemo, + "attachments": [ + { + "title": `Uptime Kuma Alert *Time (${heartbeatJSON["timezone"]})*\n${heartbeatJSON["localDateTime"]}`, + "text": "*Message*\n" + msg, + } + ] + }; + + // Color + if (heartbeatJSON.status === DOWN) { + data.attachments[0].color = "#ff0000"; + } else { + data.attachments[0].color = "#32cd32"; + } + + if (notification.rocketbutton) { + await Slack.deprecateURL(notification.rocketbutton); + } + + const baseURL = await setting("primaryBaseURL"); + + if (baseURL) { + data.attachments[0].title_link = baseURL + getMonitorRelativeURL(monitorJSON.id); + } + + await axios.post(notification.rocketwebhookURL, data); + return okMsg; + } catch (error) { + this.throwGeneralAxiosError(error); + } + + } +} + +module.exports = RocketChat; diff --git a/server/notification-providers/send-grid.js b/server/notification-providers/send-grid.js new file mode 100644 index 0000000..3489f63 --- /dev/null +++ b/server/notification-providers/send-grid.js @@ -0,0 +1,65 @@ +const NotificationProvider = require("./notification-provider"); +const axios = require("axios"); + +class SendGrid extends NotificationProvider { + name = "SendGrid"; + + /** + * @inheritdoc + */ + async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { + const okMsg = "Sent Successfully."; + + try { + let config = { + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${notification.sendgridApiKey}`, + }, + }; + + let personalizations = { + to: [{ email: notification.sendgridToEmail }], + }; + + // Add CC recipients if provided + if (notification.sendgridCcEmail) { + personalizations.cc = notification.sendgridCcEmail + .split(",") + .map((email) => ({ email: email.trim() })); + } + + // Add BCC recipients if provided + if (notification.sendgridBccEmail) { + personalizations.bcc = notification.sendgridBccEmail + .split(",") + .map((email) => ({ email: email.trim() })); + } + + let data = { + personalizations: [ personalizations ], + from: { email: notification.sendgridFromEmail.trim() }, + subject: + notification.sendgridSubject || + "Notification from Your Uptime Kuma", + content: [ + { + type: "text/plain", + value: msg, + }, + ], + }; + + await axios.post( + "https://api.sendgrid.com/v3/mail/send", + data, + config + ); + return okMsg; + } catch (error) { + this.throwGeneralAxiosError(error); + } + } +} + +module.exports = SendGrid; diff --git a/server/notification-providers/serverchan.js b/server/notification-providers/serverchan.js new file mode 100644 index 0000000..aee22f8 --- /dev/null +++ b/server/notification-providers/serverchan.js @@ -0,0 +1,51 @@ +const NotificationProvider = require("./notification-provider"); +const axios = require("axios"); +const { DOWN, UP } = require("../../src/util"); + +class ServerChan extends NotificationProvider { + name = "ServerChan"; + + /** + * @inheritdoc + */ + async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { + const okMsg = "Sent Successfully."; + + // serverchan3 requires sending via ft07.com + const matchResult = String(notification.serverChanSendKey).match(/^sctp(\d+)t/i); + const url = matchResult && matchResult[1] + ? `https://${matchResult[1]}.push.ft07.com/send/${notification.serverChanSendKey}.send` + : `https://sctapi.ftqq.com/${notification.serverChanSendKey}.send`; + + try { + await axios.post(url, { + "title": this.checkStatus(heartbeatJSON, monitorJSON), + "desp": msg, + }); + + return okMsg; + + } catch (error) { + this.throwGeneralAxiosError(error); + } + } + + /** + * Get the formatted title for message + * @param {?object} heartbeatJSON Heartbeat details (For Up/Down only) + * @param {?object} monitorJSON Monitor details (For Up/Down only) + * @returns {string} Formatted title + */ + checkStatus(heartbeatJSON, monitorJSON) { + let title = "UptimeKuma Message"; + if (heartbeatJSON != null && heartbeatJSON["status"] === UP) { + title = "UptimeKuma Monitor Up " + monitorJSON["name"]; + } + if (heartbeatJSON != null && heartbeatJSON["status"] === DOWN) { + title = "UptimeKuma Monitor Down " + monitorJSON["name"]; + } + return title; + } +} + +module.exports = ServerChan; diff --git a/server/notification-providers/serwersms.js b/server/notification-providers/serwersms.js new file mode 100644 index 0000000..f7c8644 --- /dev/null +++ b/server/notification-providers/serwersms.js @@ -0,0 +1,47 @@ +const NotificationProvider = require("./notification-provider"); +const axios = require("axios"); + +class SerwerSMS extends NotificationProvider { + name = "serwersms"; + + /** + * @inheritdoc + */ + async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { + const okMsg = "Sent Successfully."; + const url = "https://api2.serwersms.pl/messages/send_sms"; + + try { + let config = { + headers: { + "Content-Type": "application/json", + } + }; + let data = { + "username": notification.serwersmsUsername, + "password": notification.serwersmsPassword, + "phone": notification.serwersmsPhoneNumber, + "text": msg.replace(/[^\x00-\x7F]/g, ""), + "sender": notification.serwersmsSenderName, + }; + + let resp = await axios.post(url, data, config); + + if (!resp.data.success) { + if (resp.data.error) { + let error = `SerwerSMS.pl API returned error code ${resp.data.error.code} (${resp.data.error.type}) with error message: ${resp.data.error.message}`; + this.throwGeneralAxiosError(error); + } else { + let error = "SerwerSMS.pl API returned an unexpected response"; + this.throwGeneralAxiosError(error); + } + } + + return okMsg; + } catch (error) { + this.throwGeneralAxiosError(error); + } + } +} + +module.exports = SerwerSMS; diff --git a/server/notification-providers/sevenio.js b/server/notification-providers/sevenio.js new file mode 100644 index 0000000..eac38a2 --- /dev/null +++ b/server/notification-providers/sevenio.js @@ -0,0 +1,57 @@ +const NotificationProvider = require("./notification-provider"); +const axios = require("axios"); +const { DOWN, UP } = require("../../src/util"); + +class SevenIO extends NotificationProvider { + name = "SevenIO"; + + /** + * @inheritdoc + */ + async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { + const okMsg = "Sent Successfully."; + + const data = { + to: notification.sevenioTo, + from: notification.sevenioSender || "Uptime Kuma", + text: msg, + }; + + const config = { + baseURL: "https://gateway.seven.io/api/", + headers: { + "Content-Type": "application/json", + "X-API-Key": notification.sevenioApiKey, + }, + }; + + try { + // testing or certificate expiry notification + if (heartbeatJSON == null) { + await axios.post("sms", data, config); + return okMsg; + } + + let address = this.extractAddress(monitorJSON); + if (address !== "") { + address = `(${address}) `; + } + + // If heartbeatJSON is not null, we go into the normal alerting loop. + if (heartbeatJSON["status"] === DOWN) { + data.text = `Your service ${monitorJSON["name"]} ${address}went down at ${heartbeatJSON["localDateTime"]} ` + + `(${heartbeatJSON["timezone"]}). Error: ${heartbeatJSON["msg"]}`; + } else if (heartbeatJSON["status"] === UP) { + data.text = `Your service ${monitorJSON["name"]} ${address}went back up at ${heartbeatJSON["localDateTime"]} ` + + `(${heartbeatJSON["timezone"]}).`; + } + await axios.post("sms", data, config); + return okMsg; + } catch (error) { + this.throwGeneralAxiosError(error); + } + } + +} + +module.exports = SevenIO; diff --git a/server/notification-providers/signal.js b/server/notification-providers/signal.js new file mode 100644 index 0000000..9702d06 --- /dev/null +++ b/server/notification-providers/signal.js @@ -0,0 +1,29 @@ +const NotificationProvider = require("./notification-provider"); +const axios = require("axios"); + +class Signal extends NotificationProvider { + name = "signal"; + + /** + * @inheritdoc + */ + async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { + const okMsg = "Sent Successfully."; + + try { + let data = { + "message": msg, + "number": notification.signalNumber, + "recipients": notification.signalRecipients.replace(/\s/g, "").split(","), + }; + let config = {}; + + await axios.post(notification.signalURL, data, config); + return okMsg; + } catch (error) { + this.throwGeneralAxiosError(error); + } + } +} + +module.exports = Signal; diff --git a/server/notification-providers/signl4.js b/server/notification-providers/signl4.js new file mode 100644 index 0000000..8261a73 --- /dev/null +++ b/server/notification-providers/signl4.js @@ -0,0 +1,52 @@ +const NotificationProvider = require("./notification-provider"); +const axios = require("axios"); +const { UP, DOWN } = require("../../src/util"); + +class SIGNL4 extends NotificationProvider { + name = "SIGNL4"; + + /** + * @inheritdoc + */ + async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { + const okMsg = "Sent Successfully."; + + try { + let data = { + heartbeat: heartbeatJSON, + monitor: monitorJSON, + msg, + // Source system + "X-S4-SourceSystem": "UptimeKuma", + monitorUrl: this.extractAddress(monitorJSON), + }; + + const config = { + headers: { + "Content-Type": "application/json" + } + }; + + if (heartbeatJSON == null) { + // Test alert + data.title = "Uptime Kuma Alert"; + data.message = msg; + } else if (heartbeatJSON.status === UP) { + data.title = "Uptime Kuma Monitor ✅ Up"; + data["X-S4-ExternalID"] = "UptimeKuma-" + monitorJSON.monitorID; + data["X-S4-Status"] = "resolved"; + } else if (heartbeatJSON.status === DOWN) { + data.title = "Uptime Kuma Monitor 🔴 Down"; + data["X-S4-ExternalID"] = "UptimeKuma-" + monitorJSON.monitorID; + data["X-S4-Status"] = "new"; + } + + await axios.post(notification.webhookURL, data, config); + return okMsg; + } catch (error) { + this.throwGeneralAxiosError(error); + } + } +} + +module.exports = SIGNL4; diff --git a/server/notification-providers/slack.js b/server/notification-providers/slack.js new file mode 100644 index 0000000..209c7c0 --- /dev/null +++ b/server/notification-providers/slack.js @@ -0,0 +1,173 @@ +const NotificationProvider = require("./notification-provider"); +const axios = require("axios"); +const { setSettings, setting } = require("../util-server"); +const { getMonitorRelativeURL, UP } = require("../../src/util"); + +class Slack extends NotificationProvider { + name = "slack"; + + /** + * Deprecated property notification.slackbutton + * Set it as primary base url if this is not yet set. + * @deprecated + * @param {string} url The primary base URL to use + * @returns {Promise<void>} + */ + static async deprecateURL(url) { + let currentPrimaryBaseURL = await setting("primaryBaseURL"); + + if (!currentPrimaryBaseURL) { + console.log("Move the url to be the primary base URL"); + await setSettings("general", { + primaryBaseURL: url, + }); + } else { + console.log("Already there, no need to move the primary base URL"); + } + } + + /** + * Builds the actions available in the slack message + * @param {string} baseURL Uptime Kuma base URL + * @param {object} monitorJSON The monitor config + * @returns {Array} The relevant action objects + */ + buildActions(baseURL, monitorJSON) { + const actions = []; + + if (baseURL) { + actions.push({ + "type": "button", + "text": { + "type": "plain_text", + "text": "Visit Uptime Kuma", + }, + "value": "Uptime-Kuma", + "url": baseURL + getMonitorRelativeURL(monitorJSON.id), + }); + + } + + const address = this.extractAddress(monitorJSON); + if (address) { + actions.push({ + "type": "button", + "text": { + "type": "plain_text", + "text": "Visit site", + }, + "value": "Site", + "url": address, + }); + } + + return actions; + } + + /** + * Builds the different blocks the Slack message consists of. + * @param {string} baseURL Uptime Kuma base URL + * @param {object} monitorJSON The monitor object + * @param {object} heartbeatJSON The heartbeat object + * @param {string} title The message title + * @param {string} msg The message body + * @returns {Array<object>} The rich content blocks for the Slack message + */ + buildBlocks(baseURL, monitorJSON, heartbeatJSON, title, msg) { + + //create an array to dynamically add blocks + const blocks = []; + + // the header block + blocks.push({ + "type": "header", + "text": { + "type": "plain_text", + "text": title, + }, + }); + + // the body block, containing the details + blocks.push({ + "type": "section", + "fields": [ + { + "type": "mrkdwn", + "text": "*Message*\n" + msg, + }, + { + "type": "mrkdwn", + "text": `*Time (${heartbeatJSON["timezone"]})*\n${heartbeatJSON["localDateTime"]}`, + } + ], + }); + + const actions = this.buildActions(baseURL, monitorJSON); + if (actions.length > 0) { + //the actions block, containing buttons + blocks.push({ + "type": "actions", + "elements": actions, + }); + } + + return blocks; + } + + /** + * @inheritdoc + */ + async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { + const okMsg = "Sent Successfully."; + + if (notification.slackchannelnotify) { + msg += " <!channel>"; + } + + 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": this.buildBlocks(baseURL, monitorJSON, heartbeatJSON, title, msg), + } + ); + } else { + data.text = `${title}\n${msg}`; + } + + if (notification.slackbutton) { + await Slack.deprecateURL(notification.slackbutton); + } + + await axios.post(notification.slackwebhookURL, data); + return okMsg; + } catch (error) { + this.throwGeneralAxiosError(error); + } + + } +} + +module.exports = Slack; diff --git a/server/notification-providers/smsc.js b/server/notification-providers/smsc.js new file mode 100644 index 0000000..89f01d0 --- /dev/null +++ b/server/notification-providers/smsc.js @@ -0,0 +1,47 @@ +const NotificationProvider = require("./notification-provider"); +const axios = require("axios"); + +class SMSC extends NotificationProvider { + name = "smsc"; + + /** + * @inheritdoc + */ + async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { + const okMsg = "Sent Successfully."; + const url = "https://smsc.kz/sys/send.php?"; + + try { + let config = { + headers: { + "Content-Type": "application/json", + "Accept": "text/json", + } + }; + + let getArray = [ + "fmt=3", + "translit=" + notification.smscTranslit, + "login=" + notification.smscLogin, + "psw=" + notification.smscPassword, + "phones=" + notification.smscToNumber, + "mes=" + encodeURIComponent(msg.replace(/[^\x00-\x7F]/g, "")), + ]; + if (notification.smscSenderName !== "") { + getArray.push("sender=" + notification.smscSenderName); + } + + let resp = await axios.get(url + getArray.join("&"), config); + if (resp.data.id === undefined) { + let error = `Something gone wrong. Api returned code ${resp.data.error_code}: ${resp.data.error}`; + this.throwGeneralAxiosError(error); + } + + return okMsg; + } catch (error) { + this.throwGeneralAxiosError(error); + } + } +} + +module.exports = SMSC; diff --git a/server/notification-providers/smseagle.js b/server/notification-providers/smseagle.js new file mode 100644 index 0000000..4e89700 --- /dev/null +++ b/server/notification-providers/smseagle.js @@ -0,0 +1,73 @@ +const NotificationProvider = require("./notification-provider"); +const axios = require("axios"); + +class SMSEagle extends NotificationProvider { + name = "SMSEagle"; + + /** + * @inheritdoc + */ + async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { + const okMsg = "Sent Successfully."; + + try { + let config = { + headers: { + "Content-Type": "application/json", + } + }; + + let postData; + let sendMethod; + let recipientType; + + let encoding = (notification.smseagleEncoding) ? "1" : "0"; + let priority = (notification.smseaglePriority) ? notification.smseaglePriority : "0"; + + if (notification.smseagleRecipientType === "smseagle-contact") { + recipientType = "contactname"; + sendMethod = "sms.send_tocontact"; + } + if (notification.smseagleRecipientType === "smseagle-group") { + recipientType = "groupname"; + sendMethod = "sms.send_togroup"; + } + if (notification.smseagleRecipientType === "smseagle-to") { + recipientType = "to"; + sendMethod = "sms.send_sms"; + } + + let params = { + access_token: notification.smseagleToken, + [recipientType]: notification.smseagleRecipient, + message: msg, + responsetype: "extended", + unicode: encoding, + highpriority: priority + }; + + postData = { + method: sendMethod, + params: params + }; + + let resp = await axios.post(notification.smseagleUrl + "/jsonrpc/sms", postData, config); + + if ((JSON.stringify(resp.data)).indexOf("message_id") === -1) { + let error = ""; + if (resp.data.result && resp.data.result.error_text) { + error = `SMSEagle API returned error: ${JSON.stringify(resp.data.result.error_text)}`; + } else { + error = "SMSEagle API returned an unexpected response"; + } + throw new Error(error); + } + + return okMsg; + } catch (error) { + this.throwGeneralAxiosError(error); + } + } +} + +module.exports = SMSEagle; diff --git a/server/notification-providers/smsmanager.js b/server/notification-providers/smsmanager.js new file mode 100644 index 0000000..d01285d --- /dev/null +++ b/server/notification-providers/smsmanager.js @@ -0,0 +1,29 @@ +const NotificationProvider = require("./notification-provider"); +const axios = require("axios"); + +class SMSManager extends NotificationProvider { + name = "SMSManager"; + + /** + * @inheritdoc + */ + async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { + const okMsg = "Sent Successfully."; + const url = "https://http-api.smsmanager.cz/Send"; + + try { + let data = { + apikey: notification.smsmanagerApiKey, + message: msg.replace(/[^\x00-\x7F]/g, ""), + number: notification.numbers, + gateway: notification.messageType, + }; + await axios.get(`${url}?apikey=${data.apikey}&message=${data.message}&number=${data.number}&gateway=${data.messageType}`); + return okMsg; + } catch (error) { + this.throwGeneralAxiosError(error); + } + } +} + +module.exports = SMSManager; diff --git a/server/notification-providers/smspartner.js b/server/notification-providers/smspartner.js new file mode 100644 index 0000000..5595217 --- /dev/null +++ b/server/notification-providers/smspartner.js @@ -0,0 +1,46 @@ +const NotificationProvider = require("./notification-provider"); +const axios = require("axios"); + +class SMSPartner extends NotificationProvider { + name = "SMSPartner"; + + /** + * @inheritdoc + */ + async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { + const okMsg = "Sent Successfully."; + const url = "https://api.smspartner.fr/v1/send"; + + try { + // smspartner does not support non ascii characters and only a maximum 639 characters + let cleanMsg = msg.replace(/[^\x00-\x7F]/g, "").substring(0, 639); + + let data = { + "apiKey": notification.smspartnerApikey, + "sender": notification.smspartnerSenderName.substring(0, 11), + "phoneNumbers": notification.smspartnerPhoneNumber, + "message": cleanMsg, + }; + + let config = { + headers: { + "Content-Type": "application/json", + "cache-control": "no-cache", + "Accept": "application/json", + } + }; + + let resp = await axios.post(url, data, config); + + if (resp.data.success !== true) { + throw Error(`Api returned ${resp.data.response.status}.`); + } + + return okMsg; + } catch (error) { + this.throwGeneralAxiosError(error); + } + } +} + +module.exports = SMSPartner; diff --git a/server/notification-providers/smtp.js b/server/notification-providers/smtp.js new file mode 100644 index 0000000..9f3defa --- /dev/null +++ b/server/notification-providers/smtp.js @@ -0,0 +1,120 @@ +const nodemailer = require("nodemailer"); +const NotificationProvider = require("./notification-provider"); +const { DOWN } = require("../../src/util"); +const { Liquid } = require("liquidjs"); + +class SMTP extends NotificationProvider { + name = "smtp"; + + /** + * @inheritdoc + */ + async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { + const okMsg = "Sent Successfully."; + + const config = { + host: notification.smtpHost, + port: notification.smtpPort, + secure: notification.smtpSecure, + tls: { + rejectUnauthorized: !notification.smtpIgnoreTLSError || false, + } + }; + + // Fix #1129 + if (notification.smtpDkimDomain) { + config.dkim = { + domainName: notification.smtpDkimDomain, + keySelector: notification.smtpDkimKeySelector, + privateKey: notification.smtpDkimPrivateKey, + hashAlgo: notification.smtpDkimHashAlgo, + headerFieldNames: notification.smtpDkimheaderFieldNames, + skipFields: notification.smtpDkimskipFields, + }; + } + + // Should fix the issue in https://github.com/louislam/uptime-kuma/issues/26#issuecomment-896373904 + if (notification.smtpUsername || notification.smtpPassword) { + config.auth = { + user: notification.smtpUsername, + pass: notification.smtpPassword, + }; + } + + // default values in case the user does not want to template + let subject = msg; + let body = msg; + if (heartbeatJSON) { + body = `${msg}\nTime (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`; + } + // subject and body are templated + if ((monitorJSON && heartbeatJSON) || msg.endsWith("Testing")) { + // cannot end with whitespace as this often raises spam scores + 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); + } + if (customBody !== "") { + const tpl = engine.parse(customBody); + body = await engine.render(tpl, context); + } + } + + // send mail with defined transport object + let transporter = nodemailer.createTransport(config); + await transporter.sendMail({ + from: notification.smtpFrom, + cc: notification.smtpCC, + bcc: notification.smtpBCC, + to: notification.smtpTo, + subject: subject, + text: body, + }); + + 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/splunk.js b/server/notification-providers/splunk.js new file mode 100644 index 0000000..e07c510 --- /dev/null +++ b/server/notification-providers/splunk.js @@ -0,0 +1,114 @@ +const NotificationProvider = require("./notification-provider"); +const axios = require("axios"); +const { UP, DOWN, getMonitorRelativeURL } = require("../../src/util"); +const { setting } = require("../util-server"); +let successMessage = "Sent Successfully."; + +class Splunk extends NotificationProvider { + name = "Splunk"; + + /** + * @inheritdoc + */ + async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { + try { + if (heartbeatJSON == null) { + const title = "Uptime Kuma Alert"; + const monitor = { + type: "ping", + url: "Uptime Kuma Test Button", + }; + return this.postNotification(notification, title, msg, monitor, "trigger"); + } + + if (heartbeatJSON.status === UP) { + const title = "Uptime Kuma Monitor ✅ Up"; + return this.postNotification(notification, title, heartbeatJSON.msg, monitorJSON, "recovery"); + } + + if (heartbeatJSON.status === DOWN) { + const title = "Uptime Kuma Monitor 🔴 Down"; + return this.postNotification(notification, title, heartbeatJSON.msg, monitorJSON, "trigger"); + } + } catch (error) { + this.throwGeneralAxiosError(error); + } + } + + /** + * Check if result is successful, result code should be in range 2xx + * @param {object} result Axios response object + * @returns {void} + * @throws {Error} The status code is not in range 2xx + */ + checkResult(result) { + if (result.status == null) { + throw new Error("Splunk notification failed with invalid response!"); + } + if (result.status < 200 || result.status >= 300) { + throw new Error("Splunk notification failed with status code " + result.status); + } + } + + /** + * Send the message + * @param {BeanModel} notification Message title + * @param {string} title Message title + * @param {string} body Message + * @param {object} monitorInfo Monitor details (For Up/Down only) + * @param {?string} eventAction Action event for PagerDuty (trigger, acknowledge, resolve) + * @returns {Promise<string>} Success state + */ + async postNotification(notification, title, body, monitorInfo, eventAction = "trigger") { + + let monitorUrl; + if (monitorInfo.type === "port") { + monitorUrl = monitorInfo.hostname; + if (monitorInfo.port) { + monitorUrl += ":" + monitorInfo.port; + } + } else if (monitorInfo.hostname != null) { + monitorUrl = monitorInfo.hostname; + } else { + monitorUrl = monitorInfo.url; + } + + if (eventAction === "recovery") { + if (notification.splunkAutoResolve === "0") { + return "No action required"; + } + eventAction = notification.splunkAutoResolve; + } else { + eventAction = notification.splunkSeverity; + } + + const options = { + method: "POST", + url: notification.splunkRestURL, + headers: { "Content-Type": "application/json" }, + data: { + message_type: eventAction, + state_message: `[${title}] [${monitorUrl}] ${body}`, + entity_display_name: "Uptime Kuma Alert: " + monitorInfo.name, + routing_key: notification.pagerdutyIntegrationKey, + entity_id: "Uptime Kuma/" + monitorInfo.id, + } + }; + + const baseURL = await setting("primaryBaseURL"); + if (baseURL && monitorInfo) { + options.client = "Uptime Kuma"; + options.client_url = baseURL + getMonitorRelativeURL(monitorInfo.id); + } + + let result = await axios.request(options); + this.checkResult(result); + if (result.statusText != null) { + return "Splunk notification succeed: " + result.statusText; + } + + return successMessage; + } +} + +module.exports = Splunk; diff --git a/server/notification-providers/squadcast.js b/server/notification-providers/squadcast.js new file mode 100644 index 0000000..5713783 --- /dev/null +++ b/server/notification-providers/squadcast.js @@ -0,0 +1,60 @@ +const NotificationProvider = require("./notification-provider"); +const axios = require("axios"); +const { DOWN } = require("../../src/util"); + +class Squadcast extends NotificationProvider { + name = "squadcast"; + + /** + * @inheritdoc + */ + async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { + const okMsg = "Sent Successfully."; + + try { + + let config = {}; + let data = { + message: msg, + description: "", + tags: {}, + heartbeat: heartbeatJSON, + source: "uptime-kuma" + }; + + if (heartbeatJSON !== null) { + data.description = heartbeatJSON["msg"]; + data.event_id = heartbeatJSON["monitorID"]; + + if (heartbeatJSON["status"] === DOWN) { + data.message = `${monitorJSON["name"]} is DOWN`; + data.status = "trigger"; + } else { + data.message = `${monitorJSON["name"]} is UP`; + data.status = "resolve"; + } + + data.tags["AlertAddress"] = this.extractAddress(monitorJSON); + + monitorJSON["tags"].forEach(tag => { + data.tags[tag["name"]] = { + value: tag["value"] + }; + if (tag["color"] !== null) { + data.tags[tag["name"]]["color"] = tag["color"]; + } + }); + } + + await axios.post(notification.squadcastWebhookURL, data, config); + return okMsg; + + } catch (error) { + this.throwGeneralAxiosError(error); + } + + } + +} + +module.exports = Squadcast; diff --git a/server/notification-providers/stackfield.js b/server/notification-providers/stackfield.js new file mode 100644 index 0000000..65a9245 --- /dev/null +++ b/server/notification-providers/stackfield.js @@ -0,0 +1,44 @@ +const NotificationProvider = require("./notification-provider"); +const axios = require("axios"); +const { setting } = require("../util-server"); +const { getMonitorRelativeURL } = require("../../src/util"); + +class Stackfield extends NotificationProvider { + name = "stackfield"; + + /** + * @inheritdoc + */ + async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { + const okMsg = "Sent Successfully."; + + try { + // Stackfield message formatting: https://www.stackfield.com/help/formatting-messages-2001 + + let textMsg = "+Uptime Kuma Alert+"; + + if (monitorJSON && monitorJSON.name) { + textMsg += `\n*${monitorJSON.name}*`; + } + + textMsg += `\n${msg}`; + + const baseURL = await setting("primaryBaseURL"); + if (baseURL) { + textMsg += `\n${baseURL + getMonitorRelativeURL(monitorJSON.id)}`; + } + + const data = { + "Title": textMsg, + }; + + await axios.post(notification.stackfieldwebhookURL, data); + return okMsg; + } catch (error) { + this.throwGeneralAxiosError(error); + } + + } +} + +module.exports = Stackfield; diff --git a/server/notification-providers/teams.js b/server/notification-providers/teams.js new file mode 100644 index 0000000..2793604 --- /dev/null +++ b/server/notification-providers/teams.js @@ -0,0 +1,240 @@ +const NotificationProvider = require("./notification-provider"); +const axios = require("axios"); +const { setting } = require("../util-server"); +const { DOWN, UP, getMonitorRelativeURL } = require("../../src/util"); + +class Teams extends NotificationProvider { + name = "teams"; + + /** + * 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, withStatusSymbol) => { + if (status === DOWN) { + return (withStatusSymbol ? "🔴 " : "") + `[${monitorName}] went down`; + } else if (status === UP) { + return (withStatusSymbol ? "✅ " : "") + `[${monitorName}] is back online`; + } + return "Notification"; + }; + + /** + * Select the style to use based on status + * @param {const} status The status constant + * @returns {string} Selected style for adaptive cards + */ + _getStyle = (status) => { + if (status === DOWN) { + return "attention"; + } + if (status === UP) { + return "good"; + } + return "emphasis"; + }; + + /** + * Generate payload for notification + * @param {object} args Method arguments + * @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 = ({ + heartbeatJSON, + monitorName, + monitorUrl, + dashboardUrl, + }) => { + const status = heartbeatJSON?.status; + const facts = []; + const actions = []; + + if (dashboardUrl) { + actions.push({ + "type": "Action.OpenUrl", + "title": "Visit Uptime Kuma", + "url": dashboardUrl + }); + } + + if (heartbeatJSON?.msg) { + facts.push({ + title: "Description", + value: heartbeatJSON.msg, + }); + } + + if (monitorName) { + facts.push({ + title: "Monitor", + value: monitorName, + }); + } + + if (monitorUrl && monitorUrl !== "https://") { + facts.push({ + title: "URL", + // 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", + "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": "Medium", + "weight": "Bolder", + "text": `**${this._statusMessageFactory(status, monitorName, false)}**`, + }, + { + "type": "TextBlock", + "size": "Small", + "weight": "Default", + "text": "Uptime Kuma Alert", + "isSubtle": true, + "spacing": "None" + } + ] + } + ] + } + ] + }, + { + "type": "FactSet", + "separator": false, + "facts": facts + } + ], + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "version": "1.5" + } + } + ] + }; + + if (actions) { + payload.attachments[0].content.body.push({ + "type": "ActionSet", + "actions": actions, + }); + } + + return payload; + }; + + /** + * Send the notification + * @param {string} webhookUrl URL to send the request to + * @param {object} payload Payload generated by _notificationPayloadFactory + * @returns {Promise<void>} + */ + _sendNotification = async (webhookUrl, payload) => { + await axios.post(webhookUrl, payload); + }; + + /** + * Send a general notification + * @param {string} webhookUrl URL to send request to + * @param {string} msg Message to send + * @returns {Promise<void>} + */ + _handleGeneralNotification = (webhookUrl, msg) => { + const payload = this._notificationPayloadFactory({ + heartbeatJSON: { + msg: msg + } + }); + + return this._sendNotification(webhookUrl, payload); + }; + + /** + * @inheritdoc + */ + async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { + const okMsg = "Sent Successfully."; + + try { + if (heartbeatJSON == null) { + await this._handleGeneralNotification(notification.webhookUrl, msg); + return okMsg; + } + + const baseURL = await setting("primaryBaseURL"); + let dashboardUrl; + if (baseURL) { + dashboardUrl = baseURL + getMonitorRelativeURL(monitorJSON.id); + } + + const payload = this._notificationPayloadFactory({ + heartbeatJSON: heartbeatJSON, + monitorName: monitorJSON.name, + monitorUrl: this.extractAddress(monitorJSON), + dashboardUrl: dashboardUrl, + }); + + await this._sendNotification(notification.webhookUrl, payload); + return okMsg; + } catch (error) { + this.throwGeneralAxiosError(error); + } + } +} + +module.exports = Teams; diff --git a/server/notification-providers/techulus-push.js b/server/notification-providers/techulus-push.js new file mode 100644 index 0000000..bf688b1 --- /dev/null +++ b/server/notification-providers/techulus-push.js @@ -0,0 +1,36 @@ +const NotificationProvider = require("./notification-provider"); +const axios = require("axios"); + +class TechulusPush extends NotificationProvider { + name = "PushByTechulus"; + + /** + * @inheritdoc + */ + async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { + const okMsg = "Sent Successfully."; + + let data = { + "title": notification?.pushTitle?.length ? notification.pushTitle : "Uptime-Kuma", + "body": msg, + "timeSensitive": notification.pushTimeSensitive ?? true, + }; + + if (notification.pushChannel) { + data.channel = notification.pushChannel; + } + + if (notification.pushSound) { + data.sound = notification.pushSound; + } + + try { + await axios.post(`https://push.techulus.com/api/v1/notify/${notification.pushAPIKey}`, data); + return okMsg; + } catch (error) { + this.throwGeneralAxiosError(error); + } + } +} + +module.exports = TechulusPush; diff --git a/server/notification-providers/telegram.js b/server/notification-providers/telegram.js new file mode 100644 index 0000000..c5bbb19 --- /dev/null +++ b/server/notification-providers/telegram.js @@ -0,0 +1,36 @@ +const NotificationProvider = require("./notification-provider"); +const axios = require("axios"); + +class Telegram extends NotificationProvider { + name = "telegram"; + + /** + * @inheritdoc + */ + async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { + const okMsg = "Sent Successfully."; + const url = "https://api.telegram.org"; + + try { + let params = { + chat_id: notification.telegramChatID, + text: msg, + disable_notification: notification.telegramSendSilently ?? false, + protect_content: notification.telegramProtectContent ?? false, + }; + if (notification.telegramMessageThreadID) { + params.message_thread_id = notification.telegramMessageThreadID; + } + + await axios.get(`${url}/bot${notification.telegramBotToken}/sendMessage`, { + params: params, + }); + return okMsg; + + } catch (error) { + this.throwGeneralAxiosError(error); + } + } +} + +module.exports = Telegram; diff --git a/server/notification-providers/threema.js b/server/notification-providers/threema.js new file mode 100644 index 0000000..07a54ab --- /dev/null +++ b/server/notification-providers/threema.js @@ -0,0 +1,77 @@ +const NotificationProvider = require("./notification-provider"); +const axios = require("axios"); + +class Threema extends NotificationProvider { + name = "threema"; + + /** + * @inheritdoc + */ + async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { + const url = "https://msgapi.threema.ch/send_simple"; + + const config = { + headers: { + "Accept": "*/*", + "Content-Type": "application/x-www-form-urlencoded; charset=utf-8" + } + }; + + const data = { + from: notification.threemaSenderIdentity, + secret: notification.threemaSecret, + text: msg + }; + + switch (notification.threemaRecipientType) { + case "identity": + data.to = notification.threemaRecipient; + break; + case "phone": + data.phone = notification.threemaRecipient; + break; + case "email": + data.email = notification.threemaRecipient; + break; + default: + throw new Error(`Unsupported recipient type: ${notification.threemaRecipientType}`); + } + + try { + await axios.post(url, new URLSearchParams(data), config); + return "Threema notification sent successfully."; + } catch (error) { + const errorMessage = this.handleApiError(error); + this.throwGeneralAxiosError(errorMessage); + } + } + + /** + * Handle Threema API errors + * @param {any} error The error to handle + * @returns {string} Additional error context + */ + handleApiError(error) { + if (!error.response) { + return error.message; + } + switch (error.response.status) { + case 400: + return "Invalid recipient identity or account not set up for basic mode (400)."; + case 401: + return "Incorrect API identity or secret (401)."; + case 402: + return "No credits remaining (402)."; + case 404: + return "Recipient not found (404)."; + case 413: + return "Message is too long (413)."; + case 500: + return "Temporary internal server error (500)."; + default: + return error.message; + } + } +} + +module.exports = Threema; diff --git a/server/notification-providers/twilio.js b/server/notification-providers/twilio.js new file mode 100644 index 0000000..c38a6d7 --- /dev/null +++ b/server/notification-providers/twilio.js @@ -0,0 +1,38 @@ +const NotificationProvider = require("./notification-provider"); +const axios = require("axios"); + +class Twilio extends NotificationProvider { + name = "twilio"; + + /** + * @inheritdoc + */ + async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { + const okMsg = "Sent Successfully."; + + let apiKey = notification.twilioApiKey ? notification.twilioApiKey : notification.twilioAccountSID; + + try { + let config = { + headers: { + "Content-Type": "application/x-www-form-urlencoded;charset=utf-8", + "Authorization": "Basic " + Buffer.from(apiKey + ":" + notification.twilioAuthToken).toString("base64"), + } + }; + + let data = new URLSearchParams(); + data.append("To", notification.twilioToNumber); + data.append("From", notification.twilioFromNumber); + data.append("Body", msg); + + await axios.post(`https://api.twilio.com/2010-04-01/Accounts/${(notification.twilioAccountSID)}/Messages.json`, data, config); + + return okMsg; + } catch (error) { + this.throwGeneralAxiosError(error); + } + } + +} + +module.exports = Twilio; diff --git a/server/notification-providers/webhook.js b/server/notification-providers/webhook.js new file mode 100644 index 0000000..986986d --- /dev/null +++ b/server/notification-providers/webhook.js @@ -0,0 +1,66 @@ +const NotificationProvider = require("./notification-provider"); +const axios = require("axios"); +const FormData = require("form-data"); +const { Liquid } = require("liquidjs"); + +class Webhook extends NotificationProvider { + name = "webhook"; + + /** + * @inheritdoc + */ + async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { + const okMsg = "Sent Successfully."; + + try { + let data = { + heartbeat: heartbeatJSON, + monitor: monitorJSON, + msg, + }; + let config = { + headers: {} + }; + + if (notification.webhookContentType === "form-data") { + const formData = new FormData(); + formData.append("data", JSON.stringify(data)); + 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 + }); + } + + if (notification.webhookAdditionalHeaders) { + try { + config.headers = { + ...config.headers, + ...JSON.parse(notification.webhookAdditionalHeaders) + }; + } catch (err) { + throw "Additional Headers is not a valid JSON"; + } + } + + await axios.post(notification.webhookURL, data, config); + return okMsg; + + } catch (error) { + this.throwGeneralAxiosError(error); + } + + } + +} + +module.exports = Webhook; diff --git a/server/notification-providers/wecom.js b/server/notification-providers/wecom.js new file mode 100644 index 0000000..1eb0690 --- /dev/null +++ b/server/notification-providers/wecom.js @@ -0,0 +1,51 @@ +const NotificationProvider = require("./notification-provider"); +const axios = require("axios"); +const { DOWN, UP } = require("../../src/util"); + +class WeCom extends NotificationProvider { + name = "WeCom"; + + /** + * @inheritdoc + */ + async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { + const okMsg = "Sent Successfully."; + + try { + let config = { + headers: { + "Content-Type": "application/json" + } + }; + let body = this.composeMessage(heartbeatJSON, msg); + await axios.post(`https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=${notification.weComBotKey}`, body, config); + return okMsg; + } catch (error) { + this.throwGeneralAxiosError(error); + } + } + + /** + * Generate the message to send + * @param {object} heartbeatJSON Heartbeat details (For Up/Down only) + * @param {string} msg General message + * @returns {object} Message + */ + composeMessage(heartbeatJSON, msg) { + let title = "UptimeKuma Message"; + if (msg != null && heartbeatJSON != null && heartbeatJSON["status"] === UP) { + title = "UptimeKuma Monitor Up"; + } + if (msg != null && heartbeatJSON != null && heartbeatJSON["status"] === DOWN) { + title = "UptimeKuma Monitor Down"; + } + return { + msgtype: "text", + text: { + content: title + "\n" + msg + } + }; + } +} + +module.exports = WeCom; diff --git a/server/notification-providers/whapi.js b/server/notification-providers/whapi.js new file mode 100644 index 0000000..70e0fbb --- /dev/null +++ b/server/notification-providers/whapi.js @@ -0,0 +1,39 @@ +const NotificationProvider = require("./notification-provider"); +const axios = require("axios"); + +class Whapi extends NotificationProvider { + name = "whapi"; + + /** + * @inheritdoc + */ + async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { + const okMsg = "Sent Successfully."; + + try { + const config = { + headers: { + "Accept": "application/json", + "Content-Type": "application/json", + "Authorization": "Bearer " + notification.whapiAuthToken, + } + }; + + let data = { + "to": notification.whapiRecipient, + "body": msg, + }; + + let url = (notification.whapiApiUrl || "https://gate.whapi.cloud/").replace(/\/+$/, "") + "/messages/text"; + + await axios.post(url, data, config); + + return okMsg; + } catch (error) { + this.throwGeneralAxiosError(error); + } + } + +} + +module.exports = Whapi; diff --git a/server/notification-providers/wpush.js b/server/notification-providers/wpush.js new file mode 100644 index 0000000..db043f9 --- /dev/null +++ b/server/notification-providers/wpush.js @@ -0,0 +1,51 @@ +const NotificationProvider = require("./notification-provider"); +const axios = require("axios"); +const { DOWN, UP } = require("../../src/util"); + +class WPush extends NotificationProvider { + name = "WPush"; + + /** + * @inheritdoc + */ + async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { + const okMsg = "Sent Successfully."; + + try { + const context = { + "title": this.checkStatus(heartbeatJSON, monitorJSON), + "content": msg, + "apikey": notification.wpushAPIkey, + "channel": notification.wpushChannel + }; + const result = await axios.post("https://api.wpush.cn/api/v1/send", context); + if (result.data.code !== 0) { + throw result.data.message; + } + + return okMsg; + + } catch (error) { + this.throwGeneralAxiosError(error); + } + } + + /** + * Get the formatted title for message + * @param {?object} heartbeatJSON Heartbeat details (For Up/Down only) + * @param {?object} monitorJSON Monitor details (For Up/Down only) + * @returns {string} Formatted title + */ + checkStatus(heartbeatJSON, monitorJSON) { + let title = "UptimeKuma Message"; + if (heartbeatJSON != null && heartbeatJSON["status"] === UP) { + title = "UptimeKuma Monitor Up " + monitorJSON["name"]; + } + if (heartbeatJSON != null && heartbeatJSON["status"] === DOWN) { + title = "UptimeKuma Monitor Down " + monitorJSON["name"]; + } + return title; + } +} + +module.exports = WPush; diff --git a/server/notification-providers/zoho-cliq.js b/server/notification-providers/zoho-cliq.js new file mode 100644 index 0000000..3a504de --- /dev/null +++ b/server/notification-providers/zoho-cliq.js @@ -0,0 +1,101 @@ +const NotificationProvider = require("./notification-provider"); +const axios = require("axios"); +const { DOWN, UP } = require("../../src/util"); + +class ZohoCliq extends NotificationProvider { + name = "ZohoCliq"; + + /** + * Generate the message to send + * @param {const} status The status constant + * @param {string} monitorName Name of monitor + * @returns {string} Status message + */ + _statusMessageFactory = (status, monitorName) => { + if (status === DOWN) { + return `🔴 [${monitorName}] went down\n`; + } else if (status === UP) { + return `### ✅ [${monitorName}] is back online\n`; + } + return "Notification\n"; + }; + + /** + * Send the notification + * @param {string} webhookUrl URL to send the request to + * @param {Array} payload Payload generated by _notificationPayloadFactory + * @returns {Promise<void>} + */ + _sendNotification = async (webhookUrl, payload) => { + await axios.post(webhookUrl, { text: payload.join("\n") }); + }; + + /** + * 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 + * @returns {Array} Notification payload + */ + _notificationPayloadFactory = ({ + status, + monitorMessage, + monitorName, + monitorUrl, + }) => { + const payload = []; + payload.push(this._statusMessageFactory(status, monitorName)); + payload.push(`*Description:* ${monitorMessage}`); + + if (monitorUrl && monitorUrl !== "https://") { + payload.push(`*URL:* ${monitorUrl}`); + } + + return payload; + }; + + /** + * Send a general notification + * @param {string} webhookUrl URL to send request to + * @param {string} msg Message to send + * @returns {Promise<void>} + */ + _handleGeneralNotification = (webhookUrl, msg) => { + const payload = this._notificationPayloadFactory({ + monitorMessage: msg + }); + + return this._sendNotification(webhookUrl, payload); + }; + + /** + * @inheritdoc + */ + async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { + const okMsg = "Sent Successfully."; + + try { + if (heartbeatJSON == null) { + await this._handleGeneralNotification(notification.webhookUrl, msg); + return okMsg; + } + + const payload = this._notificationPayloadFactory({ + monitorMessage: heartbeatJSON.msg, + monitorName: monitorJSON.name, + monitorUrl: this.extractAddress(monitorJSON), + status: heartbeatJSON.status + }); + + await this._sendNotification(notification.webhookUrl, payload); + return okMsg; + + } catch (error) { + this.throwGeneralAxiosError(error); + } + } +} + +module.exports = ZohoCliq; |