From 8854e00a9283bdd81f3353c3fe55626dbee79c69 Mon Sep 17 00:00:00 2001 From: Mifzaal Abdul Baari Date: Sun, 5 Jul 2026 18:48:27 +0500 Subject: [PATCH] feat: add Ooredoo (Maldives) SMS notification provider (#7571) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- server/notification-providers/ooredoo.js | 89 ++++++++++++++++++++++++ server/notification.js | 2 + src/components/NotificationDialog.vue | 1 + src/components/notifications/Ooredoo.vue | 64 +++++++++++++++++ src/components/notifications/index.js | 2 + src/lang/en.json | 6 ++ 6 files changed, 164 insertions(+) create mode 100644 server/notification-providers/ooredoo.js create mode 100644 src/components/notifications/Ooredoo.vue diff --git a/server/notification-providers/ooredoo.js b/server/notification-providers/ooredoo.js new file mode 100644 index 000000000..32985bb0d --- /dev/null +++ b/server/notification-providers/ooredoo.js @@ -0,0 +1,89 @@ +const NotificationProvider = require("./notification-provider"); +const axios = require("axios"); + +// The Ooredoo bulk_sms/v2 gateway accepts at most this many recipients per +// request, so larger recipient lists are split into batches of this size. +const MAX_RECIPIENTS_PER_REQUEST = 20; + +class Ooredoo extends NotificationProvider { + name = "Ooredoo"; + + /** + * @inheritdoc + */ + async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { + const okMsg = "Sent Successfully."; + const url = notification.ooredooServerUrl || "https://o-papi1-lb01.ooredoo.mv/bulk_sms/v2"; + + // Users may enter recipients separated by comma, semicolon, space or + // newline. + const recipients = notification.ooredooToNumber + .split(/[\s,;]+/) + .map((number) => this.normalizePhoneNumber(number)) + .filter((number) => number !== ""); + + if (recipients.length === 0) { + throw new Error("No valid recipient phone number was provided."); + } + + try { + let config = { + headers: { + "Content-Type": "application/x-www-form-urlencoded", + Authorization: "Bearer " + notification.ooredooBearerToken, + }, + }; + config = this.getAxiosConfigWithProxy(config); + + // The gateway only accepts MAX_RECIPIENTS_PER_REQUEST numbers per + // call, so send the recipients in batches of that size. + for (let i = 0; i < recipients.length; i += MAX_RECIPIENTS_PER_REQUEST) { + const batch = recipients.slice(i, i + MAX_RECIPIENTS_PER_REQUEST); + const data = new URLSearchParams({ + username: notification.ooredooUsername, + // The gateway expects the access key to be Base64 encoded. + access_key: Buffer.from(notification.ooredooAccessKey).toString("base64"), + message: msg, + batch: batch.join(" "), + }); + + const resp = await axios.post(url, data.toString(), config); + + // The gateway returns HTTP 200 even on failure; the real outcome + // is carried in "response_code" (0 means the batch was accepted). + if (!resp.data || Number(resp.data.response_code) !== 0) { + const reason = + resp.data && resp.data.response_message + ? resp.data.response_message + : "response_code=" + (resp.data ? resp.data.response_code : "unknown"); + throw new Error("Ooredoo rejected the message: " + reason); + } + } + + return okMsg; + } catch (error) { + this.throwGeneralAxiosError(error); + } + } + + /** + * Normalize a Maldivian phone number to the "960XXXXXXX" format expected + * by the Ooredoo gateway. Numbers already in that form are returned as is. + * @param {string} phoneNumber The phone number to normalize + * @returns {string} The normalized phone number, or "" if it is empty + */ + normalizePhoneNumber(phoneNumber) { + const number = phoneNumber.replace(/[\s+]/g, ""); + + if (number === "") { + return ""; + } + // A bare 7-digit local number gets the Maldives (960) country code. + if (/^\d{7}$/.test(number)) { + return "960" + number; + } + return number; + } +} + +module.exports = Ooredoo; diff --git a/server/notification.js b/server/notification.js index 9cd327047..fb8a1bebc 100644 --- a/server/notification.js +++ b/server/notification.js @@ -37,6 +37,7 @@ const Ntfy = require("./notification-providers/ntfy"); const Octopush = require("./notification-providers/octopush"); const OneChat = require("./notification-providers/onechat"); const OneBot = require("./notification-providers/onebot"); +const Ooredoo = require("./notification-providers/ooredoo"); const Opsgenie = require("./notification-providers/opsgenie"); const JiraServiceManagement = require("./notification-providers/jira-service-management"); const PagerDuty = require("./notification-providers/pagerduty"); @@ -150,6 +151,7 @@ class Notification { new OneChat(), new OneBot(), new Onesender(), + new Ooredoo(), new Opsgenie(), new JiraServiceManagement(), new PagerDuty(), diff --git a/src/components/NotificationDialog.vue b/src/components/NotificationDialog.vue index 4fa6d5a32..57ae408f8 100644 --- a/src/components/NotificationDialog.vue +++ b/src/components/NotificationDialog.vue @@ -324,6 +324,7 @@ export default { Feishu: "Feishu (飞书)", FlashDuty: "FlashDuty (快猫星云)", FreeMobile: "FreeMobile (mobile.free.fr)", + Ooredoo: "Ooredoo (Maldives)", PushDeer: "PushDeer", promosms: "PromoSMS", serwersms: "SerwerSMS.pl", diff --git a/src/components/notifications/Ooredoo.vue b/src/components/notifications/Ooredoo.vue new file mode 100644 index 000000000..c90a0db20 --- /dev/null +++ b/src/components/notifications/Ooredoo.vue @@ -0,0 +1,64 @@ + + diff --git a/src/components/notifications/index.js b/src/components/notifications/index.js index ec31a77db..6f7f31a48 100644 --- a/src/components/notifications/index.js +++ b/src/components/notifications/index.js @@ -37,6 +37,7 @@ import Octopush from "./Octopush.vue"; import OneChat from "./OneChat.vue"; import OneBot from "./OneBot.vue"; import Onesender from "./Onesender.vue"; +import Ooredoo from "./Ooredoo.vue"; import Opsgenie from "./Opsgenie.vue"; import JiraServiceManagement from "./JiraServiceManagement.vue"; import PagerDuty from "./PagerDuty.vue"; @@ -138,6 +139,7 @@ const NotificationFormList = { OneChat: OneChat, OneBot: OneBot, Onesender: Onesender, + Ooredoo: Ooredoo, Opsgenie: Opsgenie, JiraServiceManagement: JiraServiceManagement, PagerDuty: PagerDuty, diff --git a/src/lang/en.json b/src/lang/en.json index 9c538de25..e45502382 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -957,6 +957,12 @@ "smspartnerSenderName": "SMS Sender Name", "smspartnerSenderNameInfo": "Must be between 3..=11 regular characters", "Recipient Number": "Recipient Number", + "Access Key": "Access Key", + "ooredooUsernameDescription": "The email address you use to sign in to the Ooredoo bulk SMS dashboard.", + "ooredooAccessKeyDescription": "Found under \"API Access Key\" on your Ooredoo SMS dashboard. Paste it exactly as shown — it is base64-encoded automatically before sending.", + "ooredooBearerTokenDescription": "The token shown after \"Authorization: Bearer\" in the sample cURL command on your Ooredoo SMS dashboard.", + "ooredooRecipientDescription": "Enter one or more numbers separated by a comma or space. Local 7-digit numbers are automatically prefixed with 960.", + "Leave blank to use the default Ooredoo endpoint.": "Leave blank to use the default Ooredoo endpoint.", "From Name/Number": "From Name/Number", "Leave blank to use a shared sender number.": "Leave blank to use a shared sender number.", "Octopush API Version": "Octopush API Version",