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:
WaMessenger
2026-02-25 14:11:59 +03:30
committed by GitHub
parent 953d97fd2e
commit 86b86fae55
6 changed files with 484 additions and 0 deletions

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

View File

@@ -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(),
];

View File

@@ -244,6 +244,7 @@ export default {
whapi: "WhatsApp (Whapi)",
evolution: "WhatsApp (Evolution)",
waha: "WhatsApp (WAHA)",
Whatsapp360messenger: "WhatsApp (360messenger)",
};
// Push Services - Push notification services

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

View File

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

View File

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