whalibmob 5.5.36 → 5.5.38
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 +99 -72
- package/lib/messages/MessageSender.js +83 -0
- 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
|
@@ -43,7 +43,14 @@ function jidStrToObj(jidStr) {
|
|
|
43
43
|
if (server === 's.whatsapp.net' && device > 0) {
|
|
44
44
|
return { user, agent: 0, device, server, toString() { return self; } };
|
|
45
45
|
}
|
|
46
|
-
//
|
|
46
|
+
// @lid multi-device (e.g. "112713111982325:2@lid"): JID_PAIR with user="112713111982325:2"
|
|
47
|
+
// so the server receives the colon-device in the user string, not stripped.
|
|
48
|
+
// Without this, ":2@lid" is encoded as device-0 "@lid" and the server
|
|
49
|
+
// returns the wrong bundle (or ignores device 2 entirely).
|
|
50
|
+
if (server === 'lid' && device > 0) {
|
|
51
|
+
return { user: raw, server, toString() { return self; } };
|
|
52
|
+
}
|
|
53
|
+
// JID_PAIR: used for @lid device-0, @g.us, and primary (device-0) @s.whatsapp.net
|
|
47
54
|
return { user, server, toString() { return self; } };
|
|
48
55
|
}
|
|
49
56
|
|
|
@@ -437,33 +444,33 @@ class DeviceManager {
|
|
|
437
444
|
// device_orientation="0" is also required by the server.
|
|
438
445
|
//
|
|
439
446
|
// 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
447
|
async _doUsyncIq(phones) {
|
|
445
448
|
const iqId = this._client._genMsgId();
|
|
446
449
|
const sid = this._client._genMsgId();
|
|
447
450
|
|
|
448
|
-
//
|
|
449
|
-
//
|
|
451
|
+
// User list: <user><contact>+phone</contact></user>
|
|
452
|
+
// This is the ONLY format the server responds to for phone-based usync.
|
|
453
|
+
// The <user jid="..."/> attribute format is silently ignored by the server.
|
|
450
454
|
const listChildren = phones.map(p =>
|
|
451
455
|
new BinaryNode('user', {}, [
|
|
452
456
|
new BinaryNode('contact', {}, Buffer.from('+' + p, 'utf8'))
|
|
453
457
|
])
|
|
454
458
|
);
|
|
455
459
|
|
|
460
|
+
// context="interactive" is REQUIRED.
|
|
461
|
+
// The server ignores context="message" usync IQs entirely (never replies).
|
|
462
|
+
// Query includes <devices>, <lid>, AND <contact> so the server returns
|
|
463
|
+
// the full device list + LID mapping + contact presence in one round-trip.
|
|
456
464
|
const iqNode = new BinaryNode('iq',
|
|
457
465
|
{ id: iqId, to: 's.whatsapp.net', type: 'get', xmlns: 'usync' },
|
|
458
466
|
[new BinaryNode('usync',
|
|
459
|
-
{ sid, mode: 'query', last: 'true', index: '0', context: '
|
|
467
|
+
{ sid, mode: 'query', last: 'true', index: '0', context: 'interactive' },
|
|
460
468
|
[
|
|
461
469
|
new BinaryNode('query', {},
|
|
462
470
|
[
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
new BinaryNode('
|
|
466
|
-
new BinaryNode('lid', {}, null)
|
|
471
|
+
new BinaryNode('devices', { version: '2' }, null),
|
|
472
|
+
new BinaryNode('lid', {}, null),
|
|
473
|
+
new BinaryNode('contact', {}, null)
|
|
467
474
|
]
|
|
468
475
|
),
|
|
469
476
|
new BinaryNode('list', {}, listChildren),
|
|
@@ -472,27 +479,18 @@ class DeviceManager {
|
|
|
472
479
|
)]
|
|
473
480
|
);
|
|
474
481
|
|
|
475
|
-
_whaDbg('[
|
|
482
|
+
_whaDbg('[USYNC_IQ] phones=[' + phones.join(',') + ']');
|
|
476
483
|
|
|
477
484
|
const response = await this._client._sendIq(iqNode).catch(err => {
|
|
478
|
-
_whaDbg('[
|
|
485
|
+
_whaDbg('[USYNC_ERR] ' + (err && err.message));
|
|
479
486
|
return null;
|
|
480
487
|
});
|
|
481
488
|
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
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) {
|
|
489
|
+
if (!response) {
|
|
490
|
+
_whaDbg('[USYNC_NULL_RESP]');
|
|
491
|
+
// Timed out — fall back to contact usync for LID mapping only.
|
|
492
|
+
await this._doContactUsync(phones).catch(() => {});
|
|
493
|
+
} else {
|
|
496
494
|
const usyncNode = findChild(response, 'usync');
|
|
497
495
|
const listNode = usyncNode
|
|
498
496
|
? findChild(usyncNode, 'list')
|
|
@@ -508,37 +506,47 @@ class DeviceManager {
|
|
|
508
506
|
const isLidUser = String(userJid).endsWith('@lid');
|
|
509
507
|
const { user: rawUser } = stripUser(String(userJid));
|
|
510
508
|
|
|
511
|
-
// Determine phone-based cache key (always a phone number, never a LID)
|
|
512
509
|
let cachePhone;
|
|
513
510
|
if (isLidUser) {
|
|
514
|
-
//
|
|
511
|
+
// Server returned a LID JID — look up the corresponding phone number.
|
|
515
512
|
const pn = this._client._lidToPn && this._client._lidToPn.get(rawUser);
|
|
516
513
|
if (pn) {
|
|
517
514
|
cachePhone = pn;
|
|
518
515
|
} else if (phones.length === 1) {
|
|
519
|
-
// Single phone queried — this LID must belong to it
|
|
520
516
|
cachePhone = phones[0];
|
|
521
517
|
if (this._client._lidToPn) this._client._lidToPn.set(rawUser, phones[0]);
|
|
522
518
|
if (this._client._pnToLid) this._client._pnToLid.set(phones[0], rawUser);
|
|
523
519
|
} else {
|
|
524
|
-
cachePhone = rawUser; // last resort
|
|
520
|
+
cachePhone = rawUser; // last resort
|
|
525
521
|
}
|
|
526
|
-
_whaDbg('[
|
|
522
|
+
_whaDbg('[USYNC_LID_USER] jid=' + userJid + ' → phone=' + cachePhone);
|
|
527
523
|
} else {
|
|
528
|
-
//
|
|
524
|
+
// Server returned a phone JID (phone@s.whatsapp.net).
|
|
529
525
|
cachePhone = rawUser;
|
|
530
526
|
|
|
531
|
-
//
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
527
|
+
// The LID is in a <lid val={user:"NNNN",...}> CHILD NODE, NOT an attr.
|
|
528
|
+
// Extract and populate both directions of the PN↔LID mapping, and
|
|
529
|
+
// pre-populate the lid: cache key so bulkEnsureSessionsForLid can
|
|
530
|
+
// skip a second IQ for this recipient.
|
|
531
|
+
const lidChildNode = findChild(userNode, 'lid');
|
|
532
|
+
const lidVal = lidChildNode && lidChildNode.attrs && lidChildNode.attrs.val;
|
|
533
|
+
if (lidVal) {
|
|
534
|
+
const lidUser = lidVal.user
|
|
535
|
+
? String(lidVal.user)
|
|
536
|
+
: String(lidVal).split('@')[0].split(':')[0];
|
|
537
|
+
if (lidUser) {
|
|
538
|
+
if (this._client._lidToPn) this._client._lidToPn.set(lidUser, cachePhone);
|
|
539
|
+
if (this._client._pnToLid) this._client._pnToLid.set(cachePhone, lidUser);
|
|
540
|
+
const store = this._client._signal && this._client._signal.store;
|
|
541
|
+
if (store && store.setLidMapping) store.setLidMapping(cachePhone, lidUser);
|
|
542
|
+
_whaDbg('[USYNC_LID_MAP] phone=' + cachePhone + ' ↔ lid=' + lidUser);
|
|
543
|
+
this._lidForPhone = this._lidForPhone || new Map();
|
|
544
|
+
this._lidForPhone.set(cachePhone, lidUser);
|
|
545
|
+
}
|
|
538
546
|
}
|
|
539
547
|
}
|
|
540
548
|
|
|
541
|
-
//
|
|
549
|
+
// Parse device list: user → devices → device-list → device[id=N]
|
|
542
550
|
const devicesNode = findChild(userNode, 'devices');
|
|
543
551
|
const deviceListNode = devicesNode ? findChild(devicesNode, 'device-list') : null;
|
|
544
552
|
|
|
@@ -547,37 +555,38 @@ class DeviceManager {
|
|
|
547
555
|
if (deviceListNode && Array.isArray(deviceListNode.content)) {
|
|
548
556
|
for (const devNode of deviceListNode.content) {
|
|
549
557
|
if (!devNode || devNode.description !== 'device') continue;
|
|
550
|
-
// FIX: server uses `id` attribute (e.g. id="0"), NOT `jid` attribute
|
|
551
558
|
const devId = devNode.attrs && devNode.attrs.id != null
|
|
552
559
|
? parseInt(String(devNode.attrs.id), 10)
|
|
553
560
|
: 0;
|
|
554
561
|
this._dcAdd(cachePhone, devId);
|
|
555
562
|
}
|
|
556
|
-
|
|
557
|
-
|
|
563
|
+
|
|
564
|
+
// Mirror device IDs to the lid: cache key so _doUsyncIqByJid is
|
|
565
|
+
// skipped when bulkEnsureSessionsForLid is later called for this LID.
|
|
566
|
+
const knownLid = this._lidForPhone && this._lidForPhone.get(cachePhone);
|
|
567
|
+
if (knownLid) {
|
|
568
|
+
const devIds = this._dcGet(cachePhone) || [];
|
|
569
|
+
this._dcSet('lid:' + knownLid, devIds.slice());
|
|
570
|
+
_whaDbg('[USYNC_LID_CACHE] lid=' + knownLid + ' ids=[' + devIds.join(',') + ']');
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
_whaDbg('[USYNC_DEVICES] phone=' + cachePhone +
|
|
574
|
+
' ids=[' + (this._dcGet(cachePhone) || []).join(',') + ']');
|
|
558
575
|
} else {
|
|
559
|
-
// No device-list in response — fall back to device 0
|
|
560
576
|
this._dcAdd(cachePhone, 0);
|
|
561
|
-
_whaDbg('[
|
|
577
|
+
_whaDbg('[USYNC_NO_DEVLIST] phone=' + cachePhone + ' → device 0 only');
|
|
562
578
|
}
|
|
563
579
|
}
|
|
564
580
|
} else {
|
|
565
|
-
_whaDbg('[
|
|
581
|
+
_whaDbg('[USYNC_RESP_NO_LIST]');
|
|
566
582
|
}
|
|
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
583
|
}
|
|
575
584
|
|
|
576
|
-
//
|
|
585
|
+
// Fallback: any phone not resolved → device 0
|
|
577
586
|
for (const p of phones) {
|
|
578
587
|
if (!this._dcHas(p)) {
|
|
579
588
|
this._dcSet(p, [0]);
|
|
580
|
-
_whaDbg('[
|
|
589
|
+
_whaDbg('[USYNC_FALLBACK] phone=' + p);
|
|
581
590
|
}
|
|
582
591
|
}
|
|
583
592
|
this._scheduleSave();
|
|
@@ -680,8 +689,25 @@ class DeviceManager {
|
|
|
680
689
|
for (const [jid, bundle] of bundles) {
|
|
681
690
|
try {
|
|
682
691
|
await signalProto.buildSessionFromBundle(jid, bundle);
|
|
683
|
-
|
|
684
|
-
|
|
692
|
+
// ── CRITICAL: remap bundle JID back to @lid format ────────────────────
|
|
693
|
+
// fetchBundles always returns JIDs in @s.whatsapp.net format (because the
|
|
694
|
+
// WA server uses its own AD_JID / JID_PAIR encoding in responses).
|
|
695
|
+
// But the outer <message to="lidUser@lid"> requires ALL participant <to>
|
|
696
|
+
// nodes to also use @lid format — mixing @s.whatsapp.net participants with
|
|
697
|
+
// a @lid routing address causes the server to close the connection.
|
|
698
|
+
//
|
|
699
|
+
// The Signal session address is keyed only by (user, device) — the @server
|
|
700
|
+
// suffix is stripped by jidToAddress() — so the session is reachable via
|
|
701
|
+
// either @lid or @s.whatsapp.net. We push the @lid form so the participants
|
|
702
|
+
// node is consistent with the routing.
|
|
703
|
+
//
|
|
704
|
+
// Mapping: strip @s.whatsapp.net / @lid suffix from jid, re-append @lid
|
|
705
|
+
// e.g. "112713111982325@s.whatsapp.net" → "112713111982325@lid"
|
|
706
|
+
// "112713111982325:2@s.whatsapp.net" → "112713111982325:2@lid"
|
|
707
|
+
const jidBase = jid.replace(/@(?:s\.whatsapp\.net|lid)$/, '');
|
|
708
|
+
const lidJidForDevice = jidBase + '@lid';
|
|
709
|
+
readyJids.push(lidJidForDevice);
|
|
710
|
+
_whaDbg('[DBG] LID_SESSION_BUILT jid=' + jid + ' → participant=' + lidJidForDevice);
|
|
685
711
|
} catch (e) {
|
|
686
712
|
_whaDbg('[DBG] LID_SESSION_ERR jid=' + jid + ' err=' + e.message);
|
|
687
713
|
}
|
|
@@ -725,15 +751,15 @@ class DeviceManager {
|
|
|
725
751
|
)]
|
|
726
752
|
);
|
|
727
753
|
|
|
728
|
-
_whaDbg('[
|
|
754
|
+
_whaDbg('[CONTACT_USYNC] phones=[' + phones.join(',') + ']');
|
|
729
755
|
|
|
730
756
|
const response = await this._client._sendIq(iqNode).catch(err => {
|
|
731
|
-
_whaDbg('[
|
|
757
|
+
_whaDbg('[CONTACT_USYNC_ERR] ' + (err && err.message));
|
|
732
758
|
return null;
|
|
733
759
|
});
|
|
734
760
|
|
|
735
761
|
if (!response) {
|
|
736
|
-
_whaDbg('[
|
|
762
|
+
_whaDbg('[CONTACT_USYNC_NULL]');
|
|
737
763
|
return;
|
|
738
764
|
}
|
|
739
765
|
|
|
@@ -794,12 +820,13 @@ class DeviceManager {
|
|
|
794
820
|
const iqNode = new BinaryNode('iq',
|
|
795
821
|
{ id: iqId, to: 's.whatsapp.net', type: 'get', xmlns: 'usync' },
|
|
796
822
|
[new BinaryNode('usync',
|
|
797
|
-
{ sid, mode: 'query', last: 'true', index: '0', context: '
|
|
823
|
+
{ sid, mode: 'query', last: 'true', index: '0', context: 'interactive' },
|
|
798
824
|
[
|
|
799
825
|
new BinaryNode('query', {},
|
|
800
826
|
[
|
|
801
|
-
//
|
|
802
|
-
|
|
827
|
+
// context="interactive" is REQUIRED — server silently ignores
|
|
828
|
+
// "message" context usync IQs (same as _doUsyncIq).
|
|
829
|
+
new BinaryNode('devices', { version: '2' }, null),
|
|
803
830
|
new BinaryNode('lid', {}, null)
|
|
804
831
|
]
|
|
805
832
|
),
|
|
@@ -811,10 +838,10 @@ class DeviceManager {
|
|
|
811
838
|
)]
|
|
812
839
|
);
|
|
813
840
|
|
|
814
|
-
_whaDbg('[
|
|
841
|
+
_whaDbg('[USYNC_LID_IQ] jid=' + jid);
|
|
815
842
|
|
|
816
843
|
const response = await this._client._sendIq(iqNode).catch(err => {
|
|
817
|
-
_whaDbg('[
|
|
844
|
+
_whaDbg('[USYNC_LID_ERR] ' + (err && err.message));
|
|
818
845
|
return null;
|
|
819
846
|
});
|
|
820
847
|
|
|
@@ -838,19 +865,19 @@ class DeviceManager {
|
|
|
838
865
|
? parseInt(String(devNode.attrs.id), 10) : 0;
|
|
839
866
|
this._dcAdd(cacheKey, devId);
|
|
840
867
|
}
|
|
841
|
-
_whaDbg('[
|
|
842
|
-
' ids=[' + (this._dcGet(cacheKey) || []).join(',') + ']
|
|
868
|
+
_whaDbg('[USYNC_LID_DEVICES] jid=' + jid +
|
|
869
|
+
' ids=[' + (this._dcGet(cacheKey) || []).join(',') + ']');
|
|
843
870
|
} else {
|
|
844
871
|
this._dcAdd(cacheKey, 0);
|
|
845
|
-
_whaDbg('[
|
|
872
|
+
_whaDbg('[USYNC_LID_NO_DEVLIST] jid=' + jid + ' → device 0 only');
|
|
846
873
|
}
|
|
847
874
|
}
|
|
848
875
|
} else {
|
|
849
|
-
_whaDbg('[
|
|
876
|
+
_whaDbg('[USYNC_LID_NO_LIST] jid=' + jid);
|
|
850
877
|
this._dcAdd(cacheKey, 0);
|
|
851
878
|
}
|
|
852
879
|
} else {
|
|
853
|
-
_whaDbg('[
|
|
880
|
+
_whaDbg('[USYNC_LID_NULL_RESP] jid=' + jid);
|
|
854
881
|
this._dcAdd(cacheKey, 0);
|
|
855
882
|
}
|
|
856
883
|
this._scheduleSave();
|
|
@@ -534,6 +534,18 @@ class MessageSender {
|
|
|
534
534
|
this._devMgr.clearCache(members.map(phoneFromJid));
|
|
535
535
|
return this._sendGroupMessage(toJid, msgId, plaintext, mediaType, options);
|
|
536
536
|
}
|
|
537
|
+
// Error 479 = stale/dead Signal session — clear all sessions for every
|
|
538
|
+
// group member, flush device cache, rebuild from fresh bundles, retry once.
|
|
539
|
+
if (err && /\b479\b/.test(err.message) && !options._479retry) {
|
|
540
|
+
_whaDbg('[DBG] 479_GROUP clearing sessions for group ' + toJid);
|
|
541
|
+
const members = this._client._getGroupMembers
|
|
542
|
+
? this._client._getGroupMembers(toJid) : [];
|
|
543
|
+
for (const memberJid of members) {
|
|
544
|
+
await this._clearSignalSessionsForRecipient(phoneFromJid(memberJid)).catch(() => {});
|
|
545
|
+
}
|
|
546
|
+
return this._sendGroupMessage(toJid, msgId, plaintext, mediaType,
|
|
547
|
+
Object.assign({}, options, { _479retry: true }));
|
|
548
|
+
}
|
|
537
549
|
throw err;
|
|
538
550
|
}
|
|
539
551
|
}
|
|
@@ -545,10 +557,81 @@ class MessageSender {
|
|
|
545
557
|
this._devMgr.clearCache([phoneFromJid(toJid)]);
|
|
546
558
|
return this._sendDMMessage(toJid, msgId, plaintext, mediaType, options);
|
|
547
559
|
}
|
|
560
|
+
// ── Error 479: stale / dead Signal session ────────────────────────────────
|
|
561
|
+
// The server rejected the ciphertext because it was encrypted with a Signal
|
|
562
|
+
// session key the server no longer recognises (recipient re-installed WA,
|
|
563
|
+
// changed device, or their prekey was exhausted / revoked).
|
|
564
|
+
//
|
|
565
|
+
// Recovery:
|
|
566
|
+
// 1. Delete all cached Signal sessions for this recipient (PN + LID).
|
|
567
|
+
// 2. Flush device-ID cache → forces a fresh usync IQ on next send.
|
|
568
|
+
// 3. Re-call _sendDMMessage → re-runs usync + fetchBundles + buildSession
|
|
569
|
+
// + re-encrypts with a fresh PreKey message (pkmsg).
|
|
570
|
+
//
|
|
571
|
+
// _479retry flag prevents infinite loops: if the fresh session also gets
|
|
572
|
+
// 479 (extremely rare — server bug or banned account) we fail cleanly.
|
|
573
|
+
if (err && /\b479\b/.test(err.message) && !options._479retry) {
|
|
574
|
+
await this._clearSignalSessionsForRecipient(phoneFromJid(toJid));
|
|
575
|
+
// CRITICAL: generate a NEW msgId — the server already ACK'd (with error=479)
|
|
576
|
+
// the original ID and will close the connection if we re-send the same ID.
|
|
577
|
+
const retryMsgId = generateMessageId();
|
|
578
|
+
_whaDbg('[DBG] 479_RETRY new msgId=' + retryMsgId);
|
|
579
|
+
return this._sendDMMessage(toJid, retryMsgId, plaintext, mediaType,
|
|
580
|
+
Object.assign({}, options, { _479retry: true }));
|
|
581
|
+
}
|
|
582
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
548
583
|
throw err;
|
|
549
584
|
}
|
|
550
585
|
}
|
|
551
586
|
|
|
587
|
+
// ─── Clear all Signal sessions for a phone-number recipient ─────────────────
|
|
588
|
+
//
|
|
589
|
+
// Called on error 479 (stale session). Deletes:
|
|
590
|
+
// • All Signal sessions keyed by phone-number device JIDs (e.g. 40756469325:0)
|
|
591
|
+
// • The corresponding LID device session if a LID mapping is known
|
|
592
|
+
//
|
|
593
|
+
// Then flushes the device-ID cache for that phone so the next send re-runs
|
|
594
|
+
// usync (gets current device list) and re-fetches fresh prekey bundles.
|
|
595
|
+
async _clearSignalSessionsForRecipient(phone) {
|
|
596
|
+
_whaDbg('[DBG] 479_RECOVERY phone=' + phone + ' — clearing sessions + device cache');
|
|
597
|
+
const signal = this._signal;
|
|
598
|
+
const devMgr = this._devMgr;
|
|
599
|
+
const client = this._client;
|
|
600
|
+
|
|
601
|
+
// ── 1. Get all known device JIDs for this phone (from cache) ──────────────
|
|
602
|
+
const devJids = devMgr._jidsFromCache([phone]);
|
|
603
|
+
for (const jid of devJids) {
|
|
604
|
+
try {
|
|
605
|
+
await signal.deleteSession(jid);
|
|
606
|
+
_whaDbg('[DBG] 479_DEL_SESSION jid=' + jid);
|
|
607
|
+
} catch (_) {}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// ── 2. Clear LID session if we have a LID mapping for this phone ──────────
|
|
611
|
+
// NOTE: LID JIDs use @lid server, NOT @s.whatsapp.net. Cannot use
|
|
612
|
+
// _jidsFromCache() (it hard-codes @s.whatsapp.net) — build JIDs manually.
|
|
613
|
+
const lidUser = client._pnToLid && client._pnToLid.get(phone);
|
|
614
|
+
if (lidUser) {
|
|
615
|
+
// Get all device IDs for this LID from cache (or assume device 0 primary)
|
|
616
|
+
const lidDeviceIds = (devMgr._dcGet && devMgr._dcGet('lid:' + lidUser)) || [0];
|
|
617
|
+
for (const devId of lidDeviceIds) {
|
|
618
|
+
const jid = (devId === 0) ? `${lidUser}@lid` : `${lidUser}:${devId}@lid`;
|
|
619
|
+
try {
|
|
620
|
+
await signal.deleteSession(jid);
|
|
621
|
+
_whaDbg('[DBG] 479_DEL_LID_SESSION jid=' + jid);
|
|
622
|
+
} catch (_) {}
|
|
623
|
+
}
|
|
624
|
+
// Always delete the bare primary JID too (in case it was stored without device suffix)
|
|
625
|
+
try { await signal.deleteSession(`${lidUser}@lid`); } catch (_) {}
|
|
626
|
+
// Flush LID device-ID cache so next send re-runs usync for LID
|
|
627
|
+
devMgr.clearCache(['lid:' + lidUser]);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// ── 3. Flush phone device cache → usync re-runs on next send ──────────────
|
|
631
|
+
devMgr.clearCache([phone]);
|
|
632
|
+
_whaDbg('[DBG] 479_CACHE_FLUSHED phone=' + phone);
|
|
633
|
+
}
|
|
634
|
+
|
|
552
635
|
// ─── 1-to-1 multi-device fanout ───────────────────────────────────────────
|
|
553
636
|
|
|
554
637
|
async _sendDMMessage(toJid, msgId, plaintext, mediaType, options) {
|