whalibmob 5.5.36 → 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 || {}));
@@ -437,33 +437,33 @@ class DeviceManager {
437
437
  // device_orientation="0" is also required by the server.
438
438
  //
439
439
  // Fix B — User list format: use <contact>+phone</contact> as Buffer content.
440
- // The BinaryNode encoder encodes string attributes as raw UTF-8 (BINARY_8 tag),
441
- // NOT as JID_PAIR binary format. The APK sends phone as Buffer content of a
442
- // <contact> child node — this is what the server actually expects in the list.
443
- //
444
440
  async _doUsyncIq(phones) {
445
441
  const iqId = this._client._genMsgId();
446
442
  const sid = this._client._genMsgId();
447
443
 
448
- // Fix B: <user><contact>+phone</contact></user> — matches APK exactly.
449
- // Buffer.from ensures binary encoding (not raw UTF-8 string attribute).
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.
450
447
  const listChildren = phones.map(p =>
451
448
  new BinaryNode('user', {}, [
452
449
  new BinaryNode('contact', {}, Buffer.from('+' + p, 'utf8'))
453
450
  ])
454
451
  );
455
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.
456
457
  const iqNode = new BinaryNode('iq',
457
458
  { id: iqId, to: 's.whatsapp.net', type: 'get', xmlns: 'usync' },
458
459
  [new BinaryNode('usync',
459
- { sid, mode: 'query', last: 'true', index: '0', context: 'message' },
460
+ { sid, mode: 'query', last: 'true', index: '0', context: 'interactive' },
460
461
  [
461
462
  new BinaryNode('query', {},
462
463
  [
463
- // Fix A: fetch_options="5" + device_orientation="0" — required for
464
- // the server to return ALL linked devices, not just device 0.
465
- new BinaryNode('devices', { version: '2', device_orientation: '0', fetch_options: '5' }, null),
466
- new BinaryNode('lid', {}, null)
464
+ new BinaryNode('devices', { version: '2' }, null),
465
+ new BinaryNode('lid', {}, null),
466
+ new BinaryNode('contact', {}, null)
467
467
  ]
468
468
  ),
469
469
  new BinaryNode('list', {}, listChildren),
@@ -472,27 +472,18 @@ class DeviceManager {
472
472
  )]
473
473
  );
474
474
 
475
- _whaDbg('[DBG] USYNC_IQ phones=[' + phones.join(',') + ']\n');
475
+ _whaDbg('[USYNC_IQ] phones=[' + phones.join(',') + ']');
476
476
 
477
477
  const response = await this._client._sendIq(iqNode).catch(err => {
478
- _whaDbg('[DBG] USYNC_ERR ' + (err && err.message));
478
+ _whaDbg('[USYNC_ERR] ' + (err && err.message));
479
479
  return null;
480
480
  });
481
481
 
482
- // FIX Bug 4: Correct response structure is:
483
- // <usync>
484
- // <list>
485
- // <user jid="phone@s.whatsapp.net" [lid="lid_user@lid"]>
486
- // <devices>
487
- // <device-list>
488
- // <device id="0"/>
489
- // <device id="2"/>
490
- // </device-list>
491
- // </devices>
492
- // </user>
493
- // </list>
494
- // </usync>
495
- 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 {
496
487
  const usyncNode = findChild(response, 'usync');
497
488
  const listNode = usyncNode
498
489
  ? findChild(usyncNode, 'list')
@@ -508,37 +499,47 @@ class DeviceManager {
508
499
  const isLidUser = String(userJid).endsWith('@lid');
509
500
  const { user: rawUser } = stripUser(String(userJid));
510
501
 
511
- // Determine phone-based cache key (always a phone number, never a LID)
512
502
  let cachePhone;
513
503
  if (isLidUser) {
514
- // Modern account: server returned LID JID in the user node
504
+ // Server returned a LID JID look up the corresponding phone number.
515
505
  const pn = this._client._lidToPn && this._client._lidToPn.get(rawUser);
516
506
  if (pn) {
517
507
  cachePhone = pn;
518
508
  } else if (phones.length === 1) {
519
- // Single phone queried — this LID must belong to it
520
509
  cachePhone = phones[0];
521
510
  if (this._client._lidToPn) this._client._lidToPn.set(rawUser, phones[0]);
522
511
  if (this._client._pnToLid) this._client._pnToLid.set(phones[0], rawUser);
523
512
  } else {
524
- cachePhone = rawUser; // last resort: use LID user as key
513
+ cachePhone = rawUser; // last resort
525
514
  }
526
- _whaDbg('[DBG] USYNC_LID_USER userJid=' + userJid + ' → cachePhone=' + cachePhone);
515
+ _whaDbg('[USYNC_LID_USER] jid=' + userJid + ' → phone=' + cachePhone);
527
516
  } else {
528
- // Normal account: user JID is phone@s.whatsapp.net
517
+ // Server returned a phone JID (phone@s.whatsapp.net).
529
518
  cachePhone = rawUser;
530
519
 
531
- // Also extract LID from user node's `lid` attribute if the server provided it
532
- const lidAttr = userNode.attrs && userNode.attrs.lid;
533
- if (lidAttr) {
534
- const lidUser = String(lidAttr).split('@')[0].split(':')[0];
535
- if (this._client._lidToPn) this._client._lidToPn.set(lidUser, cachePhone);
536
- if (this._client._pnToLid) this._client._pnToLid.set(cachePhone, lidUser);
537
- _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
+ }
538
539
  }
539
540
  }
540
541
 
541
- // Navigate user → devices → device-list → device[id=N]
542
+ // Parse device list: user → devices → device-list → device[id=N]
542
543
  const devicesNode = findChild(userNode, 'devices');
543
544
  const deviceListNode = devicesNode ? findChild(devicesNode, 'device-list') : null;
544
545
 
@@ -547,37 +548,38 @@ class DeviceManager {
547
548
  if (deviceListNode && Array.isArray(deviceListNode.content)) {
548
549
  for (const devNode of deviceListNode.content) {
549
550
  if (!devNode || devNode.description !== 'device') continue;
550
- // FIX: server uses `id` attribute (e.g. id="0"), NOT `jid` attribute
551
551
  const devId = devNode.attrs && devNode.attrs.id != null
552
552
  ? parseInt(String(devNode.attrs.id), 10)
553
553
  : 0;
554
554
  this._dcAdd(cachePhone, devId);
555
555
  }
556
- _whaDbg('[DBG] USYNC_DEVICES phone=' + cachePhone +
557
- ' 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(',') + ']');
558
568
  } else {
559
- // No device-list in response — fall back to device 0
560
569
  this._dcAdd(cachePhone, 0);
561
- _whaDbg('[DBG] USYNC_NO_DEVLIST phone=' + cachePhone + ' → fallback id=0\n');
570
+ _whaDbg('[USYNC_NO_DEVLIST] phone=' + cachePhone + ' → device 0 only');
562
571
  }
563
572
  }
564
573
  } else {
565
- _whaDbg('[DBG] USYNC_RESP_NO_LIST\n');
574
+ _whaDbg('[USYNC_RESP_NO_LIST]');
566
575
  }
567
- } else {
568
- _whaDbg('[DBG] USYNC_NULL_RESP\n');
569
- // Device usync timed out — fall back to a contact usync (query/contact).
570
- // This DOES receive a server response and resolves the phone → LID mapping.
571
- // Once _pnToLid is populated here, _sendDMMessage (after its await) will
572
- // use the LID JID for routing instead of the stale phone JID.
573
- await this._doContactUsync(phones).catch(() => {});
574
576
  }
575
577
 
576
- // 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
577
579
  for (const p of phones) {
578
580
  if (!this._dcHas(p)) {
579
581
  this._dcSet(p, [0]);
580
- _whaDbg('[DBG] USYNC_FALLBACK phone=' + p);
582
+ _whaDbg('[USYNC_FALLBACK] phone=' + p);
581
583
  }
582
584
  }
583
585
  this._scheduleSave();
@@ -725,15 +727,15 @@ class DeviceManager {
725
727
  )]
726
728
  );
727
729
 
728
- _whaDbg('[DBG] CONTACT_USYNC phones=[' + phones.join(',') + ']\n');
730
+ _whaDbg('[CONTACT_USYNC] phones=[' + phones.join(',') + ']');
729
731
 
730
732
  const response = await this._client._sendIq(iqNode).catch(err => {
731
- _whaDbg('[DBG] CONTACT_USYNC_ERR ' + (err && err.message));
733
+ _whaDbg('[CONTACT_USYNC_ERR] ' + (err && err.message));
732
734
  return null;
733
735
  });
734
736
 
735
737
  if (!response) {
736
- _whaDbg('[DBG] CONTACT_USYNC_NULL\n');
738
+ _whaDbg('[CONTACT_USYNC_NULL]');
737
739
  return;
738
740
  }
739
741
 
@@ -794,12 +796,13 @@ class DeviceManager {
794
796
  const iqNode = new BinaryNode('iq',
795
797
  { id: iqId, to: 's.whatsapp.net', type: 'get', xmlns: 'usync' },
796
798
  [new BinaryNode('usync',
797
- { sid, mode: 'query', last: 'true', index: '0', context: 'message' },
799
+ { sid, mode: 'query', last: 'true', index: '0', context: 'interactive' },
798
800
  [
799
801
  new BinaryNode('query', {},
800
802
  [
801
- // Fix A applied here too: fetch_options="5" + device_orientation="0"
802
- new BinaryNode('devices', { version: '2', device_orientation: '0', fetch_options: '5' }, null),
803
+ // context="interactive" is REQUIRED server silently ignores
804
+ // "message" context usync IQs (same as _doUsyncIq).
805
+ new BinaryNode('devices', { version: '2' }, null),
803
806
  new BinaryNode('lid', {}, null)
804
807
  ]
805
808
  ),
@@ -811,10 +814,10 @@ class DeviceManager {
811
814
  )]
812
815
  );
813
816
 
814
- _whaDbg('[DBG] USYNC_LID_IQ jid=' + jid);
817
+ _whaDbg('[USYNC_LID_IQ] jid=' + jid);
815
818
 
816
819
  const response = await this._client._sendIq(iqNode).catch(err => {
817
- _whaDbg('[DBG] USYNC_LID_IQ_ERR ' + (err && err.message));
820
+ _whaDbg('[USYNC_LID_ERR] ' + (err && err.message));
818
821
  return null;
819
822
  });
820
823
 
@@ -838,19 +841,19 @@ class DeviceManager {
838
841
  ? parseInt(String(devNode.attrs.id), 10) : 0;
839
842
  this._dcAdd(cacheKey, devId);
840
843
  }
841
- _whaDbg('[DBG] USYNC_LID_DEVICES jid=' + jid +
842
- ' ids=[' + (this._dcGet(cacheKey) || []).join(',') + ']\n');
844
+ _whaDbg('[USYNC_LID_DEVICES] jid=' + jid +
845
+ ' ids=[' + (this._dcGet(cacheKey) || []).join(',') + ']');
843
846
  } else {
844
847
  this._dcAdd(cacheKey, 0);
845
- _whaDbg('[DBG] USYNC_LID_NO_DEVLIST jid=' + jid + ' → fallback id=0\n');
848
+ _whaDbg('[USYNC_LID_NO_DEVLIST] jid=' + jid + ' → device 0 only');
846
849
  }
847
850
  }
848
851
  } else {
849
- _whaDbg('[DBG] USYNC_LID_NO_LIST jid=' + jid);
852
+ _whaDbg('[USYNC_LID_NO_LIST] jid=' + jid);
850
853
  this._dcAdd(cacheKey, 0);
851
854
  }
852
855
  } else {
853
- _whaDbg('[DBG] USYNC_LID_NULL_RESP jid=' + jid);
856
+ _whaDbg('[USYNC_LID_NULL_RESP] jid=' + jid);
854
857
  this._dcAdd(cacheKey, 0);
855
858
  }
856
859
  this._scheduleSave();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "whalibmob",
3
- "version": "5.5.36",
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",