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",