From e9935d7b3b09d4093059918a0ca492aafb980fde Mon Sep 17 00:00:00 2001
From: Moqavem <sina.farahabadi@gmail.com>
Date: Tue, 31 Dec 2024 18:29:48 +0330
Subject: [PATCH 1/2] Add Bale notification provider (#5384)

---
 server/notification-providers/bale.js |  34 +++++++
 server/notification.js                |  40 +++++---
 src/components/NotificationDialog.vue |   1 +
 src/components/notifications/Bale.vue |  93 +++++++++++++++++
 src/components/notifications/index.js | 138 +++++++++++++-------------
 src/lang/en.json                      |   3 +
 6 files changed, 226 insertions(+), 83 deletions(-)
 create mode 100644 server/notification-providers/bale.js
 create mode 100644 src/components/notifications/Bale.vue

diff --git a/server/notification-providers/bale.js b/server/notification-providers/bale.js
new file mode 100644
index 000000000..4b06ab27b
--- /dev/null
+++ b/server/notification-providers/bale.js
@@ -0,0 +1,34 @@
+const NotificationProvider = require("./notification-provider");
+const axios = require("axios");
+
+class Bale extends NotificationProvider {
+    name = "bale";
+
+    /**
+     * @inheritdoc
+     */
+    async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
+        const okMsg = "Sent Successfully.";
+        const url = "https://tapi.bale.ai";
+
+        try {
+            await axios.post(
+                `${url}/bot${notification.baleBotToken}/sendMessage`,
+                {
+                    chat_id: notification.baleChatID,
+                    text: msg
+                },
+                {
+                    headers: {
+                        "content-type": "application/json",
+                    },
+                }
+            );
+            return okMsg;
+        } catch (error) {
+            this.throwGeneralAxiosError(error);
+        }
+    }
+}
+
+module.exports = Bale;
diff --git a/server/notification.js b/server/notification.js
index e7977eb4a..7a1fd4ab6 100644
--- a/server/notification.js
+++ b/server/notification.js
@@ -4,6 +4,7 @@ const Alerta = require("./notification-providers/alerta");
 const AlertNow = require("./notification-providers/alertnow");
 const AliyunSms = require("./notification-providers/aliyun-sms");
 const Apprise = require("./notification-providers/apprise");
+const Bale = require("./notification-providers/bale");
 const Bark = require("./notification-providers/bark");
 const Bitrix24 = require("./notification-providers/bitrix24");
 const ClickSendSMS = require("./notification-providers/clicksendsms");
@@ -71,7 +72,6 @@ const Wpush = require("./notification-providers/wpush");
 const SendGrid = require("./notification-providers/send-grid");
 
 class Notification {
-
     providerList = {};
 
     /**
@@ -90,6 +90,7 @@ class Notification {
             new AlertNow(),
             new AliyunSms(),
             new Apprise(),
+            new Bale(),
             new Bark(),
             new Bitrix24(),
             new ClickSendSMS(),
@@ -154,10 +155,10 @@ class Notification {
             new GtxMessaging(),
             new Cellsynt(),
             new Wpush(),
-            new SendGrid()
+            new SendGrid(),
         ];
         for (let item of list) {
-            if (! item.name) {
+            if (!item.name) {
                 throw new Error("Notification provider without name");
             }
 
@@ -177,9 +178,19 @@ class Notification {
      * @returns {Promise<string>} Successful msg
      * @throws Error with fail msg
      */
-    static async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
+    static async send(
+        notification,
+        msg,
+        monitorJSON = null,
+        heartbeatJSON = null
+    ) {
         if (this.providerList[notification.type]) {
-            return this.providerList[notification.type].send(notification, msg, monitorJSON, heartbeatJSON);
+            return this.providerList[notification.type].send(
+                notification,
+                msg,
+                monitorJSON,
+                heartbeatJSON
+            );
         } else {
             throw new Error("Notification type is not supported");
         }
@@ -201,10 +212,9 @@ class Notification {
                 userID,
             ]);
 
-            if (! bean) {
+            if (!bean) {
                 throw new Error("notification not found");
             }
-
         } else {
             bean = R.dispense("notification");
         }
@@ -234,7 +244,7 @@ class Notification {
             userID,
         ]);
 
-        if (! bean) {
+        if (!bean) {
             throw new Error("notification not found");
         }
 
@@ -250,7 +260,6 @@ class Notification {
         let exists = commandExistsSync("apprise");
         return exists;
     }
-
 }
 
 /**
@@ -261,16 +270,17 @@ class Notification {
  */
 async function applyNotificationEveryMonitor(notificationID, userID) {
     let monitors = await R.getAll("SELECT id FROM monitor WHERE user_id = ?", [
-        userID
+        userID,
     ]);
 
     for (let i = 0; i < monitors.length; i++) {
-        let checkNotification = await R.findOne("monitor_notification", " monitor_id = ? AND notification_id = ? ", [
-            monitors[i].id,
-            notificationID,
-        ]);
+        let checkNotification = await R.findOne(
+            "monitor_notification",
+            " monitor_id = ? AND notification_id = ? ",
+            [ monitors[i].id, notificationID ]
+        );
 
-        if (! checkNotification) {
+        if (!checkNotification) {
             let relation = R.dispense("monitor_notification");
             relation.monitor_id = monitors[i].id;
             relation.notification_id = notificationID;
diff --git a/src/components/NotificationDialog.vue b/src/components/NotificationDialog.vue
index f6d728029..307b09910 100644
--- a/src/components/NotificationDialog.vue
+++ b/src/components/NotificationDialog.vue
@@ -113,6 +113,7 @@ export default {
                 "alerta": "Alerta",
                 "AlertNow": "AlertNow",
                 "apprise": this.$t("apprise"),
+                "bale": "Bale",
                 "Bark": "Bark",
                 "Bitrix24": "Bitrix24",
                 "clicksendsms": "ClickSend SMS",
diff --git a/src/components/notifications/Bale.vue b/src/components/notifications/Bale.vue
new file mode 100644
index 000000000..ea69e21b1
--- /dev/null
+++ b/src/components/notifications/Bale.vue
@@ -0,0 +1,93 @@
+<template>
+    <div class="mb-3">
+        <label for="bale-bot-token" class="form-label">{{ $t("Bot Token") }}</label>
+        <HiddenInput id="bale-bot-token" v-model="$parent.notification.baleBotToken" :required="true" autocomplete="new-password"></HiddenInput>
+        <i18n-t tag="div" keypath="wayToGetBaleToken" class="form-text">
+            <a href="https://ble.ir/BotFather" target="_blank">https://ble.ir/BotFather</a>
+        </i18n-t>
+    </div>
+
+    <div class="mb-3">
+        <label for="bale-chat-id" class="form-label">{{ $t("Chat ID") }}</label>
+
+        <div class="input-group mb-3">
+            <input id="bale-chat-id" v-model="$parent.notification.baleChatID" type="text" class="form-control" required>
+            <button v-if="$parent.notification.baleBotToken" class="btn btn-outline-secondary" type="button" @click="autoGetBaleChatID">
+                {{ $t("Auto Get") }}
+            </button>
+        </div>
+
+        <div class="form-text">
+            {{ $t("supportBaleChatID") }}
+
+            <p style="margin-top: 8px;">
+                {{ $t("wayToGetBaleChatID") }}
+            </p>
+
+            <p style="margin-top: 8px;">
+                <a :href="baleGetUpdatesURL('withToken')" target="_blank" style="word-break: break-word;">{{ baleGetUpdatesURL("masked") }}</a>
+            </p>
+        </div>
+    </div>
+</template>
+
+<script>
+import HiddenInput from "../HiddenInput.vue";
+import axios from "axios";
+
+export default {
+    components: {
+        HiddenInput,
+    },
+    methods: {
+        /**
+         * Get the URL for bale updates
+         * @param {string} mode Should the token be masked?
+         * @returns {string} formatted URL
+         */
+        baleGetUpdatesURL(mode = "masked") {
+            let token = `<${this.$t("YOUR BOT TOKEN HERE")}>`;
+
+            if (this.$parent.notification.baleBotToken) {
+                if (mode === "withToken") {
+                    token = this.$parent.notification.baleBotToken;
+                } else if (mode === "masked") {
+                    token = "*".repeat(this.$parent.notification.baleBotToken.length);
+                }
+            }
+
+            return `https://tapi.bale.ai/bot${token}/getUpdates`;
+        },
+
+        /**
+         * Get the bale chat ID
+         * @returns {Promise<void>}
+         * @throws The chat ID could not be found
+         */
+        async autoGetBaleChatID() {
+            try {
+                let res = await axios.get(this.baleGetUpdatesURL("withToken"));
+
+                if (res.data.result.length >= 1) {
+                    let update = res.data.result[res.data.result.length - 1];
+
+                    if (update.channel_post) {
+                        this.$parent.notification.baleChatID = update.channel_post.chat.id;
+                    } else if (update.message) {
+                        this.$parent.notification.baleChatID = update.message.chat.id;
+                    } else {
+                        throw new Error(this.$t("chatIDNotFound"));
+                    }
+
+                } else {
+                    throw new Error(this.$t("chatIDNotFound"));
+                }
+
+            } catch (error) {
+                this.$root.toastError(error.message);
+            }
+
+        },
+    }
+};
+</script>
diff --git a/src/components/notifications/index.js b/src/components/notifications/index.js
index efa2af5c4..d2e2364eb 100644
--- a/src/components/notifications/index.js
+++ b/src/components/notifications/index.js
@@ -2,6 +2,7 @@ import Alerta from "./Alerta.vue";
 import AlertNow from "./AlertNow.vue";
 import AliyunSMS from "./AliyunSms.vue";
 import Apprise from "./Apprise.vue";
+import Bale from "./Bale.vue";
 import Bark from "./Bark.vue";
 import Bitrix24 from "./Bitrix24.vue";
 import ClickSendSMS from "./ClickSendSMS.vue";
@@ -73,75 +74,76 @@ import SendGrid from "./SendGrid.vue";
  * @type { Record<string, any> }
  */
 const NotificationFormList = {
-    "alerta": Alerta,
-    "AlertNow": AlertNow,
-    "AliyunSMS": AliyunSMS,
-    "apprise": Apprise,
-    "Bark": Bark,
-    "Bitrix24": Bitrix24,
-    "clicksendsms": ClickSendSMS,
-    "CallMeBot": CallMeBot,
-    "smsc": SMSC,
-    "DingDing": DingDing,
-    "discord": Discord,
-    "Elks": Elks,
-    "Feishu": Feishu,
-    "FreeMobile": FreeMobile,
-    "GoogleChat": GoogleChat,
-    "gorush": Gorush,
-    "gotify": Gotify,
-    "GrafanaOncall": GrafanaOncall,
-    "HomeAssistant": HomeAssistant,
-    "HeiiOnCall": HeiiOnCall,
-    "Keep": Keep,
-    "Kook": Kook,
-    "line": Line,
-    "LineNotify": LineNotify,
-    "lunasea": LunaSea,
-    "matrix": Matrix,
-    "mattermost": Mattermost,
-    "nostr": Nostr,
-    "ntfy": Ntfy,
-    "octopush": Octopush,
-    "OneBot": OneBot,
-    "Onesender": Onesender,
-    "Opsgenie": Opsgenie,
-    "PagerDuty": PagerDuty,
-    "FlashDuty": FlashDuty,
-    "PagerTree": PagerTree,
-    "promosms": PromoSMS,
-    "pushbullet": Pushbullet,
-    "PushByTechulus": TechulusPush,
-    "PushDeer": PushDeer,
-    "pushover": Pushover,
-    "pushy": Pushy,
+    alerta: Alerta,
+    AlertNow: AlertNow,
+    AliyunSMS: AliyunSMS,
+    apprise: Apprise,
+    bale: Bale,
+    Bark: Bark,
+    Bitrix24: Bitrix24,
+    clicksendsms: ClickSendSMS,
+    CallMeBot: CallMeBot,
+    smsc: SMSC,
+    DingDing: DingDing,
+    discord: Discord,
+    Elks: Elks,
+    Feishu: Feishu,
+    FreeMobile: FreeMobile,
+    GoogleChat: GoogleChat,
+    gorush: Gorush,
+    gotify: Gotify,
+    GrafanaOncall: GrafanaOncall,
+    HomeAssistant: HomeAssistant,
+    HeiiOnCall: HeiiOnCall,
+    Keep: Keep,
+    Kook: Kook,
+    line: Line,
+    LineNotify: LineNotify,
+    lunasea: LunaSea,
+    matrix: Matrix,
+    mattermost: Mattermost,
+    nostr: Nostr,
+    ntfy: Ntfy,
+    octopush: Octopush,
+    OneBot: OneBot,
+    Onesender: Onesender,
+    Opsgenie: Opsgenie,
+    PagerDuty: PagerDuty,
+    FlashDuty: FlashDuty,
+    PagerTree: PagerTree,
+    promosms: PromoSMS,
+    pushbullet: Pushbullet,
+    PushByTechulus: TechulusPush,
+    PushDeer: PushDeer,
+    pushover: Pushover,
+    pushy: Pushy,
     "rocket.chat": RocketChat,
-    "serwersms": SerwerSMS,
-    "signal": Signal,
-    "SIGNL4": SIGNL4,
-    "SMSManager": SMSManager,
-    "SMSPartner": SMSPartner,
-    "slack": Slack,
-    "squadcast": Squadcast,
-    "SMSEagle": SMSEagle,
-    "smtp": STMP,
-    "stackfield": Stackfield,
-    "teams": Teams,
-    "telegram": Telegram,
-    "threema": Threema,
-    "twilio": Twilio,
-    "Splunk": Splunk,
-    "webhook": Webhook,
-    "WeCom": WeCom,
-    "GoAlert": GoAlert,
-    "ServerChan": ServerChan,
-    "ZohoCliq": ZohoCliq,
-    "SevenIO": SevenIO,
-    "whapi": Whapi,
-    "gtxmessaging": GtxMessaging,
-    "Cellsynt": Cellsynt,
-    "WPush": WPush,
-    "SendGrid": SendGrid,
+    serwersms: SerwerSMS,
+    signal: Signal,
+    SIGNL4: SIGNL4,
+    SMSManager: SMSManager,
+    SMSPartner: SMSPartner,
+    slack: Slack,
+    squadcast: Squadcast,
+    SMSEagle: SMSEagle,
+    smtp: STMP,
+    stackfield: Stackfield,
+    teams: Teams,
+    telegram: Telegram,
+    threema: Threema,
+    twilio: Twilio,
+    Splunk: Splunk,
+    webhook: Webhook,
+    WeCom: WeCom,
+    GoAlert: GoAlert,
+    ServerChan: ServerChan,
+    ZohoCliq: ZohoCliq,
+    SevenIO: SevenIO,
+    whapi: Whapi,
+    gtxmessaging: GtxMessaging,
+    Cellsynt: Cellsynt,
+    WPush: WPush,
+    SendGrid: SendGrid,
 };
 
 export default NotificationFormList;
diff --git a/src/lang/en.json b/src/lang/en.json
index e215f1031..b19a187ee 100644
--- a/src/lang/en.json
+++ b/src/lang/en.json
@@ -429,6 +429,9 @@
     "trustProxyDescription": "Trust 'X-Forwarded-*' headers. If you want to get the correct client IP and your Uptime Kuma is behind a proxy such as Nginx or Apache, you should enable this.",
     "wayToGetLineNotifyToken": "You can get an access token from {0}",
     "Examples": "Examples",
+    "supportBaleChatID": "Support Direct Chat / Group / Channel's Chat ID",
+    "wayToGetBaleChatID": "You can get your chat ID by sending a message to the bot and going to this URL to view the chat_id:",
+    "wayToGetBaleToken": "You can get a token from {0}.",
     "Home Assistant URL": "Home Assistant URL",
     "Long-Lived Access Token": "Long-Lived Access Token",
     "Long-Lived Access Token can be created by clicking on your profile name (bottom left) and scrolling to the bottom then click Create Token. ": "Long-Lived Access Token can be created by clicking on your profile name (bottom left) and scrolling to the bottom then click Create Token. ",

From 9579df30f2415042758644f89dcb31f0c4721107 Mon Sep 17 00:00:00 2001
From: xx <129470203+boomboomxx@users.noreply.github.com>
Date: Tue, 4 Mar 2025 00:35:57 +0800
Subject: [PATCH 2/2] feat: allow users to @People in DingTalk notifications
 (#5464)

Co-authored-by: xx <xx@123.com>
Co-authored-by: Frank Elsinga <frank@elsinga.de>
---
 server/notification-providers/dingding.js |  21 ++++-
 src/components/notifications/DingDing.vue | 110 +++++++++++++++++++++-
 src/lang/en.json                          |   8 ++
 3 files changed, 132 insertions(+), 7 deletions(-)

diff --git a/server/notification-providers/dingding.js b/server/notification-providers/dingding.js
index c66f270a7..cfacd45ed 100644
--- a/server/notification-providers/dingding.js
+++ b/server/notification-providers/dingding.js
@@ -11,17 +11,23 @@ class DingDing extends NotificationProvider {
      */
     async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
         const okMsg = "Sent Successfully.";
-
+        const mentionAll = notification.mentioning === "everyone";
+        const mobileList = notification.mentioning === "specify-mobiles" ? notification.mobileList : [];
+        const userList = notification.mentioning === "specify-users" ? notification.userList : [];
+        const finalList = [ ...mobileList || [], ...userList || [] ];
+        const mentionStr = finalList.length > 0 ? "\n" : "" + finalList.map(item => `@${item}`).join(" ");
         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"]}`,
+                        text: `## [${this.statusToString(heartbeatJSON["status"])}] ${monitorJSON["name"]} \n> ${heartbeatJSON["msg"]}\n> Time (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}${mentionStr}`,
                     },
-                    "at": {
-                        "isAtAll": notification.mentioning === "everyone"
+                    at: {
+                        isAtAll: mentionAll,
+                        atUserIds: userList,
+                        atMobiles: mobileList
                     }
                 };
                 if (await this.sendToDingDing(notification, params)) {
@@ -31,7 +37,12 @@ class DingDing extends NotificationProvider {
                 let params = {
                     msgtype: "text",
                     text: {
-                        content: msg
+                        content: `${msg}${mentionStr}`
+                    },
+                    at: {
+                        isAtAll: mentionAll,
+                        atUserIds: userList,
+                        atMobiles: mobileList
                     }
                 };
                 if (await this.sendToDingDing(notification, params)) {
diff --git a/src/components/notifications/DingDing.vue b/src/components/notifications/DingDing.vue
index 710677fd7..a5fd3c82b 100644
--- a/src/components/notifications/DingDing.vue
+++ b/src/components/notifications/DingDing.vue
@@ -16,22 +16,128 @@
     </div>
     <div class="mb-3">
         <label for="mentioning" class="form-label">{{ $t("Mentioning") }}<span style="color: red;"><sup>*</sup></span></label>
-        <select id="mentioning" v-model="$parent.notification.mentioning" class="form-select" required>
+        <select id="mentioning" v-model="$parent.notification.mentioning" class="form-select" required @change="onMentioningChange">
             <option value="nobody">{{ $t("Don't mention people") }}</option>
             <option value="everyone">{{ $t("Mention group", { group: "@everyone" }) }}</option>
+            <option value="specify-mobiles">{{ $t("Mention Mobile List") }}</option>
+            <option value="specify-users">{{ $t("Mention User List") }}</option>
         </select>
     </div>
+    <div v-if="$parent.notification.mentioning === 'specify-mobiles'" class="mb-3">
+        <label for="mobileList" class="form-label">{{ $t("Dingtalk Mobile List") }}<span style="color: red;"><sup>*</sup></span></label>
+        <VueMultiselect
+            id="mobileList-select"
+            v-model="$parent.notification.mobileList"
+            :required="$parent.notification.mentioning === 'specify-mobiles'"
+            :placeholder="$t('Enter a list of mobile')"
+            :multiple="true"
+            :options="mobileOpts"
+            :max-height="500"
+            :taggable="true"
+            :show-no-options="false"
+            :close-on-select="false"
+            :clear-on-select="false"
+            :preserve-search="false"
+            :preselect-first="false"
+            @remove="removeMobile"
+            @tag="addMobile"
+        ></VueMultiselect>
+    </div>
+    <div v-if="$parent.notification.mentioning === 'specify-users'" class="mb-3">
+        <label for="userList" class="form-label">{{ $t("Dingtalk User List") }}<span style="color: red;"><sup>*</sup></span></label>
+        <VueMultiselect
+            id="userList-select"
+            v-model="$parent.notification.userList"
+            :required="$parent.notification.mentioning === 'specify-users'"
+            :placeholder="$t('Enter a list of userId')"
+            :multiple="true"
+            :options="userIdOpts"
+            :max-height="500"
+            :taggable="true"
+            :show-no-options="false"
+            :close-on-select="false"
+            :clear-on-select="true"
+            :preserve-search="false"
+            :preselect-first="false"
+            @remove="removeUser"
+            @tag="addUser"
+        ></VueMultiselect>
+    </div>
 </template>
 
 <script lang="ts">
 import HiddenInput from "../HiddenInput.vue";
+import VueMultiselect from "vue-multiselect";
 
 export default {
-    components: { HiddenInput },
+    components: {
+        HiddenInput,
+        VueMultiselect
+    },
+    data() {
+        return {
+            mobileOpts: [],
+            userIdOpts: [],
+        };
+    },
+
     mounted() {
         if (typeof this.$parent.notification.mentioning === "undefined") {
             this.$parent.notification.mentioning = "nobody";
         }
+        if (typeof this.$parent.notification.mobileList === "undefined") {
+            this.$parent.notification.mobileList = [];
+        } else {
+            this.mobileOpts = this.$parent.notification.mobileList;
+        }
+
+        if (typeof this.$parent.notification.userList === "undefined") {
+            this.$parent.notification.userList = [];
+        } else {
+            this.userIdOpts = this.$parent.notification.userList;
+        }
+    },
+    methods: {
+        onMentioningChange(e) {
+            if (e.target.value === "specify-mobiles") {
+                this.$parent.notification.userList = [];
+            } else if (e.target.value === "specify-users") {
+                this.$parent.notification.mobileList = [];
+            } else {
+                this.$parent.notification.userList = [];
+                this.$parent.notification.mobileList = [];
+            }
+        },
+        addMobile(mobile) {
+            const trimmedMobile = mobile.trim();
+            const chinaMobileRegex = /^1[3-9]\d{9}$/;
+            if (!chinaMobileRegex.test(trimmedMobile)) {
+                this.$root.toastError(this.$t("Invalid mobile", { "mobile": trimmedMobile }));
+                return;
+            }
+            this.mobileOpts.push(mobile);
+        },
+        removeMobile(mobile) {
+            const idx = this.mobileOpts.indexOf(mobile);
+            if (idx > -1) {
+                this.mobileOpts.splice(idx, 1);
+            }
+        },
+        addUser(userId) {
+            const trimmedUserId = userId.trim();
+            const userIdRegex = /^[a-zA-Z0-9]+$/;
+            if (!userIdRegex.test(trimmedUserId)) {
+                this.$root.toastError(this.$t("Invalid userId", { "userId": trimmedUserId }));
+                return;
+            }
+            this.userIdOpts.push(trimmedUserId);
+        },
+        removeUser(userId) {
+            const idx = this.userIdOpts.indexOf(userId);
+            if (idx > -1) {
+                this.userIdOpts.splice(idx, 1);
+            }
+        },
     }
 };
 </script>
diff --git a/src/lang/en.json b/src/lang/en.json
index b19a187ee..112979bd6 100644
--- a/src/lang/en.json
+++ b/src/lang/en.json
@@ -690,6 +690,14 @@
     "Mentioning": "Mentioning",
     "Don't mention people": "Don't mention people",
     "Mention group": "Mention {group}",
+    "Mention Mobile List": "Mention mobile list",
+    "Mention User List": "Mention user id list",
+    "Dingtalk Mobile List": "Mobile list",
+    "Dingtalk User List": "User ID list",
+    "Enter a list of userId": "Enter a list of userId",
+    "Enter a list of mobile": "Enter a list of mobile",
+    "Invalid mobile": "Invalid mobile [{mobile}]",
+    "Invalid userId": "Invalid userId [{userId}]",
     "Device Token": "Device Token",
     "Platform": "Platform",
     "Huawei": "Huawei",