@xuda.io/notification_module 1.1.128 → 1.1.129

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  import path from 'path';
2
2
  import _ from 'lodash';
3
3
  import admin from 'firebase-admin';
4
+ import { parsePhoneNumberFromString } from 'libphonenumber-js';
4
5
 
5
6
  console.log('Notifications Module loaded...');
6
7
 
@@ -23,6 +24,78 @@ admin.initializeApp({
23
24
  const messaging = admin.messaging();
24
25
  //////////////////////////////////////////////
25
26
 
27
+ //////////////////// SMS (Twilio) /////////////////////
28
+ // Best-effort HTML->plain for SMS bodies (templates are authored as HTML).
29
+ const _strip_html = (s) =>
30
+ String(s || '')
31
+ .replace(/<style[\s\S]*?<\/style>/gi, ' ')
32
+ .replace(/<[^>]+>/g, ' ')
33
+ .replace(/&nbsp;/gi, ' ')
34
+ .replace(/&amp;/gi, '&')
35
+ .replace(/\s+/g, ' ')
36
+ .trim();
37
+
38
+ // Normalize a stored phone (+ its account country) to E.164 for Twilio.
39
+ const _to_e164 = (phone, country) => {
40
+ if (!phone) return null;
41
+ const raw = String(phone).trim();
42
+ try {
43
+ const pn = parsePhoneNumberFromString(raw, (country || '').toUpperCase() || undefined);
44
+ if (pn && pn.isValid()) return pn.number; // E.164
45
+ } catch (e) { /* fall through */ }
46
+ const digits = raw.replace(/[^\d+]/g, '');
47
+ if (/^\+\d{7,15}$/.test(digits)) return digits;
48
+ if (/^00\d{7,15}$/.test(digits)) return '+' + digits.slice(2);
49
+ return null;
50
+ };
51
+
52
+ // Send an SMS via Twilio (Messaging Service). Returns
53
+ // { sent, log_only?, sid?, status?, to?, error? }.
54
+ // GATED: unless _conf.bounce_monitor.sms_enabled === true (or opts.force) AND the
55
+ // twilio creds + a sender (messaging_service_sid|from) exist, it composes + LOGS
56
+ // only and sends nothing — so we can build/test before toll-free verification is
57
+ // approved. Never logs credential values.
58
+ export const send_sms = async (to, body, opts = {}) => {
59
+ const tw = _conf.twilio || {};
60
+ const enabled = _conf.bounce_monitor?.sms_enabled === true || opts.force === true;
61
+ const to_e164 = _to_e164(to, opts.country);
62
+ const text = _strip_html(body).slice(0, 480);
63
+ if (!to_e164) {
64
+ console.warn(`[sms] skip: cannot normalize "${to}" (country=${opts.country || '?'})`);
65
+ return { sent: false, error: 'invalid_phone' };
66
+ }
67
+ const have_creds = tw.account_sid && tw.api_key_sid && tw.api_key_secret && (tw.messaging_service_sid || tw.from);
68
+ if (!enabled || !have_creds) {
69
+ console.log(`[sms log_only]${enabled ? '' : ' (disabled)'}${have_creds ? '' : ' (no creds)'} -> ${to_e164} :: ${text}`);
70
+ return { sent: false, log_only: true, to: to_e164 };
71
+ }
72
+ try {
73
+ const url = `https://api.twilio.com/2010-04-01/Accounts/${tw.account_sid}/Messages.json`;
74
+ const form = new URLSearchParams();
75
+ form.set('To', to_e164);
76
+ if (tw.messaging_service_sid) form.set('MessagingServiceSid', tw.messaging_service_sid);
77
+ else form.set('From', tw.from);
78
+ form.set('Body', text);
79
+ const auth = Buffer.from(`${tw.api_key_sid}:${tw.api_key_secret}`).toString('base64');
80
+ const resp = await fetch(url, {
81
+ method: 'POST',
82
+ headers: { Authorization: `Basic ${auth}`, 'Content-Type': 'application/x-www-form-urlencoded' },
83
+ body: form.toString(),
84
+ });
85
+ const data = await resp.json().catch(() => ({}));
86
+ if (!resp.ok) {
87
+ console.error(`[sms] Twilio ${resp.status} (code ${data?.code || '?'}):`, data?.message || data);
88
+ return { sent: false, error: data?.message || `http_${resp.status}`, code: data?.code, to: to_e164 };
89
+ }
90
+ console.log(`[sms] sent sid=${data.sid} to=${to_e164} status=${data.status}`);
91
+ return { sent: true, sid: data.sid, status: data.status, to: to_e164 };
92
+ } catch (err) {
93
+ console.error('[sms] send error:', err.message);
94
+ return { sent: false, error: err.message, to: to_e164 };
95
+ }
96
+ };
97
+ //////////////////////////////////////////////
98
+
26
99
  export const get_new_notification_count = async (req) => {
27
100
  return await db_module.get_couch_view('xuda_notification', 'new_notifications_count', {
28
101
  key: [req.uid],
@@ -61,7 +134,7 @@ export const update_notification_status = async (req) => {
61
134
  //, _ch= global[`_notification_module_ch`]
62
135
  export const submit_notification = async (req) => {
63
136
  try {
64
- let { type, app_id, to_app_id, uid_arr, subject = '', body = '', delivery_method = [], display_type = 'info', ref, email, sender_uid, system, topic, params } = req;
137
+ let { type, app_id, to_app_id, uid_arr, subject = '', body = '', sms_body = '', delivery_method = [], display_type = 'info', ref, email, sender_uid, system, topic, params } = req;
65
138
  let obj = {};
66
139
  const { notification_categories } = await import(path.join(process.env.XUDA_HOME, 'notification_templates.mjs'));
67
140
 
@@ -89,6 +162,9 @@ export const submit_notification = async (req) => {
89
162
  if (!body) {
90
163
  body = _.template(_topic.body)(params);
91
164
  }
165
+ if (!sms_body && _topic.sms_body) {
166
+ sms_body = _.template(_topic.sms_body)(params);
167
+ }
92
168
  } else {
93
169
  return {
94
170
  code: -1,
@@ -237,6 +313,27 @@ export const submit_notification = async (req) => {
237
313
  }
238
314
  break;
239
315
 
316
+ case 'sms': {
317
+ if (!account_ret) {
318
+ account_ret = await db_module.get_couch_doc('xuda_accounts', uid).catch(() => null);
319
+ }
320
+ const ai = account_ret?.data?.account_info || {};
321
+ const phone = req.phone || ai.phone_number;
322
+ const country = req.country || ai.country;
323
+ const text = sms_body || _strip_html(body);
324
+ if (!phone) {
325
+ console.warn(`[sms] no phone on ${uid}; sms channel skipped`);
326
+ send_result[uid] = false;
327
+ obj.sms_status = 'no_phone';
328
+ break;
329
+ }
330
+ const r = await send_sms(phone, text, { country });
331
+ obj.sms_to = r.to;
332
+ obj.sms_status = r.sent ? (r.sid || 'sent') : r.log_only ? 'log_only' : r.error || 'failed';
333
+ if (!r.sent && !r.log_only) send_result[uid] = false;
334
+ break;
335
+ }
336
+
240
337
  default:
241
338
  send_result[uid] = false;
242
339
  break;
package/index_ms.mjs CHANGED
@@ -17,6 +17,10 @@
17
17
 
18
18
 
19
19
 
20
+ export const send_sms = async function (...args) {
21
+ return await broker.send_to_queue("send_sms", ...args);
22
+ };
23
+
20
24
  export const get_new_notification_count = async function (...args) {
21
25
  return await broker.send_to_queue("get_new_notification_count", ...args);
22
26
  };
package/index_msa.mjs CHANGED
@@ -17,6 +17,10 @@
17
17
 
18
18
 
19
19
 
20
+ export const send_sms = function (...args) {
21
+ broker.send_to_queue_async("send_sms", ...args);
22
+ };
23
+
20
24
  export const get_new_notification_count = function (...args) {
21
25
  broker.send_to_queue_async("get_new_notification_count", ...args);
22
26
  };
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@xuda.io/notification_module",
3
- "version": "1.1.128",
3
+ "version": "1.1.129",
4
4
  "description": "Xuda Notification Server Module",
5
5
  "main": "index.mjs",
6
6
  "dependencies": {
7
7
  "lodash": "^4.17.21",
8
8
  "amqplib": "^0.10.9",
9
- "firebase-admin": "^12.0.0"
9
+ "firebase-admin": "^12.0.0",
10
+ "libphonenumber-js": "^1.11.4"
10
11
  },
11
12
  "devDependencies": {},
12
13
  "scripts": {