From da74391c3e6c315d2f317f7f34224a50a4431520 Mon Sep 17 00:00:00 2001 From: LouisLam Date: Tue, 7 Sep 2021 22:42:46 +0800 Subject: [PATCH] convert notifications into modules --- .eslintrc.js | 3 +- server/notification-providers/apprise.js | 26 + server/notification-providers/discord.js | 105 +++ server/notification-providers/gotify.js | 28 + server/notification-providers/line.js | 60 ++ server/notification-providers/lunasea.js | 48 ++ server/notification-providers/mattermost.js | 123 +++ .../notification-provider.js | 36 + server/notification-providers/octopush.js | 40 + server/notification-providers/pushbullet.js | 50 ++ server/notification-providers/pushover.js | 49 ++ server/notification-providers/pushy.js | 30 + server/notification-providers/rocket-chat.js | 46 ++ server/notification-providers/signal.js | 27 + server/notification-providers/slack.js | 70 ++ server/notification-providers/smtp.js | 43 ++ server/notification-providers/telegram.js | 27 + server/notification-providers/webhook.js | 44 ++ server/notification.js | 714 ++---------------- server/server.js | 3 + 20 files changed, 918 insertions(+), 654 deletions(-) create mode 100644 server/notification-providers/apprise.js create mode 100644 server/notification-providers/discord.js create mode 100644 server/notification-providers/gotify.js create mode 100644 server/notification-providers/line.js create mode 100644 server/notification-providers/lunasea.js create mode 100644 server/notification-providers/mattermost.js create mode 100644 server/notification-providers/notification-provider.js create mode 100644 server/notification-providers/octopush.js create mode 100644 server/notification-providers/pushbullet.js create mode 100644 server/notification-providers/pushover.js create mode 100644 server/notification-providers/pushy.js create mode 100644 server/notification-providers/rocket-chat.js create mode 100644 server/notification-providers/signal.js create mode 100644 server/notification-providers/slack.js create mode 100644 server/notification-providers/smtp.js create mode 100644 server/notification-providers/telegram.js create mode 100644 server/notification-providers/webhook.js diff --git a/.eslintrc.js b/.eslintrc.js index fe63d4a4..6704a85b 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -17,7 +17,8 @@ module.exports = { }, rules: { "camelcase": ["warn", { - "properties": "never" + "properties": "never", + "ignoreImports": true }], // override/add rules settings here, such as: // 'vue/no-unused-vars': 'error' diff --git a/server/notification-providers/apprise.js b/server/notification-providers/apprise.js new file mode 100644 index 00000000..fdcd8d61 --- /dev/null +++ b/server/notification-providers/apprise.js @@ -0,0 +1,26 @@ +const NotificationProvider = require("./notification-provider"); +const child_process = require("child_process"); + +class Apprise extends NotificationProvider { + + name = "apprise"; + + async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { + let s = child_process.spawnSync("apprise", [ "-vv", "-b", msg, notification.appriseURL]) + + let output = (s.stdout) ? s.stdout.toString() : "ERROR: maybe apprise not found"; + + if (output) { + + if (! output.includes("ERROR")) { + return "Sent Successfully"; + } + + throw new Error(output) + } else { + return "No output from apprise"; + } + } +} + +module.exports = Apprise; diff --git a/server/notification-providers/discord.js b/server/notification-providers/discord.js new file mode 100644 index 00000000..d6ee0afe --- /dev/null +++ b/server/notification-providers/discord.js @@ -0,0 +1,105 @@ +const NotificationProvider = require("./notification-provider"); +const axios = require("axios"); +const { DOWN, UP } = require("../../src/util"); + +class Discord extends NotificationProvider { + + name = "discord"; + + async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { + let okMsg = "Sent Successfully. "; + + try { + const discordDisplayName = notification.discordUsername || "Uptime Kuma"; + + // If heartbeatJSON is null, assume we're testing. + if (heartbeatJSON == null) { + let discordtestdata = { + username: discordDisplayName, + content: msg, + } + await axios.post(notification.discordWebhookUrl, discordtestdata) + return okMsg; + } + + let url; + + if (monitorJSON["type"] === "port") { + url = monitorJSON["hostname"]; + if (monitorJSON["port"]) { + url += ":" + monitorJSON["port"]; + } + + } else { + url = monitorJSON["url"]; + } + + // 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: "Service URL", + value: url, + }, + { + name: "Time (UTC)", + value: heartbeatJSON["time"], + }, + { + name: "Error", + value: heartbeatJSON["msg"], + }, + ], + }], + } + await axios.post(notification.discordWebhookUrl, 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: "Service URL", + value: url.startsWith("http") ? "[Visit Service](" + url + ")" : url, + }, + { + name: "Time (UTC)", + value: heartbeatJSON["time"], + }, + { + name: "Ping", + value: heartbeatJSON["ping"] + "ms", + }, + ], + }], + } + await axios.post(notification.discordWebhookUrl, discordupdata) + return okMsg; + } + } catch (error) { + this.throwGeneralAxiosError(error) + } + } + +} + +module.exports = Discord; diff --git a/server/notification-providers/gotify.js b/server/notification-providers/gotify.js new file mode 100644 index 00000000..9d2d55aa --- /dev/null +++ b/server/notification-providers/gotify.js @@ -0,0 +1,28 @@ +const NotificationProvider = require("./notification-provider"); +const axios = require("axios"); + +class Gotify extends NotificationProvider { + + name = "gotify"; + + async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { + let 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/line.js b/server/notification-providers/line.js new file mode 100644 index 00000000..83096903 --- /dev/null +++ b/server/notification-providers/line.js @@ -0,0 +1,60 @@ +const NotificationProvider = require("./notification-provider"); +const axios = require("axios"); +const { DOWN, UP } = require("../../src/util"); + +class Line extends NotificationProvider { + + name = "line"; + + async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { + let okMsg = "Sent Successfully. "; + try { + let lineAPIUrl = "https://api.line.me/v2/bot/message/push"; + 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(lineAPIUrl, 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 (UTC): " + heartbeatJSON["time"] + } + ] + } + await axios.post(lineAPIUrl, 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 (UTC): " + heartbeatJSON["time"] + } + ] + } + await axios.post(lineAPIUrl, upMessage, config) + } + return okMsg; + } catch (error) { + this.throwGeneralAxiosError(error) + } + } +} + +module.exports = Line; diff --git a/server/notification-providers/lunasea.js b/server/notification-providers/lunasea.js new file mode 100644 index 00000000..fb6cd236 --- /dev/null +++ b/server/notification-providers/lunasea.js @@ -0,0 +1,48 @@ +const NotificationProvider = require("./notification-provider"); +const axios = require("axios"); +const { DOWN, UP } = require("../../src/util"); + +class LunaSea extends NotificationProvider { + + name = "lunasea"; + + async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { + let okMsg = "Sent Successfully. "; + let lunaseadevice = "https://notify.lunasea.app/v1/custom/device/" + notification.lunaseaDevice + + try { + if (heartbeatJSON == null) { + let testdata = { + "title": "Uptime Kuma Alert", + "body": "Testing Successful.", + } + await axios.post(lunaseadevice, testdata) + return okMsg; + } + + if (heartbeatJSON["status"] == DOWN) { + let downdata = { + "title": "UptimeKuma Alert: " + monitorJSON["name"], + "body": "[🔴 Down] " + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"], + } + await axios.post(lunaseadevice, downdata) + return okMsg; + } + + if (heartbeatJSON["status"] == UP) { + let updata = { + "title": "UptimeKuma Alert: " + monitorJSON["name"], + "body": "[✅ Up] " + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"], + } + await axios.post(lunaseadevice, updata) + return okMsg; + } + + } catch (error) { + this.throwGeneralAxiosError(error) + } + + } +} + +module.exports = LunaSea; diff --git a/server/notification-providers/mattermost.js b/server/notification-providers/mattermost.js new file mode 100644 index 00000000..97779435 --- /dev/null +++ b/server/notification-providers/mattermost.js @@ -0,0 +1,123 @@ +const NotificationProvider = require("./notification-provider"); +const axios = require("axios"); +const { DOWN, UP } = require("../../src/util"); + +class Mattermost extends NotificationProvider { + + name = "mattermost"; + + async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { + let okMsg = "Sent Successfully. "; + try { + const mattermostUserName = notification.mattermostusername || "Uptime Kuma"; + // If heartbeatJSON is null, assume we're testing. + if (heartbeatJSON == null) { + let mattermostTestData = { + username: mattermostUserName, + text: msg, + } + await axios.post(notification.mattermostWebhookUrl, mattermostTestData) + return okMsg; + } + + const mattermostChannel = notification.mattermostchannel; + const mattermostIconEmoji = notification.mattermosticonemo; + const mattermostIconUrl = notification.mattermosticonurl; + + if (heartbeatJSON["status"] == DOWN) { + let mattermostdowndata = { + username: mattermostUserName, + text: "Uptime Kuma Alert", + channel: mattermostChannel, + icon_emoji: mattermostIconEmoji, + icon_url: mattermostIconUrl, + attachments: [ + { + fallback: + "Your " + + monitorJSON["name"] + + " service went down.", + color: "#FF0000", + title: + "❌ " + + monitorJSON["name"] + + " service went down. ❌", + title_link: monitorJSON["url"], + fields: [ + { + short: true, + title: "Service Name", + value: monitorJSON["name"], + }, + { + short: true, + title: "Time (UTC)", + value: heartbeatJSON["time"], + }, + { + short: false, + title: "Error", + value: heartbeatJSON["msg"], + }, + ], + }, + ], + }; + await axios.post( + notification.mattermostWebhookUrl, + mattermostdowndata + ); + return okMsg; + } else if (heartbeatJSON["status"] == UP) { + let mattermostupdata = { + username: mattermostUserName, + text: "Uptime Kuma Alert", + channel: mattermostChannel, + icon_emoji: mattermostIconEmoji, + icon_url: mattermostIconUrl, + attachments: [ + { + fallback: + "Your " + + monitorJSON["name"] + + " service went up!", + color: "#32CD32", + title: + "✅ " + + monitorJSON["name"] + + " service went up! ✅", + title_link: monitorJSON["url"], + fields: [ + { + short: true, + title: "Service Name", + value: monitorJSON["name"], + }, + { + short: true, + title: "Time (UTC)", + value: heartbeatJSON["time"], + }, + { + short: false, + title: "Ping", + value: heartbeatJSON["ping"] + "ms", + }, + ], + }, + ], + }; + await axios.post( + notification.mattermostWebhookUrl, + mattermostupdata + ); + return okMsg; + } + } catch (error) { + this.throwGeneralAxiosError(error); + } + + } +} + +module.exports = Mattermost; diff --git a/server/notification-providers/notification-provider.js b/server/notification-providers/notification-provider.js new file mode 100644 index 00000000..61c6242d --- /dev/null +++ b/server/notification-providers/notification-provider.js @@ -0,0 +1,36 @@ +class NotificationProvider { + + /** + * Notification Provider Name + * @type string + */ + name = undefined; + + /** + * @param notification : BeanModel + * @param msg : string General Message + * @param monitorJSON : object Monitor details (For Up/Down only) + * @param heartbeatJSON : object Heartbeat details (For Up/Down only) + * @returns {Promise} Return Successful Message + * Throw Error with fail msg + */ + async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { + throw new Error("Have to override Notification.send(...)"); + } + + 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/octopush.js b/server/notification-providers/octopush.js new file mode 100644 index 00000000..40273f9b --- /dev/null +++ b/server/notification-providers/octopush.js @@ -0,0 +1,40 @@ +const NotificationProvider = require("./notification-provider"); +const axios = require("axios"); + +class Octopush extends NotificationProvider { + + name = "octopush"; + + async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { + let okMsg = "Sent Successfully. "; + + try { + 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("https://api.octopush.com/v1/public/sms-campaign/send", data, config) + return okMsg; + } catch (error) { + this.throwGeneralAxiosError(error); + } + } +} + +module.exports = Octopush; diff --git a/server/notification-providers/pushbullet.js b/server/notification-providers/pushbullet.js new file mode 100644 index 00000000..0ed6f0fd --- /dev/null +++ b/server/notification-providers/pushbullet.js @@ -0,0 +1,50 @@ +const NotificationProvider = require("./notification-provider"); +const axios = require("axios"); + +const { DOWN, UP } = require("../../src/util"); + +class Pushbullet extends NotificationProvider { + + name = "pushbullet"; + + async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { + let okMsg = "Sent Successfully. "; + + try { + let pushbulletUrl = "https://api.pushbullet.com/v2/pushes"; + let config = { + headers: { + "Access-Token": notification.pushbulletAccessToken, + "Content-Type": "application/json" + } + }; + if (heartbeatJSON == null) { + let testdata = { + "type": "note", + "title": "Uptime Kuma Alert", + "body": "Testing Successful.", + } + await axios.post(pushbulletUrl, testdata, config) + } else if (heartbeatJSON["status"] == DOWN) { + let downdata = { + "type": "note", + "title": "UptimeKuma Alert: " + monitorJSON["name"], + "body": "[🔴 Down] " + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"], + } + await axios.post(pushbulletUrl, downdata, config) + } else if (heartbeatJSON["status"] == UP) { + let updata = { + "type": "note", + "title": "UptimeKuma Alert: " + monitorJSON["name"], + "body": "[✅ Up] " + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"], + } + await axios.post(pushbulletUrl, updata, config) + } + return okMsg; + } catch (error) { + this.throwGeneralAxiosError(error) + } + } +} + +module.exports = Pushbullet; diff --git a/server/notification-providers/pushover.js b/server/notification-providers/pushover.js new file mode 100644 index 00000000..2133ca1c --- /dev/null +++ b/server/notification-providers/pushover.js @@ -0,0 +1,49 @@ +const NotificationProvider = require("./notification-provider"); +const axios = require("axios"); + +class Pushover extends NotificationProvider { + + name = "pushover"; + + async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { + let okMsg = "Sent Successfully. "; + let pushoverlink = "https://api.pushover.net/1/messages.json" + + try { + if (heartbeatJSON == null) { + let data = { + "message": "Uptime Kuma Pushover testing successful.", + "user": notification.pushoveruserkey, + "token": notification.pushoverapptoken, + "sound": notification.pushoversounds, + "priority": notification.pushoverpriority, + "title": notification.pushovertitle, + "retry": "30", + "expire": "3600", + "html": 1, + } + await axios.post(pushoverlink, data) + return okMsg; + } + + let data = { + "message": "Uptime Kuma Alert\n\nMessage:" + msg + "\nTime (UTC):" + heartbeatJSON["time"], + "user": notification.pushoveruserkey, + "token": notification.pushoverapptoken, + "sound": notification.pushoversounds, + "priority": notification.pushoverpriority, + "title": notification.pushovertitle, + "retry": "30", + "expire": "3600", + "html": 1, + } + await axios.post(pushoverlink, 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 00000000..431cf8c3 --- /dev/null +++ b/server/notification-providers/pushy.js @@ -0,0 +1,30 @@ +const NotificationProvider = require("./notification-provider"); +const axios = require("axios"); + +class Pushy extends NotificationProvider { + + name = "pushy"; + + async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { + let 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 00000000..14918965 --- /dev/null +++ b/server/notification-providers/rocket-chat.js @@ -0,0 +1,46 @@ +const NotificationProvider = require("./notification-provider"); +const axios = require("axios"); + +class RocketChat extends NotificationProvider { + + name = "rocket.chat"; + + async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { + let okMsg = "Sent Successfully. "; + try { + if (heartbeatJSON == null) { + let data = { + "text": "Uptime Kuma Rocket.chat testing successful.", + "channel": notification.rocketchannel, + "username": notification.rocketusername, + "icon_emoji": notification.rocketiconemo, + } + await axios.post(notification.rocketwebhookURL, data) + return okMsg; + } + + const time = heartbeatJSON["time"]; + let data = { + "text": "Uptime Kuma Alert", + "channel": notification.rocketchannel, + "username": notification.rocketusername, + "icon_emoji": notification.rocketiconemo, + "attachments": [ + { + "title": "Uptime Kuma Alert *Time (UTC)*\n" + time, + "title_link": notification.rocketbutton, + "text": "*Message*\n" + msg, + "color": "#32cd32" + } + ] + } + await axios.post(notification.rocketwebhookURL, data) + return okMsg; + } catch (error) { + this.throwGeneralAxiosError(error) + } + + } +} + +module.exports = RocketChat; diff --git a/server/notification-providers/signal.js b/server/notification-providers/signal.js new file mode 100644 index 00000000..ba5f87f9 --- /dev/null +++ b/server/notification-providers/signal.js @@ -0,0 +1,27 @@ +const NotificationProvider = require("./notification-provider"); +const axios = require("axios"); + +class Signal extends NotificationProvider { + + name = "signal"; + + async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { + let 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/slack.js b/server/notification-providers/slack.js new file mode 100644 index 00000000..661df5a0 --- /dev/null +++ b/server/notification-providers/slack.js @@ -0,0 +1,70 @@ +const NotificationProvider = require("./notification-provider"); +const axios = require("axios"); + +class Slack extends NotificationProvider { + + name = "slack"; + + async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { + let okMsg = "Sent Successfully. "; + try { + if (heartbeatJSON == null) { + let data = { + "text": "Uptime Kuma Slack testing successful.", + "channel": notification.slackchannel, + "username": notification.slackusername, + "icon_emoji": notification.slackiconemo, + } + await axios.post(notification.slackwebhookURL, data) + return okMsg; + } + + const time = heartbeatJSON["time"]; + let data = { + "text": "Uptime Kuma Alert", + "channel": notification.slackchannel, + "username": notification.slackusername, + "icon_emoji": notification.slackiconemo, + "blocks": [{ + "type": "header", + "text": { + "type": "plain_text", + "text": "Uptime Kuma Alert", + }, + }, + { + "type": "section", + "fields": [{ + "type": "mrkdwn", + "text": "*Message*\n" + msg, + }, + { + "type": "mrkdwn", + "text": "*Time (UTC)*\n" + time, + }], + }, + { + "type": "actions", + "elements": [ + { + "type": "button", + "text": { + "type": "plain_text", + "text": "Visit Uptime Kuma", + }, + "value": "Uptime-Kuma", + "url": notification.slackbutton || "https://github.com/louislam/uptime-kuma", + }, + ], + }], + } + await axios.post(notification.slackwebhookURL, data) + return okMsg; + } catch (error) { + this.throwGeneralAxiosError(error) + } + + } +} + +module.exports = Slack; diff --git a/server/notification-providers/smtp.js b/server/notification-providers/smtp.js new file mode 100644 index 00000000..4914c074 --- /dev/null +++ b/server/notification-providers/smtp.js @@ -0,0 +1,43 @@ +const nodemailer = require("nodemailer"); +const NotificationProvider = require("./notification-provider"); + +class SMTP extends NotificationProvider { + + name = "smtp"; + + async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { + + const config = { + host: notification.smtpHost, + port: notification.smtpPort, + secure: notification.smtpSecure, + }; + + // 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, + }; + } + + let transporter = nodemailer.createTransport(config); + + let bodyTextContent = msg; + if (heartbeatJSON) { + bodyTextContent = `${msg}\nTime (UTC): ${heartbeatJSON["time"]}`; + } + + // send mail with defined transport object + await transporter.sendMail({ + from: `"Uptime Kuma" <${notification.smtpFrom}>`, + to: notification.smtpTo, + subject: msg, + text: bodyTextContent, + }); + + return "Sent Successfully."; + } +} + +module.exports = SMTP; diff --git a/server/notification-providers/telegram.js b/server/notification-providers/telegram.js new file mode 100644 index 00000000..f88dcf5d --- /dev/null +++ b/server/notification-providers/telegram.js @@ -0,0 +1,27 @@ +const NotificationProvider = require("./notification-provider"); +const axios = require("axios"); + +class Telegram extends NotificationProvider { + + name = "telegram"; + + async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { + let okMsg = "Sent Successfully. "; + + try { + await axios.get(`https://api.telegram.org/bot${notification.telegramBotToken}/sendMessage`, { + params: { + chat_id: notification.telegramChatID, + text: msg, + }, + }) + return okMsg; + + } catch (error) { + let msg = (error.response.data.description) ? error.response.data.description : "Error without description" + throw new Error(msg) + } + } +} + +module.exports = Telegram; diff --git a/server/notification-providers/webhook.js b/server/notification-providers/webhook.js new file mode 100644 index 00000000..197e9f9f --- /dev/null +++ b/server/notification-providers/webhook.js @@ -0,0 +1,44 @@ +const NotificationProvider = require("./notification-provider"); +const axios = require("axios"); +const FormData = require("form-data"); + +class Webhook extends NotificationProvider { + + name = "webhook"; + + async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { + let okMsg = "Sent Successfully. "; + + try { + let data = { + heartbeat: heartbeatJSON, + monitor: monitorJSON, + msg, + }; + let finalData; + let config = {}; + + if (notification.webhookContentType === "form-data") { + finalData = new FormData(); + finalData.append("data", JSON.stringify(data)); + + config = { + headers: finalData.getHeaders(), + } + + } else { + finalData = data; + } + + await axios.post(notification.webhookURL, finalData, config) + return okMsg; + + } catch (error) { + this.throwGeneralAxiosError(error) + } + + } + +} + +module.exports = Webhook; diff --git a/server/notification.js b/server/notification.js index f78401d9..5d2ffb03 100644 --- a/server/notification.js +++ b/server/notification.js @@ -1,602 +1,75 @@ -const axios = require("axios"); const { R } = require("redbean-node"); -const FormData = require("form-data"); -const nodemailer = require("nodemailer"); -const child_process = require("child_process"); - -const { UP, DOWN } = require("../src/util"); +const Apprise = require("./notification-providers/apprise"); +const Discord = require("./notification-providers/discord"); +const Gotify = require("./notification-providers/gotify"); +const Line = require("./notification-providers/line"); +const LunaSea = require("./notification-providers/lunasea"); +const Mattermost = require("./notification-providers/mattermost"); +const Octopush = require("./notification-providers/octopush"); +const Pushbullet = require("./notification-providers/pushbullet"); +const Pushover = require("./notification-providers/pushover"); +const Pushy = require("./notification-providers/pushy"); +const RocketChat = require("./notification-providers/rocket-chat"); +const Signal = require("./notification-providers/signal"); +const Slack = require("./notification-providers/slack"); +const SMTP = require("./notification-providers/smtp"); +const Telegram = require("./notification-providers/telegram"); +const Webhook = require("./notification-providers/webhook"); class Notification { + providerList = {}; + + static init() { + console.log("Prepare Notification Providers"); + + this.providerList = {}; + + const list = [ + new Apprise(), + new Discord(), + new Gotify(), + new Line(), + new LunaSea(), + new Mattermost(), + new Octopush(), + new Pushbullet(), + new Pushover(), + new Pushy(), + new RocketChat(), + new Signal(), + new Slack(), + new SMTP(), + new Telegram(), + new Webhook(), + ]; + + for (let item of list) { + if (! item.name) { + throw new Error("Notification provider without name"); + } + + if (this.providerList[item.name]) { + throw new Error("Duplicate notification provider name"); + } + this.providerList[item.name] = item; + } + } + /** * - * @param notification - * @param msg - * @param monitorJSON - * @param heartbeatJSON + * @param notification : BeanModel + * @param msg : string General Message + * @param monitorJSON : object Monitor details (For Up/Down only) + * @param heartbeatJSON : object Heartbeat details (For Up/Down only) * @returns {Promise} Successful msg * Throw Error with fail msg */ static async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { - let okMsg = "Sent Successfully. "; - - if (notification.type === "telegram") { - try { - await axios.get(`https://api.telegram.org/bot${notification.telegramBotToken}/sendMessage`, { - params: { - chat_id: notification.telegramChatID, - text: msg, - }, - }) - return okMsg; - - } catch (error) { - let msg = (error.response.data.description) ? error.response.data.description : "Error without description" - throw new Error(msg) - } - - } else if (notification.type === "gotify") { - 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) { - throwGeneralAxiosError(error) - } - - } else if (notification.type === "webhook") { - try { - let data = { - heartbeat: heartbeatJSON, - monitor: monitorJSON, - msg, - }; - let finalData; - let config = {}; - - if (notification.webhookContentType === "form-data") { - finalData = new FormData(); - finalData.append("data", JSON.stringify(data)); - - config = { - headers: finalData.getHeaders(), - } - - } else { - finalData = data; - } - - await axios.post(notification.webhookURL, finalData, config) - return okMsg; - - } catch (error) { - throwGeneralAxiosError(error) - } - - } else if (notification.type === "smtp") { - return await Notification.smtp(notification, msg, heartbeatJSON) - - } else if (notification.type === "discord") { - try { - const discordDisplayName = notification.discordUsername || "Uptime Kuma"; - - // If heartbeatJSON is null, assume we're testing. - if (heartbeatJSON == null) { - let discordtestdata = { - username: discordDisplayName, - content: msg, - } - await axios.post(notification.discordWebhookUrl, discordtestdata) - return okMsg; - } - - let url; - - if (monitorJSON["type"] === "port") { - url = monitorJSON["hostname"]; - if (monitorJSON["port"]) { - url += ":" + monitorJSON["port"]; - } - - } else { - url = monitorJSON["url"]; - } - - // 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: "Service URL", - value: url, - }, - { - name: "Time (UTC)", - value: heartbeatJSON["time"], - }, - { - name: "Error", - value: heartbeatJSON["msg"], - }, - ], - }], - } - await axios.post(notification.discordWebhookUrl, 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: "Service URL", - value: url.startsWith("http") ? "[Visit Service](" + url + ")" : url, - }, - { - name: "Time (UTC)", - value: heartbeatJSON["time"], - }, - { - name: "Ping", - value: heartbeatJSON["ping"] + "ms", - }, - ], - }], - } - await axios.post(notification.discordWebhookUrl, discordupdata) - return okMsg; - } - } catch (error) { - throwGeneralAxiosError(error) - } - - } else if (notification.type === "signal") { - 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) { - throwGeneralAxiosError(error) - } - - } else if (notification.type === "pushy") { - 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 true; - } catch (error) { - console.log(error) - return false; - } - } else if (notification.type === "octopush") { - try { - 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("https://api.octopush.com/v1/public/sms-campaign/send", data, config) - return true; - } catch (error) { - console.log(error) - return false; - } - } else if (notification.type === "slack") { - try { - if (heartbeatJSON == null) { - let data = { - "text": "Uptime Kuma Slack testing successful.", - "channel": notification.slackchannel, - "username": notification.slackusername, - "icon_emoji": notification.slackiconemo, - } - await axios.post(notification.slackwebhookURL, data) - return okMsg; - } - - const time = heartbeatJSON["time"]; - let data = { - "text": "Uptime Kuma Alert", - "channel": notification.slackchannel, - "username": notification.slackusername, - "icon_emoji": notification.slackiconemo, - "blocks": [{ - "type": "header", - "text": { - "type": "plain_text", - "text": "Uptime Kuma Alert", - }, - }, - { - "type": "section", - "fields": [{ - "type": "mrkdwn", - "text": "*Message*\n" + msg, - }, - { - "type": "mrkdwn", - "text": "*Time (UTC)*\n" + time, - }], - }, - { - "type": "actions", - "elements": [ - { - "type": "button", - "text": { - "type": "plain_text", - "text": "Visit Uptime Kuma", - }, - "value": "Uptime-Kuma", - "url": notification.slackbutton || "https://github.com/louislam/uptime-kuma", - }, - ], - }], - } - await axios.post(notification.slackwebhookURL, data) - return okMsg; - } catch (error) { - throwGeneralAxiosError(error) - } - - } else if (notification.type === "rocket.chat") { - try { - if (heartbeatJSON == null) { - let data = { - "text": "Uptime Kuma Rocket.chat testing successful.", - "channel": notification.rocketchannel, - "username": notification.rocketusername, - "icon_emoji": notification.rocketiconemo, - } - await axios.post(notification.rocketwebhookURL, data) - return okMsg; - } - - const time = heartbeatJSON["time"]; - let data = { - "text": "Uptime Kuma Alert", - "channel": notification.rocketchannel, - "username": notification.rocketusername, - "icon_emoji": notification.rocketiconemo, - "attachments": [ - { - "title": "Uptime Kuma Alert *Time (UTC)*\n" + time, - "title_link": notification.rocketbutton, - "text": "*Message*\n" + msg, - "color": "#32cd32" - } - ] - } - await axios.post(notification.rocketwebhookURL, data) - return okMsg; - } catch (error) { - throwGeneralAxiosError(error) - } - - } else if (notification.type === "mattermost") { - try { - const mattermostUserName = notification.mattermostusername || "Uptime Kuma"; - // If heartbeatJSON is null, assume we're testing. - if (heartbeatJSON == null) { - let mattermostTestData = { - username: mattermostUserName, - text: msg, - } - await axios.post(notification.mattermostWebhookUrl, mattermostTestData) - return okMsg; - } - - const mattermostChannel = notification.mattermostchannel; - const mattermostIconEmoji = notification.mattermosticonemo; - const mattermostIconUrl = notification.mattermosticonurl; - - if (heartbeatJSON["status"] == DOWN) { - let mattermostdowndata = { - username: mattermostUserName, - text: "Uptime Kuma Alert", - channel: mattermostChannel, - icon_emoji: mattermostIconEmoji, - icon_url: mattermostIconUrl, - attachments: [ - { - fallback: - "Your " + - monitorJSON["name"] + - " service went down.", - color: "#FF0000", - title: - "❌ " + - monitorJSON["name"] + - " service went down. ❌", - title_link: monitorJSON["url"], - fields: [ - { - short: true, - title: "Service Name", - value: monitorJSON["name"], - }, - { - short: true, - title: "Time (UTC)", - value: heartbeatJSON["time"], - }, - { - short: false, - title: "Error", - value: heartbeatJSON["msg"], - }, - ], - }, - ], - }; - await axios.post( - notification.mattermostWebhookUrl, - mattermostdowndata - ); - return okMsg; - } else if (heartbeatJSON["status"] == UP) { - let mattermostupdata = { - username: mattermostUserName, - text: "Uptime Kuma Alert", - channel: mattermostChannel, - icon_emoji: mattermostIconEmoji, - icon_url: mattermostIconUrl, - attachments: [ - { - fallback: - "Your " + - monitorJSON["name"] + - " service went up!", - color: "#32CD32", - title: - "✅ " + - monitorJSON["name"] + - " service went up! ✅", - title_link: monitorJSON["url"], - fields: [ - { - short: true, - title: "Service Name", - value: monitorJSON["name"], - }, - { - short: true, - title: "Time (UTC)", - value: heartbeatJSON["time"], - }, - { - short: false, - title: "Ping", - value: heartbeatJSON["ping"] + "ms", - }, - ], - }, - ], - }; - await axios.post( - notification.mattermostWebhookUrl, - mattermostupdata - ); - return okMsg; - } - } catch (error) { - throwGeneralAxiosError(error); - } - - } else if (notification.type === "pushover") { - let pushoverlink = "https://api.pushover.net/1/messages.json" - try { - if (heartbeatJSON == null) { - let data = { - "message": "Uptime Kuma Pushover testing successful.", - "user": notification.pushoveruserkey, - "token": notification.pushoverapptoken, - "sound": notification.pushoversounds, - "priority": notification.pushoverpriority, - "title": notification.pushovertitle, - "retry": "30", - "expire": "3600", - "html": 1, - } - await axios.post(pushoverlink, data) - return okMsg; - } - - let data = { - "message": "Uptime Kuma Alert\n\nMessage:" + msg + "\nTime (UTC):" + heartbeatJSON["time"], - "user": notification.pushoveruserkey, - "token": notification.pushoverapptoken, - "sound": notification.pushoversounds, - "priority": notification.pushoverpriority, - "title": notification.pushovertitle, - "retry": "30", - "expire": "3600", - "html": 1, - } - await axios.post(pushoverlink, data) - return okMsg; - } catch (error) { - throwGeneralAxiosError(error) - } - - } else if (notification.type === "apprise") { - - return Notification.apprise(notification, msg) - - } else if (notification.type === "lunasea") { - let lunaseadevice = "https://notify.lunasea.app/v1/custom/device/" + notification.lunaseaDevice - - try { - if (heartbeatJSON == null) { - let testdata = { - "title": "Uptime Kuma Alert", - "body": "Testing Successful.", - } - await axios.post(lunaseadevice, testdata) - return okMsg; - } - - if (heartbeatJSON["status"] == DOWN) { - let downdata = { - "title": "UptimeKuma Alert: " + monitorJSON["name"], - "body": "[🔴 Down] " + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"], - } - await axios.post(lunaseadevice, downdata) - return okMsg; - } - - if (heartbeatJSON["status"] == UP) { - let updata = { - "title": "UptimeKuma Alert: " + monitorJSON["name"], - "body": "[✅ Up] " + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"], - } - await axios.post(lunaseadevice, updata) - return okMsg; - } - - } catch (error) { - throwGeneralAxiosError(error) - } - - } else if (notification.type === "pushbullet") { - try { - let pushbulletUrl = "https://api.pushbullet.com/v2/pushes"; - let config = { - headers: { - "Access-Token": notification.pushbulletAccessToken, - "Content-Type": "application/json" - } - }; - if (heartbeatJSON == null) { - let testdata = { - "type": "note", - "title": "Uptime Kuma Alert", - "body": "Testing Successful.", - } - await axios.post(pushbulletUrl, testdata, config) - } else if (heartbeatJSON["status"] == DOWN) { - let downdata = { - "type": "note", - "title": "UptimeKuma Alert: " + monitorJSON["name"], - "body": "[🔴 Down] " + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"], - } - await axios.post(pushbulletUrl, downdata, config) - } else if (heartbeatJSON["status"] == UP) { - let updata = { - "type": "note", - "title": "UptimeKuma Alert: " + monitorJSON["name"], - "body": "[✅ Up] " + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"], - } - await axios.post(pushbulletUrl, updata, config) - } - return okMsg; - } catch (error) { - throwGeneralAxiosError(error) - } - } else if (notification.type === "line") { - try { - let lineAPIUrl = "https://api.line.me/v2/bot/message/push"; - 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(lineAPIUrl, 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 (UTC): " + heartbeatJSON["time"] - } - ] - } - await axios.post(lineAPIUrl, 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 (UTC): " + heartbeatJSON["time"] - } - ] - } - await axios.post(lineAPIUrl, upMessage, config) - } - return okMsg; - } catch (error) { - throwGeneralAxiosError(error) - } + if (this.providerList[notification.type]) { + return this.providerList[notification.type].send(notification, msg, monitorJSON, heartbeatJSON); } else { - throw new Error("Notification type is not supported") + throw new Error("Notification type is not supported"); } } @@ -636,57 +109,6 @@ class Notification { await R.trash(bean) } - static async smtp(notification, msg, heartbeatJSON = null) { - - const config = { - host: notification.smtpHost, - port: notification.smtpPort, - secure: notification.smtpSecure, - }; - - // 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, - }; - } - - let transporter = nodemailer.createTransport(config); - - let bodyTextContent = msg; - if(heartbeatJSON) { - bodyTextContent = `${msg}\nTime (UTC): ${heartbeatJSON["time"]}`; - } - - // send mail with defined transport object - await transporter.sendMail({ - from: `"Uptime Kuma" <${notification.smtpFrom}>`, - to: notification.smtpTo, - subject: msg, - text: bodyTextContent, - }); - - return "Sent Successfully."; - } - - static async apprise(notification, msg) { - let s = child_process.spawnSync("apprise", [ "-vv", "-b", msg, notification.appriseURL]) - - let output = (s.stdout) ? s.stdout.toString() : "ERROR: maybe apprise not found"; - - if (output) { - - if (! output.includes("ERROR")) { - return "Sent Successfully"; - } - - throw new Error(output) - } else { - return "" - } - } - static checkApprise() { let commandExistsSync = require("command-exists").sync; let exists = commandExistsSync("apprise"); @@ -695,20 +117,6 @@ class Notification { } -function 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 = { Notification, } diff --git a/server/server.js b/server/server.js index 7d929535..29e0857a 100644 --- a/server/server.js +++ b/server/server.js @@ -27,8 +27,11 @@ debug("Importing Monitor"); const Monitor = require("./model/monitor"); debug("Importing Settings"); const { getSettings, setSettings, setting, initJWTSecret } = require("./util-server"); + debug("Importing Notification"); const { Notification } = require("./notification"); +Notification.init(); + debug("Importing Database"); const Database = require("./database");