mirror of
https://github.com/louislam/uptime-kuma.git
synced 2026-03-02 22:47:01 +00:00
feat: add whatsApp (360messenger) notification provider (#7046)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
161
server/notification-providers/360messenger.js
Normal file
161
server/notification-providers/360messenger.js
Normal file
@@ -0,0 +1,161 @@
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
|
||||
class Whatsapp360messenger extends NotificationProvider {
|
||||
name = "Whatsapp360messenger";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
const okMsg = "Sent Successfully.";
|
||||
|
||||
try {
|
||||
let config = {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
Authorization: "Bearer " + notification.Whatsapp360messengerAuthToken,
|
||||
},
|
||||
};
|
||||
config = this.getAxiosConfigWithProxy(config);
|
||||
|
||||
// Use custom template if enabled
|
||||
let message = msg;
|
||||
if (notification.Whatsapp360messengerUseTemplate && notification.Whatsapp360messengerTemplate) {
|
||||
message = this.applyTemplate(
|
||||
notification.Whatsapp360messengerTemplate,
|
||||
msg,
|
||||
monitorJSON,
|
||||
heartbeatJSON
|
||||
);
|
||||
}
|
||||
|
||||
// Normalize recipients: support comma/semicolon-separated list
|
||||
const recipients = (notification.Whatsapp360messengerRecipient || "")
|
||||
.split(/[;,]/)
|
||||
.map((r) => r.trim())
|
||||
.filter((r) => r !== "");
|
||||
|
||||
// Normalize group IDs: support array (multi-select) and fallback to single value / delimited string
|
||||
const rawGroupIds =
|
||||
notification.Whatsapp360messengerGroupIds || notification.Whatsapp360messengerGroupId || "";
|
||||
|
||||
let groupIds = [];
|
||||
if (Array.isArray(rawGroupIds)) {
|
||||
groupIds = rawGroupIds
|
||||
.map((g) => {
|
||||
if (typeof g === "string") {
|
||||
return g.trim();
|
||||
}
|
||||
if (g && typeof g === "object" && g.id) {
|
||||
return String(g.id).trim();
|
||||
}
|
||||
return "";
|
||||
})
|
||||
.filter((g) => g !== "");
|
||||
} else if (typeof rawGroupIds === "string" && rawGroupIds.trim() !== "") {
|
||||
groupIds = rawGroupIds
|
||||
.split(/[;,]/)
|
||||
.map((g) => g.trim())
|
||||
.filter((g) => g !== "");
|
||||
}
|
||||
|
||||
const hasGroupId = groupIds.length > 0;
|
||||
const hasRecipient = recipients.length > 0;
|
||||
|
||||
// Send to both if both are provided
|
||||
if (hasGroupId && hasRecipient) {
|
||||
// Send to all individual recipients
|
||||
await Promise.all(
|
||||
recipients.map((recipient) => {
|
||||
const recipientData = {
|
||||
phonenumber: recipient,
|
||||
text: message,
|
||||
};
|
||||
return axios.post("https://api.360messenger.com/v2/sendMessage", recipientData, config);
|
||||
})
|
||||
);
|
||||
|
||||
// Send to all selected groups
|
||||
await Promise.all(
|
||||
groupIds.map((groupId) => {
|
||||
const groupData = {
|
||||
groupId,
|
||||
text: message,
|
||||
};
|
||||
return axios.post("https://api.360messenger.com/v2/sendGroup", groupData, config);
|
||||
})
|
||||
);
|
||||
|
||||
return `${okMsg} (Sent to ${recipients.length} recipient(s) and ${groupIds.length} group(s))`;
|
||||
} else if (hasGroupId) {
|
||||
// Send to group(s) only
|
||||
await Promise.all(
|
||||
groupIds.map((groupId) => {
|
||||
const data = {
|
||||
groupId,
|
||||
text: message,
|
||||
};
|
||||
return axios.post("https://api.360messenger.com/v2/sendGroup", data, config);
|
||||
})
|
||||
);
|
||||
|
||||
return `${okMsg} (Sent to ${groupIds.length} group(s))`;
|
||||
} else if (hasRecipient) {
|
||||
// Send to recipient(s) only
|
||||
await Promise.all(
|
||||
recipients.map((recipient) => {
|
||||
const data = {
|
||||
phonenumber: recipient,
|
||||
text: message,
|
||||
};
|
||||
return axios.post("https://api.360messenger.com/v2/sendMessage", data, config);
|
||||
})
|
||||
);
|
||||
|
||||
return `${okMsg} (Sent to ${recipients.length} recipient(s))`;
|
||||
} else {
|
||||
throw new Error("No recipient or group specified");
|
||||
}
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply template with variables
|
||||
* @param {string} template - Template string
|
||||
* @param {string} msg - Default message
|
||||
* @param {object} monitorJSON - Monitor data
|
||||
* @param {object} heartbeatJSON - Heartbeat data
|
||||
* @returns {string} Formatted message
|
||||
*/
|
||||
applyTemplate(template, msg, monitorJSON, heartbeatJSON) {
|
||||
try {
|
||||
// Simple template replacement
|
||||
let result = template;
|
||||
|
||||
// Replace monitor variables
|
||||
if (monitorJSON) {
|
||||
result = result.replace(/{{ monitorJSON\['name'\] }}/g, monitorJSON.name || "");
|
||||
result = result.replace(/{{ monitorJSON\['url'\] }}/g, monitorJSON.url || "");
|
||||
}
|
||||
|
||||
// Replace message variable
|
||||
result = result.replace(/{{ msg }}/g, msg);
|
||||
|
||||
// Handle conditional blocks (simple if statements)
|
||||
result = result.replace(/{% if monitorJSON %}([\s\S]*?){% endif %}/g, (match, content) => {
|
||||
return monitorJSON ? content : "";
|
||||
});
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
// If template parsing fails, return original message
|
||||
return msg;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Whatsapp360messenger;
|
||||
@@ -86,6 +86,7 @@ const SMSPlanet = require("./notification-providers/sms-planet");
|
||||
const SpugPush = require("./notification-providers/spugpush");
|
||||
const SMSIR = require("./notification-providers/smsir");
|
||||
const { commandExists } = require("./util-server");
|
||||
const Whatsapp360messenger = require("./notification-providers/360messenger");
|
||||
const Webpush = require("./notification-providers/Webpush");
|
||||
const HaloPSA = require("./notification-providers/HaloPSA");
|
||||
|
||||
@@ -189,6 +190,7 @@ class Notification {
|
||||
new Notifery(),
|
||||
new SMSIR(),
|
||||
new SendGrid(),
|
||||
new Whatsapp360messenger(),
|
||||
new Webpush(),
|
||||
new HaloPSA(),
|
||||
];
|
||||
|
||||
@@ -244,6 +244,7 @@ export default {
|
||||
whapi: "WhatsApp (Whapi)",
|
||||
evolution: "WhatsApp (Evolution)",
|
||||
waha: "WhatsApp (WAHA)",
|
||||
Whatsapp360messenger: "WhatsApp (360messenger)",
|
||||
};
|
||||
|
||||
// Push Services - Push notification services
|
||||
|
||||
300
src/components/notifications/360messenger.vue
Normal file
300
src/components/notifications/360messenger.vue
Normal file
@@ -0,0 +1,300 @@
|
||||
<template>
|
||||
<div class="mb-3">
|
||||
<label for="360messenger-auth-token" class="form-label">{{ $t("360messengerAuthToken") }}</label>
|
||||
<HiddenInput
|
||||
id="360messenger-auth-token"
|
||||
v-model="$parent.notification.Whatsapp360messengerAuthToken"
|
||||
:required="true"
|
||||
autocomplete="new-password"
|
||||
></HiddenInput>
|
||||
<i18n-t tag="div" keypath="360messengerWayToGetUrlAndToken" class="form-text">
|
||||
<a href="https://360messenger.com/en/uptime-kuma" target="_blank">
|
||||
https://360messenger.com/en/uptime-kuma
|
||||
</a>
|
||||
</i18n-t>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="360messenger-recipient" class="form-label">{{ $t("360messengerRecipient") }}</label>
|
||||
<input
|
||||
id="360messenger-recipient"
|
||||
v-model="$parent.notification.Whatsapp360messengerRecipient"
|
||||
type="text"
|
||||
class="form-control"
|
||||
placeholder="447488888888, 447499999999"
|
||||
:required="!hasAnySelectedGroup"
|
||||
/>
|
||||
<div class="form-text">{{ $t("360messengerWayToWriteRecipient", ["447488888888"]) }}</div>
|
||||
</div>
|
||||
|
||||
<!-- Checkbox to enable/disable Combobox -->
|
||||
<div class="mb-3 form-check form-switch">
|
||||
<input id="360messenger-enable-options" v-model="isOptionsEnabled" type="checkbox" class="form-check-input" />
|
||||
<label for="360messenger-enable-options" class="form-check-label">
|
||||
{{ $t("360messengerEnableSendToGroup") }}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Group selection using existing VueMultiselect -->
|
||||
<div class="mb-3">
|
||||
<label for="360messenger-group-list" class="form-label">
|
||||
{{ $t("360messengerGroupList") }}
|
||||
</label>
|
||||
<VueMultiselect
|
||||
id="360messenger-group-list"
|
||||
v-model="$parent.notification.Whatsapp360messengerGroupIds"
|
||||
:options="groupOptions"
|
||||
:multiple="true"
|
||||
:close-on-select="false"
|
||||
:clear-on-select="false"
|
||||
:preserve-search="true"
|
||||
:placeholder="$t('360messengerSelectGroupList')"
|
||||
:preselect-first="false"
|
||||
:max-height="400"
|
||||
:taggable="false"
|
||||
:disabled="!isOptionsEnabled || isLoadingGroups"
|
||||
label="label"
|
||||
track-by="id"
|
||||
>
|
||||
<template #noOptions>
|
||||
<div class="multiselect__option">
|
||||
<span v-if="isLoadingGroups">{{ $t("Loading...") }}</span>
|
||||
<span v-else>{{ $t("360messengerErrorNoGroups") }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</VueMultiselect>
|
||||
<div v-if="errorMessage" class="text-danger mt-1">{{ errorMessage }}</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="form-check form-switch">
|
||||
<input
|
||||
v-model="$parent.notification.Whatsapp360messengerUseTemplate"
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
/>
|
||||
<label class="form-check-label">{{ $t("360messengerCustomMessageTemplate") }}</label>
|
||||
</div>
|
||||
|
||||
<div class="form-text">
|
||||
{{ $t("360messengerEnableCustomMessage") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="$parent.notification.Whatsapp360messengerUseTemplate">
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="360messenger-template">{{ $t("360messengerMessageTemplate") }}</label>
|
||||
<TemplatedTextarea
|
||||
id="360messenger-template"
|
||||
v-model="$parent.notification.Whatsapp360messengerTemplate"
|
||||
:required="true"
|
||||
:placeholder="Whatsapp360messengerTemplatedTextareaPlaceholder"
|
||||
></TemplatedTextarea>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import HiddenInput from "../HiddenInput.vue";
|
||||
import TemplatedTextarea from "../TemplatedTextarea.vue";
|
||||
import VueMultiselect from "vue-multiselect";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
HiddenInput,
|
||||
TemplatedTextarea,
|
||||
VueMultiselect,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isOptionsEnabled: false,
|
||||
groups: [],
|
||||
isLoadingGroups: false,
|
||||
errorMessage: "",
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
Whatsapp360messengerTemplatedTextareaPlaceholder() {
|
||||
return this.$t("Example:", [
|
||||
`
|
||||
Uptime Kuma Alert{% if monitorJSON %} - {{ monitorJSON['name'] }}{% endif %}
|
||||
|
||||
{{ msg }}
|
||||
`,
|
||||
]);
|
||||
},
|
||||
groupOptions() {
|
||||
return this.groups.map((g) => ({
|
||||
id: g.id,
|
||||
label: `${g.id} - ${g.name}`,
|
||||
}));
|
||||
},
|
||||
selectedGroupIds() {
|
||||
const raw =
|
||||
this.$parent.notification.Whatsapp360messengerGroupIds ||
|
||||
this.$parent.notification.Whatsapp360messengerGroupId;
|
||||
|
||||
if (Array.isArray(raw)) {
|
||||
return raw
|
||||
.map((item) => {
|
||||
if (typeof item === "string") {
|
||||
return item.trim();
|
||||
}
|
||||
if (item && typeof item === "object" && item.id) {
|
||||
return String(item.id).trim();
|
||||
}
|
||||
return "";
|
||||
})
|
||||
.filter((id) => id !== "");
|
||||
}
|
||||
|
||||
if (typeof raw === "string" && raw.trim() !== "") {
|
||||
return raw
|
||||
.split(/[;,]/)
|
||||
.map((id) => id.trim())
|
||||
.filter((id) => id !== "");
|
||||
}
|
||||
|
||||
return [];
|
||||
},
|
||||
hasAnySelectedGroup() {
|
||||
return this.selectedGroupIds.length > 0;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
// When checkbox is enabled, fetch groups from API
|
||||
isOptionsEnabled(newValue, oldValue) {
|
||||
if (newValue) {
|
||||
this.fetchGroups();
|
||||
} else if (oldValue && !this.errorMessage) {
|
||||
// Only clear if user manually unchecked (not due to error)
|
||||
this.$parent.notification.Whatsapp360messengerGroupIds = [];
|
||||
this.$parent.notification.Whatsapp360messengerGroupId = "";
|
||||
this.groups = [];
|
||||
}
|
||||
},
|
||||
"$parent.notification.Whatsapp360messengerGroupIds": {
|
||||
immediate: true,
|
||||
handler(value) {
|
||||
if (Array.isArray(value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let source = value;
|
||||
|
||||
if (!source && this.$parent.notification.Whatsapp360messengerGroupId) {
|
||||
source = this.$parent.notification.Whatsapp360messengerGroupId;
|
||||
}
|
||||
|
||||
let normalized = [];
|
||||
|
||||
if (typeof source === "string" && source.trim() !== "") {
|
||||
normalized = source
|
||||
.split(/[;,]/)
|
||||
.map((v) => v.trim())
|
||||
.filter((v) => v !== "");
|
||||
}
|
||||
|
||||
this.$parent.notification.Whatsapp360messengerGroupIds = normalized;
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
toggleDropdown() {
|
||||
if (!this.isOptionsEnabled || this.isLoadingGroups) {
|
||||
return;
|
||||
}
|
||||
this.isDropdownOpen = !this.isDropdownOpen;
|
||||
},
|
||||
toggleGroupId(id) {
|
||||
const trimmed = typeof id === "string" ? id.trim() : "";
|
||||
if (!trimmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.selectedGroupIds.includes(trimmed)) {
|
||||
this.removeGroupId(trimmed);
|
||||
} else {
|
||||
this.addGroupId(trimmed);
|
||||
}
|
||||
},
|
||||
addGroupId(id) {
|
||||
const trimmed = typeof id === "string" ? id.trim() : "";
|
||||
if (!trimmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
const list = this.$parent.notification.Whatsapp360messengerGroupIds;
|
||||
if (!Array.isArray(list)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Prefer the new array-based field going forward
|
||||
this.$parent.notification.Whatsapp360messengerGroupId = "";
|
||||
|
||||
if (!list.includes(trimmed)) {
|
||||
list.push(trimmed);
|
||||
}
|
||||
},
|
||||
removeGroupId(id) {
|
||||
const list = this.$parent.notification.Whatsapp360messengerGroupIds;
|
||||
if (!Array.isArray(list)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$parent.notification.Whatsapp360messengerGroupIds = list.filter((x) => x !== id);
|
||||
},
|
||||
async fetchGroups() {
|
||||
this.isLoadingGroups = true;
|
||||
this.errorMessage = "";
|
||||
|
||||
try {
|
||||
const token = this.$parent.notification.Whatsapp360messengerAuthToken;
|
||||
|
||||
if (!token) {
|
||||
this.errorMessage = this.$t("360messengerErrorNoApiKey");
|
||||
this.isLoadingGroups = false;
|
||||
this.isOptionsEnabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await fetch("https://api.360messenger.com/v2/groupChat/getGroupList", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success && result.data && result.data.groups) {
|
||||
this.groups = result.data.groups;
|
||||
if (this.groups.length === 0) {
|
||||
this.errorMessage = this.$t("360messengerErrorNoGroups");
|
||||
this.isOptionsEnabled = false;
|
||||
}
|
||||
} else {
|
||||
// Handle API error response
|
||||
const statusCode = result.statusCode || response.status;
|
||||
const message = result.message || "Failed to load groups";
|
||||
this.errorMessage = this.$t("360messengerErrorApi", { statusCode, message });
|
||||
this.isOptionsEnabled = false;
|
||||
}
|
||||
} catch (error) {
|
||||
this.errorMessage = this.$t("360messengerErrorGeneric", { message: error.message });
|
||||
this.isOptionsEnabled = false;
|
||||
console.error("Error fetching groups:", error);
|
||||
} finally {
|
||||
this.isLoadingGroups = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
textarea {
|
||||
min-height: 150px;
|
||||
}
|
||||
</style>
|
||||
@@ -73,6 +73,7 @@ import SpugPush from "./SpugPush.vue";
|
||||
import SevenIO from "./SevenIO.vue";
|
||||
import Whapi from "./Whapi.vue";
|
||||
import WAHA from "./WAHA.vue";
|
||||
import Whatsapp360messenger from "./360messenger.vue";
|
||||
import Evolution from "./Evolution.vue";
|
||||
import Cellsynt from "./Cellsynt.vue";
|
||||
import WPush from "./WPush.vue";
|
||||
@@ -168,6 +169,7 @@ const NotificationFormList = {
|
||||
evolution: Evolution,
|
||||
notifery: Notifery,
|
||||
waha: WAHA,
|
||||
Whatsapp360messenger: Whatsapp360messenger,
|
||||
gtxmessaging: GtxMessaging,
|
||||
Cellsynt: Cellsynt,
|
||||
WPush: WPush,
|
||||
|
||||
@@ -1314,6 +1314,24 @@
|
||||
"wayToGetWahaApiKey": "API Key is WHATSAPP_API_KEY environment variable value you used to run WAHA.",
|
||||
"wayToGetWahaSession": "From this session WAHA sends notifications to Chat ID. You can find it in WAHA Dashboard.",
|
||||
"wayToWriteWahaChatId": "The phone number with the international prefix, but without the plus sign at the start ({0}), the Contact ID ({1}) or the Group ID ({2}). Notifications are sent to this Chat ID from WAHA Session.",
|
||||
"360messengerAuthToken": "360messenger API Key",
|
||||
"360messengerRecipient": "Recipient phone number(s)",
|
||||
"360messengerGroupId": "360messenger Group ID",
|
||||
"360messengerUseTemplate": "Use a custom message template",
|
||||
"360messengerTemplate": "360messenger Message Template",
|
||||
"360messengerGroupList": "WhatsApp groups",
|
||||
"360messengerSelectGroupList": "Select a group to add",
|
||||
"360messengerSelectedGroupID": "Selected Group ID(s)",
|
||||
"360messengerEnableSendToGroup": "Enable sending to WhatsApp group(s)",
|
||||
"360messengerCustomMessageTemplate": "Custom message template",
|
||||
"360messengerEnableCustomMessage": "Enable a custom message template instead of the default message.",
|
||||
"360messengerMessageTemplate": "Message template",
|
||||
"360messengerWayToGetUrlAndToken": "You can get your 360messenger API key from {0}.",
|
||||
"360messengerWayToWriteRecipient": "Enter one or more phone numbers in international format without a leading plus (e.g. {0}). Separate multiple numbers with commas.",
|
||||
"360messengerErrorNoApiKey": "Please enter your 360messenger API key first.",
|
||||
"360messengerErrorNoGroups": "No WhatsApp groups were found for this account.",
|
||||
"360messengerErrorApi": "Unable to load the WhatsApp group list (Error {statusCode}: {message}).",
|
||||
"360messengerErrorGeneric": "Unable to load the WhatsApp group list: {message}",
|
||||
"YZJ Webhook URL": "YZJ Webhook URL",
|
||||
"YZJ Robot Token": "YZJ Robot token",
|
||||
"Plain Text": "Plain Text",
|
||||
|
||||
Reference in New Issue
Block a user