diff --git a/.github/ISSUE_TEMPLATE/ask-for-help.yaml b/.github/ISSUE_TEMPLATE/ask-for-help.yaml index 3442e8b73..9c30b2dc8 100644 --- a/.github/ISSUE_TEMPLATE/ask-for-help.yaml +++ b/.github/ISSUE_TEMPLATE/ask-for-help.yaml @@ -26,6 +26,12 @@ body: label: "📝 Describe your problem" description: "Please walk us through it step by step." placeholder: "Describe what are you asking for..." + - type: textarea + id: error-msg + validations: + required: false + attributes: + label: "📝 Error Message(s) or Log" - type: input id: uptime-kuma-version attributes: diff --git a/.github/workflows/json-yaml-validate.yml b/.github/workflows/json-yaml-validate.yml new file mode 100644 index 000000000..4e8f29650 --- /dev/null +++ b/.github/workflows/json-yaml-validate.yml @@ -0,0 +1,25 @@ +name: json-yaml-validate +on: + push: + branches: + - master + pull_request: + branches: + - master + workflow_dispatch: + +permissions: + contents: read + pull-requests: write # enable write permissions for pull request comments + +jobs: + json-yaml-validate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: json-yaml-validate + id: json-yaml-validate + uses: GrantBirki/json-yaml-validate@v1.2.0 + with: + comment: "true" # enable comment mode diff --git a/server/model/monitor.js b/server/model/monitor.js index 1ffae58e5..44460819e 100644 --- a/server/model/monitor.js +++ b/server/model/monitor.js @@ -1245,7 +1245,7 @@ class Monitor extends BeanModel { if (notificationList.length > 0) { - let row = await R.getRow("SELECT * FROM notification_sent_history WHERE type = ? AND monitor_id = ? AND days = ?", [ + let row = await R.getRow("SELECT * FROM notification_sent_history WHERE type = ? AND monitor_id = ? AND days <= ?", [ "certificate", this.id, targetDays, diff --git a/server/notification-providers/opsgenie.js b/server/notification-providers/opsgenie.js new file mode 100644 index 000000000..16bf9fc60 --- /dev/null +++ b/server/notification-providers/opsgenie.js @@ -0,0 +1,97 @@ +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"; +let 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); + } + } + + /** + * + * @param {BeanModel} notification + * @param {string} url Request url + * @param {Object} data Request body + * @returns {Promise} + */ + 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/twilio.js b/server/notification-providers/twilio.js new file mode 100644 index 000000000..8f4db0404 --- /dev/null +++ b/server/notification-providers/twilio.js @@ -0,0 +1,41 @@ +const NotificationProvider = require("./notification-provider"); +const axios = require("axios"); + +class Twilio extends NotificationProvider { + + name = "twilio"; + + async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { + + let okMsg = "Sent Successfully."; + + let accountSID = notification.twilioAccountSID; + let authToken = notification.twilioAuthToken; + + try { + + let config = { + headers: { + "Content-Type": "application/x-www-form-urlencoded;charset=utf-8", + "Authorization": "Basic " + Buffer.from(accountSID + ":" + authToken).toString("base64"), + } + }; + + let data = new URLSearchParams(); + data.append("To", notification.twilioToNumber); + data.append("From", notification.twilioFromNumber); + data.append("Body", msg); + + let url = "https://api.twilio.com/2010-04-01/Accounts/" + accountSID + "/Messages.json"; + + await axios.post(url, data, config); + + return okMsg; + } catch (error) { + this.throwGeneralAxiosError(error); + } + } + +} + +module.exports = Twilio; diff --git a/server/notification.js b/server/notification.js index 1897f5cc0..9bfa371d9 100644 --- a/server/notification.js +++ b/server/notification.js @@ -23,6 +23,7 @@ const Mattermost = require("./notification-providers/mattermost"); const Ntfy = require("./notification-providers/ntfy"); const Octopush = require("./notification-providers/octopush"); const OneBot = require("./notification-providers/onebot"); +const Opsgenie = require("./notification-providers/opsgenie"); const PagerDuty = require("./notification-providers/pagerduty"); const PagerTree = require("./notification-providers/pagertree"); const PromoSMS = require("./notification-providers/promosms"); @@ -41,6 +42,7 @@ const Stackfield = require("./notification-providers/stackfield"); const Teams = require("./notification-providers/teams"); const TechulusPush = require("./notification-providers/techulus-push"); const Telegram = require("./notification-providers/telegram"); +const Twilio = require("./notification-providers/twilio"); const Splunk = require("./notification-providers/splunk"); const Webhook = require("./notification-providers/webhook"); const WeCom = require("./notification-providers/wecom"); @@ -83,6 +85,7 @@ class Notification { new Ntfy(), new Octopush(), new OneBot(), + new Opsgenie(), new PagerDuty(), new PagerTree(), new PromoSMS(), @@ -103,6 +106,7 @@ class Notification { new Teams(), new TechulusPush(), new Telegram(), + new Twilio(), new Splunk(), new Webhook(), new WeCom(), diff --git a/server/routers/api-router.js b/server/routers/api-router.js index a36159cae..0b4982db6 100644 --- a/server/routers/api-router.js +++ b/server/routers/api-router.js @@ -147,7 +147,11 @@ router.get("/api/badge/:id/status", cache("5 minutes"), async (request, response const heartbeat = await Monitor.getPreviousHeartbeat(requestedMonitorId); const state = overrideValue !== undefined ? overrideValue : heartbeat.status; - badgeValues.label = label ?? "Status"; + if (label === undefined) { + badgeValues.label = "Status"; + } else { + badgeValues.label = label; + } switch (state) { case DOWN: badgeValues.color = downColor; @@ -224,7 +228,7 @@ router.get("/api/badge/:id/uptime/:duration?", cache("5 minutes"), async (reques ); // limit the displayed uptime percentage to four (two, when displayed as percent) decimal digits - const cleanUptime = parseFloat(uptime.toPrecision(4)); + const cleanUptime = (uptime * 100).toPrecision(4); // use a given, custom color or calculate one based on the uptime value badgeValues.color = color ?? percentageToColor(uptime); @@ -235,7 +239,7 @@ router.get("/api/badge/:id/uptime/:duration?", cache("5 minutes"), async (reques labelPrefix, label ?? `Uptime (${requestedDuration}${labelSuffix})`, ]); - badgeValues.message = filterAndJoin([ prefix, `${cleanUptime * 100}`, suffix ]); + badgeValues.message = filterAndJoin([ prefix, cleanUptime, suffix ]); } // build the SVG based on given values diff --git a/src/components/NotificationDialog.vue b/src/components/NotificationDialog.vue index c3851b568..7454931e6 100644 --- a/src/components/NotificationDialog.vue +++ b/src/components/NotificationDialog.vue @@ -129,6 +129,7 @@ export default { "ntfy": "Ntfy", "octopush": "Octopush", "OneBot": "OneBot", + "Opsgenie": "Opsgenie", "PagerDuty": "PagerDuty", "pushbullet": "Pushbullet", "PushByTechulus": "Push by Techulus", @@ -143,6 +144,7 @@ export default { "stackfield": "Stackfield", "teams": "Microsoft Teams", "telegram": "Telegram", + "twilio": "Twilio", "Splunk": "Splunk", "webhook": "Webhook", "GoAlert": "GoAlert", diff --git a/src/components/notifications/Opsgenie.vue b/src/components/notifications/Opsgenie.vue new file mode 100644 index 000000000..3f07d0528 --- /dev/null +++ b/src/components/notifications/Opsgenie.vue @@ -0,0 +1,36 @@ + + + diff --git a/src/components/notifications/Twilio.vue b/src/components/notifications/Twilio.vue new file mode 100644 index 000000000..3edf1e3df --- /dev/null +++ b/src/components/notifications/Twilio.vue @@ -0,0 +1,27 @@ + diff --git a/src/components/notifications/index.js b/src/components/notifications/index.js index ed9dde0f1..7b5e6b6c7 100644 --- a/src/components/notifications/index.js +++ b/src/components/notifications/index.js @@ -21,6 +21,7 @@ import Mattermost from "./Mattermost.vue"; import Ntfy from "./Ntfy.vue"; import Octopush from "./Octopush.vue"; import OneBot from "./OneBot.vue"; +import Opsgenie from "./Opsgenie.vue"; import PagerDuty from "./PagerDuty.vue"; import PagerTree from "./PagerTree.vue"; import PromoSMS from "./PromoSMS.vue"; @@ -41,6 +42,7 @@ import STMP from "./SMTP.vue"; import Teams from "./Teams.vue"; import TechulusPush from "./TechulusPush.vue"; import Telegram from "./Telegram.vue"; +import Twilio from "./Twilio.vue"; import Webhook from "./Webhook.vue"; import WeCom from "./WeCom.vue"; import GoAlert from "./GoAlert.vue"; @@ -76,6 +78,7 @@ const NotificationFormList = { "ntfy": Ntfy, "octopush": Octopush, "OneBot": OneBot, + "Opsgenie": Opsgenie, "PagerDuty": PagerDuty, "PagerTree": PagerTree, "promosms": PromoSMS, @@ -95,6 +98,7 @@ const NotificationFormList = { "stackfield": Stackfield, "teams": Teams, "telegram": Telegram, + "twilio": Twilio, "Splunk": Splunk, "webhook": Webhook, "WeCom": WeCom, diff --git a/src/lang/en.json b/src/lang/en.json index 0c9fc3840..e7656c474 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -707,5 +707,9 @@ "wayToGetPagerTreeIntegrationURL": "After creating the Uptime Kuma integration in PagerTree, copy the Endpoint. See full details {0}", "lunaseaTarget": "Target", "lunaseaDeviceID": "Device ID", - "lunaseaUserID": "User ID" + "lunaseaUserID": "User ID", + "twilioAccountSID": "Account SID", + "twilioAuthToken": "Auth Token", + "twilioFromNumber": "From Number", + "twilioToNumber": "To Number" }