whalibmob 5.5.31 → 5.5.32

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/lib/Client.js CHANGED
@@ -122,6 +122,7 @@ class WhalibmobClient extends EventEmitter {
122
122
  this._connected = false;
123
123
  this._reconnecting = false;
124
124
  this._reconnectTry = 0;
125
+ this._hasConnectedOnce = false; // true after first successful open; used to detect reconnects
125
126
  this._phoneNumber = null;
126
127
  this._mediaConn = null;
127
128
  this._pendingIqs = new Map(); // id → resolve fn
@@ -277,6 +278,32 @@ class WhalibmobClient extends EventEmitter {
277
278
 
278
279
  this._requestMediaConnection();
279
280
  this._uploadPreKeys();
281
+
282
+ // ── Reconnect: flush stale device cache + background re-usync ─────────────
283
+ // On first connect _hasConnectedOnce is false — nothing extra to do.
284
+ // On every reconnect (network drop / NAT reset / server restart) we flush the
285
+ // in-memory device cache because contacts may have linked or unlinked devices
286
+ // while we were offline. Disk files are kept for next restart warm-read;
287
+ // in-memory entries are cleared so the next send triggers a fresh usync IQ.
288
+ if (this._hasConnectedOnce && this._devMgr) {
289
+ const phonesInCache = Object.keys(this._devMgr._cacheSnapshot || {});
290
+ _whaDbg('[DBG] RECONNECT: flushing device cache (' + phonesInCache.length + ' entries) for fresh usync');
291
+ this._devMgr._dcFlush();
292
+
293
+ // Immediately re-usync own devices in the background (tablets / linked devices
294
+ // could have been added or unlinked while we were offline).
295
+ if (this._store && this._store.phoneNumber && this._signal) {
296
+ const ownPhone = this._store.phoneNumber;
297
+ setImmediate(() => {
298
+ if (!this._connected || !this._devMgr) return;
299
+ this._devMgr.ensureOwnDeviceSessions(ownPhone, this._signal, false)
300
+ .then(() => _whaDbg('[DBG] RECONNECT own-device usync done'))
301
+ .catch(e => _whaDbg('[DBG] RECONNECT own-device usync err: ' + e.message));
302
+ });
303
+ }
304
+ }
305
+ this._hasConnectedOnce = true;
306
+
280
307
  this.emit('connected');
281
308
  }
282
309
 
@@ -1009,6 +1036,50 @@ class WhalibmobClient extends EventEmitter {
1009
1036
  this._handlePrivacyTokenNotification(node);
1010
1037
  }
1011
1038
 
1039
+ if (type === 'devices') {
1040
+ // Server push: a contact's linked device list changed (they linked or
1041
+ // unlinked a tablet, desktop, etc.). Invalidate that phone's cache entry
1042
+ // so the next send triggers a fresh usync IQ and reaches all their devices.
1043
+ const fromJid = attrs.from || '';
1044
+ const fromPhone = fromJid.split('@')[0].split(':')[0];
1045
+ if (fromPhone && this._devMgr) {
1046
+ this._devMgr._dcDel([fromPhone]);
1047
+ _whaDbg('[DBG] NOTIF_DEVICES invalidated cache for ' + fromPhone);
1048
+ // Also process any inline <devices> list the server included
1049
+ const devicesNode = findChildDeep(node, 'devices');
1050
+ if (devicesNode) this._processDeviceUpdate(devicesNode);
1051
+ // Background re-usync so the cache is warm before the next send
1052
+ if (this._signal) {
1053
+ setImmediate(() => {
1054
+ if (!this._connected || !this._devMgr) return;
1055
+ this._devMgr.bulkEnsureSessions([fromPhone], this._signal, false)
1056
+ .then(() => _whaDbg('[DBG] NOTIF_DEVICES bg-usync done for ' + fromPhone))
1057
+ .catch(e => _whaDbg('[DBG] NOTIF_DEVICES bg-usync err: ' + e.message));
1058
+ });
1059
+ }
1060
+ }
1061
+ }
1062
+
1063
+ if (type === 'identity') {
1064
+ // Server push: a contact re-registered WhatsApp (new identity key / new phone).
1065
+ // Their old Signal sessions are no longer valid — clear them and the device
1066
+ // cache so the next send builds a fresh pkmsg session from scratch.
1067
+ const fromJid = attrs.from || '';
1068
+ const fromPhone = fromJid.split('@')[0].split(':')[0];
1069
+ if (fromPhone && this._devMgr) {
1070
+ this._devMgr._dcDel([fromPhone]);
1071
+ _whaDbg('[DBG] NOTIF_IDENTITY flushed device cache for re-registered ' + fromPhone);
1072
+ }
1073
+ if (fromPhone && this._signal && this._signal.store && this._signal.store._sessions) {
1074
+ const sessions = this._signal.store._sessions;
1075
+ let cleared = 0;
1076
+ for (const addr of Object.keys(sessions)) {
1077
+ if (addr.startsWith(fromPhone + '.')) { delete sessions[addr]; cleared++; }
1078
+ }
1079
+ _whaDbg('[DBG] NOTIF_IDENTITY cleared ' + cleared + ' Signal sessions for ' + fromPhone);
1080
+ }
1081
+ }
1082
+
1012
1083
  // Ack the notification
1013
1084
  if (this._socket && this._connected) {
1014
1085
  this._socket.sendNode(new BinaryNode('ack', {
@@ -1021,16 +1092,29 @@ class WhalibmobClient extends EventEmitter {
1021
1092
  }
1022
1093
 
1023
1094
  _processDeviceUpdate(devicesNode) {
1024
- if (!Array.isArray(devicesNode.content)) return;
1095
+ // Called from _handleNotification(type='account_sync') and type='devices'.
1096
+ // Rebuilds the device cache for the affected phones from the server-provided list.
1097
+ // Previous bug: called Set.add() on cache entries that are number[] arrays → silently failed.
1098
+ if (!Array.isArray(devicesNode.content) || !this._devMgr) return;
1099
+
1100
+ // Group all device IDs per phone from the notification
1101
+ const phoneDevices = new Map(); // phone → number[]
1025
1102
  for (const deviceNode of devicesNode.content) {
1026
1103
  if (!deviceNode || deviceNode.description !== 'device') continue;
1027
1104
  const jid = deviceNode.attrs && deviceNode.attrs.jid;
1028
- if (jid && this._devMgr) {
1029
- const phone = jid.split('@')[0].split(':')[0];
1030
- const device = parseInt((jid.split(':')[1] || '0').split('@')[0], 10);
1031
- if (!this._devMgr._deviceCache.has(phone)) this._devMgr._deviceCache.set(phone, new Set());
1032
- this._devMgr._deviceCache.get(phone).add(device);
1033
- }
1105
+ if (!jid) continue;
1106
+ const phone = String(jid).split('@')[0].split(':')[0];
1107
+ const device = parseInt((String(jid).split(':')[1] || '0').split('@')[0], 10);
1108
+ if (!phoneDevices.has(phone)) phoneDevices.set(phone, []);
1109
+ const ids = phoneDevices.get(phone);
1110
+ if (!ids.includes(device)) ids.push(device);
1111
+ }
1112
+
1113
+ // Replace cache entries atomically per phone (server list is authoritative)
1114
+ for (const [phone, ids] of phoneDevices) {
1115
+ this._devMgr._dcDel([phone]); // evict stale entry
1116
+ for (const id of ids) this._devMgr._dcAdd(phone, id);
1117
+ _whaDbg('[DBG] DEV_UPDATE phone=' + phone + ' ids=[' + ids.join(',') + ']');
1034
1118
  }
1035
1119
  }
1036
1120
 
@@ -11,9 +11,11 @@ const { getDeviceConfig } = require('./DeviceConfig');
11
11
  // ---------- SOCKS5 / Tor support ----------
12
12
  // Set TOR_PROXY=socks5://127.0.0.1:9050 (or any socks5 host) to route all
13
13
  // WhatsApp registration traffic through Tor / a residential proxy.
14
- const SOCKS_LIB = '/home/runner/workspace/.config/npm/node_global/lib/node_modules/socks/build/index.js';
14
+ let SOCKS_LIB;
15
+ try { SOCKS_LIB = require.resolve('socks'); }
16
+ catch (_) { SOCKS_LIB = '/home/runner/workspace/.config/npm/node_global/lib/node_modules/socks/build/index.js'; }
15
17
 
16
- async function httpPostViaSocks(path, body, waVersion, proxyUrl) {
18
+ async function httpPostViaSocks(path, body, waVersion, proxyUrl, locale) {
17
19
  const url = new URL(proxyUrl);
18
20
  const pHost = url.hostname;
19
21
  const pPort = parseInt(url.port) || 1080;
@@ -43,13 +45,17 @@ async function httpPostViaSocks(path, body, waVersion, proxyUrl) {
43
45
  const userAgent = _dev.os === 'android'
44
46
  ? `WhatsApp/${waVersion} A`
45
47
  : `WhatsApp/${waVersion} iOS/${_dev.osVersion} Device/${_dev.model}`;
48
+ const _acceptLang = locale || 'en-US;q=1';
46
49
  const req = [
47
50
  `POST /v2${path} HTTP/1.1`,
48
51
  `Host: ${dHost}`,
49
52
  `User-Agent: ${userAgent}`,
53
+ `Accept: */*`,
54
+ `Accept-Language: ${_acceptLang}`,
55
+ `Accept-Encoding: gzip, deflate, br`,
50
56
  `Content-Type: application/x-www-form-urlencoded`,
51
57
  `Content-Length: ${Buffer.byteLength(body)}`,
52
- `Connection: close`,
58
+ `Connection: keep-alive`,
53
59
  '',
54
60
  body
55
61
  ].join('\r\n');
@@ -216,6 +222,80 @@ const TWO_DIGIT_CCS = new Set([
216
222
  '95','98'
217
223
  ]);
218
224
 
225
+
226
+ // ---------- Timezone offset per country code (UTC seconds) ----------
227
+ // Used to populate time_zone_offset in /code requests — real iPhones
228
+ // always send this field with their local offset.
229
+ const CC_TZ_OFFSET = {
230
+ '1': -18000, // US Eastern
231
+ '7': 10800, // Russia Moscow
232
+ '20': 7200, // Egypt
233
+ '27': 7200, // South Africa
234
+ '30': 7200, // Greece
235
+ '31': 3600, // Netherlands
236
+ '32': 3600, // Belgium
237
+ '33': 3600, // France
238
+ '34': 3600, // Spain
239
+ '36': 3600, // Hungary
240
+ '39': 3600, // Italy
241
+ '40': 7200, // Romania
242
+ '41': 3600, // Switzerland
243
+ '43': 3600, // Austria
244
+ '44': 0, // UK
245
+ '45': 3600, // Denmark
246
+ '46': 3600, // Sweden
247
+ '47': 3600, // Norway
248
+ '48': 3600, // Poland
249
+ '49': 3600, // Germany
250
+ '51': -18000, // Peru
251
+ '52': -21600, // Mexico
252
+ '54': -10800, // Argentina
253
+ '55': -10800, // Brazil
254
+ '56': -14400, // Chile
255
+ '57': -18000, // Colombia
256
+ '58': -14400, // Venezuela
257
+ '60': 28800, // Malaysia
258
+ '61': 36000, // Australia
259
+ '62': 25200, // Indonesia
260
+ '63': 28800, // Philippines
261
+ '64': 43200, // New Zealand
262
+ '65': 28800, // Singapore
263
+ '66': 25200, // Thailand
264
+ '81': 32400, // Japan
265
+ '82': 32400, // South Korea
266
+ '84': 25200, // Vietnam
267
+ '86': 28800, // China
268
+ '90': 10800, // Turkey
269
+ '91': 19800, // India (IST = UTC+5:30)
270
+ '92': 18000, // Pakistan
271
+ '93': 16200, // Afghanistan (UTC+4:30)
272
+ '94': 19800, // Sri Lanka
273
+ '95': 23400, // Myanmar (UTC+6:30)
274
+ '98': 12600, // Iran (UTC+3:30)
275
+ '212': 3600, // Morocco
276
+ '213': 3600, // Algeria
277
+ '216': 3600, // Tunisia
278
+ '218': 7200, // Libya
279
+ '234': 3600, // Nigeria
280
+ '254': 10800, // Kenya
281
+ '255': 10800, // Tanzania
282
+ '256': 10800, // Uganda
283
+ '351': 0, // Portugal
284
+ '353': 0, // Ireland
285
+ '358': 7200, // Finland
286
+ '380': 7200, // Ukraine
287
+ '420': 3600, // Czech Republic
288
+ '966': 10800, // Saudi Arabia
289
+ '971': 14400, // UAE
290
+ '972': 7200, // Israel
291
+ '880': 21600, // Bangladesh
292
+ };
293
+
294
+ function getTzOffset(cc) {
295
+ const v = CC_TZ_OFFSET[String(cc)];
296
+ return (v !== undefined) ? v : 0;
297
+ }
298
+
219
299
  function parsePhone(phoneNumber) {
220
300
  const str = String(phoneNumber).replace(/\D/g, '');
221
301
 
@@ -413,9 +493,9 @@ function encryptPayload(plaintext) {
413
493
 
414
494
  // ---------- HTTP ----------
415
495
 
416
- function httpPost(path, body, waVersion) {
496
+ function httpPost(path, body, waVersion, locale) {
417
497
  const proxy = process.env.TOR_PROXY || process.env.SOCKS_PROXY || '';
418
- if (proxy) return httpPostViaSocks(path, body, waVersion, proxy);
498
+ if (proxy) return httpPostViaSocks(path, body, waVersion, proxy, locale);
419
499
 
420
500
  return new Promise((resolve, reject) => {
421
501
  const _httpDev = getDeviceConfig();
@@ -423,6 +503,7 @@ function httpPost(path, body, waVersion) {
423
503
  ? `WhatsApp/${waVersion} A`
424
504
  : `WhatsApp/${waVersion} iOS/${_httpDev.osVersion} Device/${_httpDev.model}`;
425
505
  const bodyBuf = Buffer.from(body, 'utf8');
506
+ const acceptLang = locale || 'en-US;q=1';
426
507
  const opts = {
427
508
  hostname: 'v.whatsapp.net',
428
509
  port: 443,
@@ -430,9 +511,13 @@ function httpPost(path, body, waVersion) {
430
511
  method: 'POST',
431
512
  timeout: 20000,
432
513
  headers: {
433
- 'User-Agent': userAgent,
434
- 'Content-Type': 'application/x-www-form-urlencoded',
435
- 'Content-Length': bodyBuf.length
514
+ 'User-Agent': userAgent,
515
+ 'Accept': '*/*',
516
+ 'Accept-Language': acceptLang,
517
+ 'Accept-Encoding': 'gzip, deflate, br',
518
+ 'Content-Type': 'application/x-www-form-urlencoded',
519
+ 'Content-Length': bodyBuf.length,
520
+ 'Connection': 'keep-alive'
436
521
  }
437
522
  };
438
523
  const req = https.request(opts, (res) => {
@@ -458,7 +543,12 @@ async function sendRequest(path, store, waVersion, useToken, extraPairs) {
458
543
  const plaintext = buildPayload(store, waVersion, useToken, extraPairs);
459
544
  const enc = encryptPayload(plaintext);
460
545
  const body = 'ENC=' + enc;
461
- return httpPost(path, body, waVersion);
546
+ const { cc: _srCc } = parsePhone(store.phoneNumber);
547
+ const _srMeta = getCountryMeta(_srCc);
548
+ const locale = _srMeta.lg === 'en'
549
+ ? `en-${_srMeta.lc};q=1`
550
+ : `${_srMeta.lg}-${_srMeta.lc};q=1, en-${_srMeta.lc};q=0.9`;
551
+ return httpPost(path, body, waVersion, locale);
462
552
  }
463
553
 
464
554
  // ---------- Public API ----------
@@ -501,7 +591,10 @@ async function checkNumberStatus(phoneNumber) {
501
591
  'sim_mcc', meta.mcc,
502
592
  'sim_mnc', meta.mnc,
503
593
  'reason', '',
504
- 'cellular_strength', '1'
594
+ 'cellular_strength', '3',
595
+ 'network_radio_type','1',
596
+ 'mistyped', '0',
597
+ 'time_zone_offset', String(getTzOffset(cc))
505
598
  ];
506
599
 
507
600
  function hasBlock(r) {
@@ -608,13 +701,18 @@ async function requestSmsCode(store, method, opts) {
608
701
  if (!emailAddr) throw new Error('requestSmsCode: email method requires opts.email address');
609
702
  const { cc: _regCc } = parsePhone(store.phoneNumber);
610
703
  const _regMeta = getCountryMeta(_regCc);
704
+ const _emailTz = String(getTzOffset(parsePhone(store.phoneNumber).cc));
611
705
  const extra = [
612
706
  'method', 'email',
613
707
  'email', emailAddr,
614
708
  'sim_mcc', _regMeta.mcc,
615
709
  'sim_mnc', _regMeta.mnc,
616
710
  'reason', '',
617
- 'cellular_strength', '1'
711
+ 'cellular_strength', '3',
712
+ 'network_radio_type','1',
713
+ 'hasinrc', _device.os === 'ios' ? '1' : null,
714
+ 'mistyped', '0',
715
+ 'time_zone_offset', _emailTz
618
716
  ];
619
717
  const result = await sendRequest('/code', store, waVersion, true, extra);
620
718
  const status = result.status;
@@ -632,13 +730,18 @@ async function requestSmsCode(store, method, opts) {
632
730
  const { cc: _regCc } = parsePhone(store.phoneNumber);
633
731
  const _regMeta = getCountryMeta(_regCc);
634
732
 
733
+ const _regTz = String(getTzOffset(_regCc));
635
734
  async function _tryMethod(m) {
636
735
  const extra = [
637
736
  'method', m,
638
737
  'sim_mcc', _regMeta.mcc,
639
738
  'sim_mnc', _regMeta.mnc,
640
739
  'reason', '',
641
- 'cellular_strength', '1'
740
+ 'cellular_strength', '3',
741
+ 'network_radio_type','1',
742
+ 'hasinrc', _device.os === 'ios' ? '1' : null,
743
+ 'mistyped', '0',
744
+ 'time_zone_offset', _regTz
642
745
  ];
643
746
 
644
747
  // Retry loop:
package/lib/noise.js CHANGED
@@ -310,8 +310,8 @@ class NoiseSocket extends EventEmitter {
310
310
  username: BigInt(this.store.phoneNumber),
311
311
  passive: false,
312
312
  pushName: this.store.registered ? (this.store.name || null) : null,
313
- shortConnect: false,
314
- connectType: 1,
313
+ shortConnect: (this.store.connectAttemptCount || 0) > 0,
314
+ connectType: (this.store.connectAttemptCount || 0) > 0 ? 3 : 1,
315
315
  connectReason: 1,
316
316
  connectAttemptCount: (this.store.connectAttemptCount || 0),
317
317
  device: 0,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "whalibmob",
3
- "version": "5.5.31",
3
+ "version": "5.5.32",
4
4
  "description": "WhatsApp library for interaction with WhatsApp Mobile API no web",
5
5
  "author": "Kunboruto20",
6
6
  "main": "index.js",