pull/5487/merge
Louis Lam 2025-03-07 22:55:22 +00:00 committed by GitHub
commit ca3effc8df
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 358 additions and 90 deletions

View File

@ -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;

View File

@ -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)) {

View File

@ -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;

View File

@ -113,6 +113,7 @@ export default {
"alerta": "Alerta",
"AlertNow": "AlertNow",
"apprise": this.$t("apprise"),
"bale": "Bale",
"Bark": "Bark",
"Bitrix24": "Bitrix24",
"clicksendsms": "ClickSend SMS",

View File

@ -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>

View File

@ -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>

View File

@ -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;

View File

@ -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. ",
@ -687,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",