From 30f82b9cb4f4f870f5be02ba27a2ff8a57151c1f Mon Sep 17 00:00:00 2001
From: Teror Fox <ImagineHTR@163.com>
Date: Thu, 27 Mar 2025 00:31:34 +0800
Subject: [PATCH] feat: add notification provider `PushPlus` (#5716)

Co-authored-by: Teror Fox <i@trfox.top>
Co-authored-by: Frank Elsinga <frank@elsinga.de>
---
 server/notification-providers/pushplus.js | 56 +++++++++++++++++++++++
 server/notification.js                    |  2 +
 src/components/NotificationDialog.vue     |  1 +
 src/components/notifications/PushPlus.vue | 19 ++++++++
 src/components/notifications/index.js     |  2 +
 5 files changed, 80 insertions(+)
 create mode 100644 server/notification-providers/pushplus.js
 create mode 100644 src/components/notifications/PushPlus.vue

diff --git a/server/notification-providers/pushplus.js b/server/notification-providers/pushplus.js
new file mode 100644
index 000000000..a26ba176f
--- /dev/null
+++ b/server/notification-providers/pushplus.js
@@ -0,0 +1,56 @@
+const NotificationProvider = require("./notification-provider");
+const axios = require("axios");
+const { DOWN, UP } = require("../../src/util");
+
+class PushPlus extends NotificationProvider {
+    name = "PushPlus";
+
+    /**
+     * @inheritdoc
+     * @param {BeanModel} notification Notification object
+     * @param {string} msg Message content
+     * @param {?object} monitorJSON Monitor details
+     * @param {?object} heartbeatJSON Heartbeat details
+     * @returns {Promise<string>} Success message
+     */
+    async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
+        const okMsg = "Sent Successfully.";
+        const url = "https://www.pushplus.plus/send";
+        try {
+            const config = {
+                headers: {
+                    "Content-Type": "application/json",
+                },
+            };
+            const params = {
+                "token": notification.pushPlusSendKey,
+                "title": this.checkStatus(heartbeatJSON, monitorJSON),
+                "content": msg,
+                "template": "html"
+            };
+            await axios.post(url, params, config);
+            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 = PushPlus;
diff --git a/server/notification.js b/server/notification.js
index a77094ac9..0c222d932 100644
--- a/server/notification.js
+++ b/server/notification.js
@@ -39,6 +39,7 @@ const PromoSMS = require("./notification-providers/promosms");
 const Pushbullet = require("./notification-providers/pushbullet");
 const PushDeer = require("./notification-providers/pushdeer");
 const Pushover = require("./notification-providers/pushover");
+const PushPlus = require("./notification-providers/pushplus");
 const Pushy = require("./notification-providers/pushy");
 const RocketChat = require("./notification-providers/rocket-chat");
 const SerwerSMS = require("./notification-providers/serwersms");
@@ -128,6 +129,7 @@ class Notification {
             new Pushbullet(),
             new PushDeer(),
             new Pushover(),
+            new PushPlus(),
             new Pushy(),
             new RocketChat(),
             new ServerChan(),
diff --git a/src/components/NotificationDialog.vue b/src/components/NotificationDialog.vue
index 561637ec0..bed841fa5 100644
--- a/src/components/NotificationDialog.vue
+++ b/src/components/NotificationDialog.vue
@@ -182,6 +182,7 @@ export default {
                 "SMSManager": "SmsManager (smsmanager.cz)",
                 "WeCom": "WeCom (企业微信群机器人)",
                 "ServerChan": "ServerChan (Server酱)",
+                "PushPlus": "PushPlus (推送加)",
                 "smsc": "SMSC",
                 "WPush": "WPush(wpush.cn)",
                 "YZJ": "YZJ (云之家自定义机器人)"
diff --git a/src/components/notifications/PushPlus.vue b/src/components/notifications/PushPlus.vue
new file mode 100644
index 000000000..80e47cea0
--- /dev/null
+++ b/src/components/notifications/PushPlus.vue
@@ -0,0 +1,19 @@
+<template>
+    <div class="mb-3">
+        <label for="pushPlus-sendkey" class="form-label">{{ $t("SendKey") }}</label>
+        <HiddenInput id="pushPlus-sendkey" v-model="$parent.notification.pushPlusSendKey" :required="true" autocomplete="new-password"></HiddenInput>
+    </div>
+    <i18n-t tag="div" keypath="More info on:" class="mb-3 form-text">
+        <a href="https://www.pushplus.plus/" target="_blank">https://www.pushplus.plus/</a>
+    </i18n-t>
+</template>
+
+<script>
+import HiddenInput from "../HiddenInput.vue";
+
+export default {
+    components: {
+        HiddenInput,
+    },
+};
+</script>
diff --git a/src/components/notifications/index.js b/src/components/notifications/index.js
index c1162da92..cbd6f2b68 100644
--- a/src/components/notifications/index.js
+++ b/src/components/notifications/index.js
@@ -39,6 +39,7 @@ import PromoSMS from "./PromoSMS.vue";
 import Pushbullet from "./Pushbullet.vue";
 import PushDeer from "./PushDeer.vue";
 import Pushover from "./Pushover.vue";
+import PushPlus from "./PushPlus.vue";
 import Pushy from "./Pushy.vue";
 import RocketChat from "./RocketChat.vue";
 import ServerChan from "./ServerChan.vue";
@@ -116,6 +117,7 @@ const NotificationFormList = {
     "PushByTechulus": TechulusPush,
     "PushDeer": PushDeer,
     "pushover": Pushover,
+    "PushPlus": PushPlus,
     "pushy": Pushy,
     "rocket.chat": RocketChat,
     "serwersms": SerwerSMS,