summaryrefslogtreecommitdiffstats
path: root/server/notification-providers
diff options
context:
space:
mode:
Diffstat (limited to 'server/notification-providers')
-rw-r--r--server/notification-providers/46elks.js35
-rw-r--r--server/notification-providers/alerta.js68
-rw-r--r--server/notification-providers/alertnow.js53
-rw-r--r--server/notification-providers/aliyun-sms.js143
-rw-r--r--server/notification-providers/apprise.js37
-rw-r--r--server/notification-providers/bitrix24.js31
-rw-r--r--server/notification-providers/call-me-bot.js23
-rw-r--r--server/notification-providers/cellsynt.js39
-rw-r--r--server/notification-providers/clicksendsms.js45
-rw-r--r--server/notification-providers/dingding.js101
-rw-r--r--server/notification-providers/discord.js120
-rw-r--r--server/notification-providers/feishu.js104
-rw-r--r--server/notification-providers/flashduty.js108
-rw-r--r--server/notification-providers/freemobile.js27
-rw-r--r--server/notification-providers/goalert.js36
-rw-r--r--server/notification-providers/google-chat.js94
-rw-r--r--server/notification-providers/gorush.js44
-rw-r--r--server/notification-providers/gotify.js31
-rw-r--r--server/notification-providers/grafana-oncall.js51
-rw-r--r--server/notification-providers/gtx-messaging.js33
-rw-r--r--server/notification-providers/heii-oncall.js52
-rw-r--r--server/notification-providers/home-assistant.js45
-rw-r--r--server/notification-providers/keep.js42
-rw-r--r--server/notification-providers/kook.js34
-rw-r--r--server/notification-providers/line.js69
-rw-r--r--server/notification-providers/linenotify.js52
-rw-r--r--server/notification-providers/lunasea.js67
-rw-r--r--server/notification-providers/matrix.js48
-rw-r--r--server/notification-providers/mattermost.js110
-rw-r--r--server/notification-providers/nostr.js122
-rw-r--r--server/notification-providers/notification-provider.js73
-rw-r--r--server/notification-providers/ntfy.js83
-rw-r--r--server/notification-providers/octopush.js76
-rw-r--r--server/notification-providers/onebot.js48
-rw-r--r--server/notification-providers/onesender.js47
-rw-r--r--server/notification-providers/opsgenie.js96
-rw-r--r--server/notification-providers/pagerduty.js114
-rw-r--r--server/notification-providers/pagertree.js93
-rw-r--r--server/notification-providers/promosms.js53
-rw-r--r--server/notification-providers/pushbullet.js56
-rw-r--r--server/notification-providers/pushdeer.js55
-rw-r--r--server/notification-providers/pushover.js58
-rw-r--r--server/notification-providers/pushy.js32
-rw-r--r--server/notification-providers/rocket-chat.js67
-rw-r--r--server/notification-providers/send-grid.js65
-rw-r--r--server/notification-providers/serverchan.js51
-rw-r--r--server/notification-providers/serwersms.js47
-rw-r--r--server/notification-providers/sevenio.js57
-rw-r--r--server/notification-providers/signal.js29
-rw-r--r--server/notification-providers/signl4.js52
-rw-r--r--server/notification-providers/slack.js173
-rw-r--r--server/notification-providers/smsc.js47
-rw-r--r--server/notification-providers/smseagle.js73
-rw-r--r--server/notification-providers/smsmanager.js29
-rw-r--r--server/notification-providers/smspartner.js46
-rw-r--r--server/notification-providers/smtp.js120
-rw-r--r--server/notification-providers/splunk.js114
-rw-r--r--server/notification-providers/squadcast.js60
-rw-r--r--server/notification-providers/stackfield.js44
-rw-r--r--server/notification-providers/teams.js240
-rw-r--r--server/notification-providers/techulus-push.js36
-rw-r--r--server/notification-providers/telegram.js36
-rw-r--r--server/notification-providers/threema.js77
-rw-r--r--server/notification-providers/twilio.js38
-rw-r--r--server/notification-providers/webhook.js66
-rw-r--r--server/notification-providers/wecom.js51
-rw-r--r--server/notification-providers/whapi.js39
-rw-r--r--server/notification-providers/wpush.js51
-rw-r--r--server/notification-providers/zoho-cliq.js101
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}&timestamp=${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;