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 +2 -31
- package/lib/DeviceManager.js +101 -77
- package/package.json +1 -1
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)
|
|
536
|
-
if (this._keepTimer)
|
|
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 || {}));
|
package/lib/DeviceManager.js
CHANGED
|
@@ -412,45 +412,58 @@ class DeviceManager {
|
|
|
412
412
|
return jids.length > 0 ? jids : phones.map(p => makeDeviceJid(p, 0));
|
|
413
413
|
}
|
|
414
414
|
|
|
415
|
-
// ───
|
|
415
|
+
// ─── usync IQ — builds the device-list query exactly as WhatsApp APK does ────
|
|
416
416
|
//
|
|
417
|
-
//
|
|
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
|
-
//
|
|
422
|
-
//
|
|
423
|
-
//
|
|
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
|
-
//
|
|
426
|
-
// Was: (absent)
|
|
427
|
-
// Fix: <side_list/> (required for LID device discovery)
|
|
432
|
+
// Critical fixes vs original whalibmob implementation:
|
|
428
433
|
//
|
|
429
|
-
//
|
|
430
|
-
//
|
|
431
|
-
//
|
|
432
|
-
//
|
|
433
|
-
// The phone/LID identifier is on the PARENT <user jid="..."> node, not on device.
|
|
434
|
+
// Fix A — fetch_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
|
-
//
|
|
440
|
-
//
|
|
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', {
|
|
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: '
|
|
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('[
|
|
475
|
+
_whaDbg('[USYNC_IQ] phones=[' + phones.join(',') + ']');
|
|
463
476
|
|
|
464
477
|
const response = await this._client._sendIq(iqNode).catch(err => {
|
|
465
|
-
_whaDbg('[
|
|
478
|
+
_whaDbg('[USYNC_ERR] ' + (err && err.message));
|
|
466
479
|
return null;
|
|
467
480
|
});
|
|
468
481
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
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
|
-
//
|
|
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
|
|
513
|
+
cachePhone = rawUser; // last resort
|
|
512
514
|
}
|
|
513
|
-
_whaDbg('[
|
|
515
|
+
_whaDbg('[USYNC_LID_USER] jid=' + userJid + ' → phone=' + cachePhone);
|
|
514
516
|
} else {
|
|
515
|
-
//
|
|
517
|
+
// Server returned a phone JID (phone@s.whatsapp.net).
|
|
516
518
|
cachePhone = rawUser;
|
|
517
519
|
|
|
518
|
-
//
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
544
|
-
|
|
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('[
|
|
570
|
+
_whaDbg('[USYNC_NO_DEVLIST] phone=' + cachePhone + ' → device 0 only');
|
|
549
571
|
}
|
|
550
572
|
}
|
|
551
573
|
} else {
|
|
552
|
-
_whaDbg('[
|
|
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
|
-
//
|
|
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('[
|
|
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('[
|
|
730
|
+
_whaDbg('[CONTACT_USYNC] phones=[' + phones.join(',') + ']');
|
|
716
731
|
|
|
717
732
|
const response = await this._client._sendIq(iqNode).catch(err => {
|
|
718
|
-
_whaDbg('[
|
|
733
|
+
_whaDbg('[CONTACT_USYNC_ERR] ' + (err && err.message));
|
|
719
734
|
return null;
|
|
720
735
|
});
|
|
721
736
|
|
|
722
737
|
if (!response) {
|
|
723
|
-
_whaDbg('[
|
|
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: '
|
|
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('[
|
|
817
|
+
_whaDbg('[USYNC_LID_IQ] jid=' + jid);
|
|
794
818
|
|
|
795
819
|
const response = await this._client._sendIq(iqNode).catch(err => {
|
|
796
|
-
_whaDbg('[
|
|
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('[
|
|
821
|
-
' ids=[' + (this._dcGet(cacheKey) || []).join(',') + ']
|
|
844
|
+
_whaDbg('[USYNC_LID_DEVICES] jid=' + jid +
|
|
845
|
+
' ids=[' + (this._dcGet(cacheKey) || []).join(',') + ']');
|
|
822
846
|
} else {
|
|
823
847
|
this._dcAdd(cacheKey, 0);
|
|
824
|
-
_whaDbg('[
|
|
848
|
+
_whaDbg('[USYNC_LID_NO_DEVLIST] jid=' + jid + ' → device 0 only');
|
|
825
849
|
}
|
|
826
850
|
}
|
|
827
851
|
} else {
|
|
828
|
-
_whaDbg('[
|
|
852
|
+
_whaDbg('[USYNC_LID_NO_LIST] jid=' + jid);
|
|
829
853
|
this._dcAdd(cacheKey, 0);
|
|
830
854
|
}
|
|
831
855
|
} else {
|
|
832
|
-
_whaDbg('[
|
|
856
|
+
_whaDbg('[USYNC_LID_NULL_RESP] jid=' + jid);
|
|
833
857
|
this._dcAdd(cacheKey, 0);
|
|
834
858
|
}
|
|
835
859
|
this._scheduleSave();
|