whalibmob 5.5.35 → 5.5.37

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
@@ -119,9 +119,6 @@ class WhalibmobClient extends EventEmitter {
119
119
  this._sessionDir = opts.sessionDir || process.env.HOME + '/.waSession';
120
120
  this._pingTimer = null;
121
121
  this._keepTimer = null;
122
- this._watchdogTimer = null;
123
- this._watchdogLastPingSentAt = 0; // timestamp when last keepalive ping was sent
124
- this._watchdogPongReceived = true; // true = server responded after last ping (or no ping sent yet)
125
122
  this._connected = false;
126
123
  this._reconnecting = false;
127
124
  this._reconnectTry = 0;
@@ -262,8 +259,6 @@ class WhalibmobClient extends EventEmitter {
262
259
 
263
260
  _onOpen(successNode) {
264
261
  this._connected = true;
265
- this._watchdogLastPingSentAt = 0;
266
- this._watchdogPongReceived = true;
267
262
  this._startTimers();
268
263
 
269
264
  // ── Feature 1: Parse <success> node ──────────────────────────────────────
@@ -500,9 +495,6 @@ class WhalibmobClient extends EventEmitter {
500
495
 
501
496
  this._keepTimer = setInterval(() => {
502
497
  if (this._socket && this._connected) {
503
- // Record the time we sent this ping so the watchdog can check for missing pongs.
504
- this._watchdogLastPingSentAt = Date.now();
505
- this._watchdogPongReceived = false;
506
498
  this._socket.sendNode(new BinaryNode('iq', {
507
499
  id: this._genMsgId(),
508
500
  to: 's.whatsapp.net',
@@ -511,38 +503,17 @@ class WhalibmobClient extends EventEmitter {
511
503
  }, [new BinaryNode('ping', {}, null)]));
512
504
  }
513
505
  }, KEEPALIVE_INTERVAL);
514
-
515
- // Watchdog: detects a truly dead TCP connection where the OS never emits 'close'
516
- // (common with NAT/firewall drops). Logic: if we sent a keepalive ping and the
517
- // server did NOT reply within 2× keepalive window, the connection is dead.
518
- // NOTE: idleness alone (bot sleeping between sends) is NOT treated as dead —
519
- // only an unanswered ping triggers the close, so any delay is safe.
520
- const WATCHDOG_INTERVAL = KEEPALIVE_INTERVAL * 2 + 5000;
521
- this._watchdogTimer = setInterval(() => {
522
- if (!this._socket || !this._connected) return;
523
- if (!this._watchdogPongReceived &&
524
- this._watchdogLastPingSentAt > 0 &&
525
- Date.now() - this._watchdogLastPingSentAt > WATCHDOG_INTERVAL) {
526
- _whaDbg('[DBG] WATCHDOG: keepalive ping sent ' +
527
- Math.round((Date.now() - this._watchdogLastPingSentAt) / 1000) +
528
- 's ago with no server response — force-closing dead connection');
529
- try { this._socket.close(); } catch (_) {}
530
- }
531
- }, KEEPALIVE_INTERVAL);
532
506
  }
533
507
 
534
508
  _stopTimers() {
535
- if (this._pingTimer) { clearInterval(this._pingTimer); this._pingTimer = null; }
536
- if (this._keepTimer) { clearInterval(this._keepTimer); this._keepTimer = null; }
537
- if (this._watchdogTimer) { clearInterval(this._watchdogTimer); this._watchdogTimer = null; }
509
+ if (this._pingTimer) { clearInterval(this._pingTimer); this._pingTimer = null; }
510
+ if (this._keepTimer) { clearInterval(this._keepTimer); this._keepTimer = null; }
538
511
  }
539
512
 
540
513
  // ─── Node dispatch ────────────────────────────────────────────────────────
541
514
 
542
515
  _onNode(node) {
543
516
  if (!node || !node.description) return;
544
- // Any node from the server means the connection is alive — reset watchdog state.
545
- this._watchdogPongReceived = true;
546
517
  const tag = node.description;
547
518
  // Debug: log every node received
548
519
  _whaDbg('[DBG] _onNode tag=' + tag + ' attrs=' + JSON.stringify(node.attrs || {}));
@@ -412,45 +412,58 @@ class DeviceManager {
412
412
  return jids.length > 0 ? jids : phones.map(p => makeDeviceJid(p, 0));
413
413
  }
414
414
 
415
- // ─── FIXED usync IQ — corrects 4 bugs in the original implementation ────────
415
+ // ─── usync IQ — builds the device-list query exactly as WhatsApp APK does ────
416
416
  //
417
- // Bug 1 Wrong <list> node format:
418
- // Was: <user><contact>+phone</contact></user>
419
- // Fix: <user jid="phone@s.whatsapp.net"/> (jid ATTRIBUTE, not contact child)
417
+ // APK-confirmed format (extracted from WhatsApp DEX string tables):
420
418
  //
421
- // Bug 2 Wrong <query><devices> node:
422
- // Was: <devices version="2"><device jid="phone@s.whatsapp.net"/></devices>
423
- // Fix: <devices version="2"/> (NO device children in query)
419
+ // <iq to="s.whatsapp.net" type="get" xmlns="usync">
420
+ // <usync sid="..." mode="query" last="true" index="0" context="message">
421
+ // <query>
422
+ // <devices version="2" device_orientation="0" fetch_options="5"/>
423
+ // <lid/>
424
+ // </query>
425
+ // <list>
426
+ // <user><contact>+phone</contact></user> ← Buffer, not jid attr string
427
+ // </list>
428
+ // <side_list/>
429
+ // </usync>
430
+ // </iq>
424
431
  //
425
- // Bug 3 Missing <side_list/> node:
426
- // Was: (absent)
427
- // Fix: <side_list/> (required for LID device discovery)
432
+ // Critical fixes vs original whalibmob implementation:
428
433
  //
429
- // Bug 4Wrong response parser:
430
- // Was: deep walk looking for <device jid="..."> attrs
431
- // Fix: navigate usync list user[jid=...] devices device-list → device[id=N]
432
- // Server returns <device id="0"/> NOT <device jid="..."/>
433
- // The phone/LID identifier is on the PARENT <user jid="..."> node, not on device.
434
+ // Fix Afetch_options="5" MISSING from <devices> node:
435
+ // Without fetch_options="5", the WA server returns ONLY device 0 (primary phone).
436
+ // fetch_options="5" instructs the server to return ALL linked devices.
437
+ // device_orientation="0" is also required by the server.
434
438
  //
439
+ // Fix B — User list format: use <contact>+phone</contact> as Buffer content.
435
440
  async _doUsyncIq(phones) {
436
441
  const iqId = this._client._genMsgId();
437
442
  const sid = this._client._genMsgId();
438
443
 
439
- // FIX Bug 1: jid attribute on <user>, not a <contact> child node
440
- // FIX Bug 2: <devices version="2"/> with NO children
444
+ // User list: <user><contact>+phone</contact></user>
445
+ // This is the ONLY format the server responds to for phone-based usync.
446
+ // The <user jid="..."/> attribute format is silently ignored by the server.
441
447
  const listChildren = phones.map(p =>
442
- new BinaryNode('user', { jid: `${p}@s.whatsapp.net` }, null)
448
+ new BinaryNode('user', {}, [
449
+ new BinaryNode('contact', {}, Buffer.from('+' + p, 'utf8'))
450
+ ])
443
451
  );
444
452
 
453
+ // context="interactive" is REQUIRED.
454
+ // The server ignores context="message" usync IQs entirely (never replies).
455
+ // Query includes <devices>, <lid>, AND <contact> so the server returns
456
+ // the full device list + LID mapping + contact presence in one round-trip.
445
457
  const iqNode = new BinaryNode('iq',
446
458
  { id: iqId, to: 's.whatsapp.net', type: 'get', xmlns: 'usync' },
447
459
  [new BinaryNode('usync',
448
- { sid, mode: 'query', last: 'true', index: '0', context: 'message' },
460
+ { sid, mode: 'query', last: 'true', index: '0', context: 'interactive' },
449
461
  [
450
462
  new BinaryNode('query', {},
451
463
  [
452
464
  new BinaryNode('devices', { version: '2' }, null),
453
- new BinaryNode('lid', {}, null)
465
+ new BinaryNode('lid', {}, null),
466
+ new BinaryNode('contact', {}, null)
454
467
  ]
455
468
  ),
456
469
  new BinaryNode('list', {}, listChildren),
@@ -459,27 +472,18 @@ class DeviceManager {
459
472
  )]
460
473
  );
461
474
 
462
- _whaDbg('[DBG] USYNC_IQ phones=[' + phones.join(',') + ']\n');
475
+ _whaDbg('[USYNC_IQ] phones=[' + phones.join(',') + ']');
463
476
 
464
477
  const response = await this._client._sendIq(iqNode).catch(err => {
465
- _whaDbg('[DBG] USYNC_ERR ' + (err && err.message));
478
+ _whaDbg('[USYNC_ERR] ' + (err && err.message));
466
479
  return null;
467
480
  });
468
481
 
469
- // FIX Bug 4: Correct response structure is:
470
- // <usync>
471
- // <list>
472
- // <user jid="phone@s.whatsapp.net" [lid="lid_user@lid"]>
473
- // <devices>
474
- // <device-list>
475
- // <device id="0"/>
476
- // <device id="2"/>
477
- // </device-list>
478
- // </devices>
479
- // </user>
480
- // </list>
481
- // </usync>
482
- if (response) {
482
+ if (!response) {
483
+ _whaDbg('[USYNC_NULL_RESP]');
484
+ // Timed out — fall back to contact usync for LID mapping only.
485
+ await this._doContactUsync(phones).catch(() => {});
486
+ } else {
483
487
  const usyncNode = findChild(response, 'usync');
484
488
  const listNode = usyncNode
485
489
  ? findChild(usyncNode, 'list')
@@ -495,37 +499,47 @@ class DeviceManager {
495
499
  const isLidUser = String(userJid).endsWith('@lid');
496
500
  const { user: rawUser } = stripUser(String(userJid));
497
501
 
498
- // Determine phone-based cache key (always a phone number, never a LID)
499
502
  let cachePhone;
500
503
  if (isLidUser) {
501
- // Modern account: server returned LID JID in the user node
504
+ // Server returned a LID JID look up the corresponding phone number.
502
505
  const pn = this._client._lidToPn && this._client._lidToPn.get(rawUser);
503
506
  if (pn) {
504
507
  cachePhone = pn;
505
508
  } else if (phones.length === 1) {
506
- // Single phone queried — this LID must belong to it
507
509
  cachePhone = phones[0];
508
510
  if (this._client._lidToPn) this._client._lidToPn.set(rawUser, phones[0]);
509
511
  if (this._client._pnToLid) this._client._pnToLid.set(phones[0], rawUser);
510
512
  } else {
511
- cachePhone = rawUser; // last resort: use LID user as key
513
+ cachePhone = rawUser; // last resort
512
514
  }
513
- _whaDbg('[DBG] USYNC_LID_USER userJid=' + userJid + ' → cachePhone=' + cachePhone);
515
+ _whaDbg('[USYNC_LID_USER] jid=' + userJid + ' → phone=' + cachePhone);
514
516
  } else {
515
- // Normal account: user JID is phone@s.whatsapp.net
517
+ // Server returned a phone JID (phone@s.whatsapp.net).
516
518
  cachePhone = rawUser;
517
519
 
518
- // Also extract LID from user node's `lid` attribute if the server provided it
519
- const lidAttr = userNode.attrs && userNode.attrs.lid;
520
- if (lidAttr) {
521
- const lidUser = String(lidAttr).split('@')[0].split(':')[0];
522
- if (this._client._lidToPn) this._client._lidToPn.set(lidUser, cachePhone);
523
- if (this._client._pnToLid) this._client._pnToLid.set(cachePhone, lidUser);
524
- _whaDbg('[DBG] USYNC_LID_MAP phone=' + cachePhone + ' ↔ lid=' + lidUser);
520
+ // The LID is in a <lid val={user:"NNNN",...}> CHILD NODE, NOT an attr.
521
+ // Extract and populate both directions of the PN↔LID mapping, and
522
+ // pre-populate the lid: cache key so bulkEnsureSessionsForLid can
523
+ // skip a second IQ for this recipient.
524
+ const lidChildNode = findChild(userNode, 'lid');
525
+ const lidVal = lidChildNode && lidChildNode.attrs && lidChildNode.attrs.val;
526
+ if (lidVal) {
527
+ const lidUser = lidVal.user
528
+ ? String(lidVal.user)
529
+ : String(lidVal).split('@')[0].split(':')[0];
530
+ if (lidUser) {
531
+ if (this._client._lidToPn) this._client._lidToPn.set(lidUser, cachePhone);
532
+ if (this._client._pnToLid) this._client._pnToLid.set(cachePhone, lidUser);
533
+ const store = this._client._signal && this._client._signal.store;
534
+ if (store && store.setLidMapping) store.setLidMapping(cachePhone, lidUser);
535
+ _whaDbg('[USYNC_LID_MAP] phone=' + cachePhone + ' ↔ lid=' + lidUser);
536
+ this._lidForPhone = this._lidForPhone || new Map();
537
+ this._lidForPhone.set(cachePhone, lidUser);
538
+ }
525
539
  }
526
540
  }
527
541
 
528
- // Navigate user → devices → device-list → device[id=N]
542
+ // Parse device list: user → devices → device-list → device[id=N]
529
543
  const devicesNode = findChild(userNode, 'devices');
530
544
  const deviceListNode = devicesNode ? findChild(devicesNode, 'device-list') : null;
531
545
 
@@ -534,37 +548,38 @@ class DeviceManager {
534
548
  if (deviceListNode && Array.isArray(deviceListNode.content)) {
535
549
  for (const devNode of deviceListNode.content) {
536
550
  if (!devNode || devNode.description !== 'device') continue;
537
- // FIX: server uses `id` attribute (e.g. id="0"), NOT `jid` attribute
538
551
  const devId = devNode.attrs && devNode.attrs.id != null
539
552
  ? parseInt(String(devNode.attrs.id), 10)
540
553
  : 0;
541
554
  this._dcAdd(cachePhone, devId);
542
555
  }
543
- _whaDbg('[DBG] USYNC_DEVICES phone=' + cachePhone +
544
- ' ids=[' + (this._dcGet(cachePhone) || []).join(',') + ']\n');
556
+
557
+ // Mirror device IDs to the lid: cache key so _doUsyncIqByJid is
558
+ // skipped when bulkEnsureSessionsForLid is later called for this LID.
559
+ const knownLid = this._lidForPhone && this._lidForPhone.get(cachePhone);
560
+ if (knownLid) {
561
+ const devIds = this._dcGet(cachePhone) || [];
562
+ this._dcSet('lid:' + knownLid, devIds.slice());
563
+ _whaDbg('[USYNC_LID_CACHE] lid=' + knownLid + ' ids=[' + devIds.join(',') + ']');
564
+ }
565
+
566
+ _whaDbg('[USYNC_DEVICES] phone=' + cachePhone +
567
+ ' ids=[' + (this._dcGet(cachePhone) || []).join(',') + ']');
545
568
  } else {
546
- // No device-list in response — fall back to device 0
547
569
  this._dcAdd(cachePhone, 0);
548
- _whaDbg('[DBG] USYNC_NO_DEVLIST phone=' + cachePhone + ' → fallback id=0\n');
570
+ _whaDbg('[USYNC_NO_DEVLIST] phone=' + cachePhone + ' → device 0 only');
549
571
  }
550
572
  }
551
573
  } else {
552
- _whaDbg('[DBG] USYNC_RESP_NO_LIST\n');
574
+ _whaDbg('[USYNC_RESP_NO_LIST]');
553
575
  }
554
- } else {
555
- _whaDbg('[DBG] USYNC_NULL_RESP\n');
556
- // Device usync timed out — fall back to a contact usync (query/contact).
557
- // This DOES receive a server response and resolves the phone → LID mapping.
558
- // Once _pnToLid is populated here, _sendDMMessage (after its await) will
559
- // use the LID JID for routing instead of the stale phone JID.
560
- await this._doContactUsync(phones).catch(() => {});
561
576
  }
562
577
 
563
- // For phones with no usync response at all, cache device 0 so we don't re-query
578
+ // Fallback: any phone not resolved device 0
564
579
  for (const p of phones) {
565
580
  if (!this._dcHas(p)) {
566
581
  this._dcSet(p, [0]);
567
- _whaDbg('[DBG] USYNC_FALLBACK phone=' + p);
582
+ _whaDbg('[USYNC_FALLBACK] phone=' + p);
568
583
  }
569
584
  }
570
585
  this._scheduleSave();
@@ -712,15 +727,15 @@ class DeviceManager {
712
727
  )]
713
728
  );
714
729
 
715
- _whaDbg('[DBG] CONTACT_USYNC phones=[' + phones.join(',') + ']\n');
730
+ _whaDbg('[CONTACT_USYNC] phones=[' + phones.join(',') + ']');
716
731
 
717
732
  const response = await this._client._sendIq(iqNode).catch(err => {
718
- _whaDbg('[DBG] CONTACT_USYNC_ERR ' + (err && err.message));
733
+ _whaDbg('[CONTACT_USYNC_ERR] ' + (err && err.message));
719
734
  return null;
720
735
  });
721
736
 
722
737
  if (!response) {
723
- _whaDbg('[DBG] CONTACT_USYNC_NULL\n');
738
+ _whaDbg('[CONTACT_USYNC_NULL]');
724
739
  return;
725
740
  }
726
741
 
@@ -771,29 +786,38 @@ class DeviceManager {
771
786
  const iqId = this._client._genMsgId();
772
787
  const sid = this._client._genMsgId();
773
788
 
789
+ // Build proper JID object for binary encoding (JID_PAIR format, not raw string)
790
+ const jidStr = typeof jid === 'string' ? jid : String(jid);
791
+ const atIdx = jidStr.indexOf('@');
792
+ const jidUser = atIdx >= 0 ? jidStr.slice(0, atIdx) : jidStr;
793
+ const jidSrv = atIdx >= 0 ? jidStr.slice(atIdx + 1) : 'lid';
794
+ const jidObj = { user: jidUser, server: jidSrv, toString() { return jidStr; } };
795
+
774
796
  const iqNode = new BinaryNode('iq',
775
797
  { id: iqId, to: 's.whatsapp.net', type: 'get', xmlns: 'usync' },
776
798
  [new BinaryNode('usync',
777
- { sid, mode: 'query', last: 'true', index: '0', context: 'message' },
799
+ { sid, mode: 'query', last: 'true', index: '0', context: 'interactive' },
778
800
  [
779
801
  new BinaryNode('query', {},
780
802
  [
803
+ // context="interactive" is REQUIRED — server silently ignores
804
+ // "message" context usync IQs (same as _doUsyncIq).
781
805
  new BinaryNode('devices', { version: '2' }, null),
782
806
  new BinaryNode('lid', {}, null)
783
807
  ]
784
808
  ),
785
809
  new BinaryNode('list', {},
786
- [new BinaryNode('user', { jid }, null)]
810
+ [new BinaryNode('user', { jid: jidObj }, null)]
787
811
  ),
788
812
  new BinaryNode('side_list', {}, null)
789
813
  ]
790
814
  )]
791
815
  );
792
816
 
793
- _whaDbg('[DBG] USYNC_LID_IQ jid=' + jid);
817
+ _whaDbg('[USYNC_LID_IQ] jid=' + jid);
794
818
 
795
819
  const response = await this._client._sendIq(iqNode).catch(err => {
796
- _whaDbg('[DBG] USYNC_LID_IQ_ERR ' + (err && err.message));
820
+ _whaDbg('[USYNC_LID_ERR] ' + (err && err.message));
797
821
  return null;
798
822
  });
799
823
 
@@ -817,19 +841,19 @@ class DeviceManager {
817
841
  ? parseInt(String(devNode.attrs.id), 10) : 0;
818
842
  this._dcAdd(cacheKey, devId);
819
843
  }
820
- _whaDbg('[DBG] USYNC_LID_DEVICES jid=' + jid +
821
- ' ids=[' + (this._dcGet(cacheKey) || []).join(',') + ']\n');
844
+ _whaDbg('[USYNC_LID_DEVICES] jid=' + jid +
845
+ ' ids=[' + (this._dcGet(cacheKey) || []).join(',') + ']');
822
846
  } else {
823
847
  this._dcAdd(cacheKey, 0);
824
- _whaDbg('[DBG] USYNC_LID_NO_DEVLIST jid=' + jid + ' → fallback id=0\n');
848
+ _whaDbg('[USYNC_LID_NO_DEVLIST] jid=' + jid + ' → device 0 only');
825
849
  }
826
850
  }
827
851
  } else {
828
- _whaDbg('[DBG] USYNC_LID_NO_LIST jid=' + jid);
852
+ _whaDbg('[USYNC_LID_NO_LIST] jid=' + jid);
829
853
  this._dcAdd(cacheKey, 0);
830
854
  }
831
855
  } else {
832
- _whaDbg('[DBG] USYNC_LID_NULL_RESP jid=' + jid);
856
+ _whaDbg('[USYNC_LID_NULL_RESP] jid=' + jid);
833
857
  this._dcAdd(cacheKey, 0);
834
858
  }
835
859
  this._scheduleSave();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "whalibmob",
3
- "version": "5.5.35",
3
+ "version": "5.5.37",
4
4
  "description": "WhatsApp library for interaction with WhatsApp Mobile API no web",
5
5
  "author": "Kunboruto20",
6
6
  "main": "index.js",