whalibmob 5.5.71 → 5.5.73
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/BinaryNode.js +7 -1
- package/lib/Client.js +30 -3
- package/lib/messages/MessageSender.js +49 -2
- package/lib/signal/SignalProtocol.js +8 -0
- package/package.json +1 -1
package/lib/BinaryNode.js
CHANGED
|
@@ -310,9 +310,15 @@ class NodeDecoder {
|
|
|
310
310
|
// that does not exist, so device queries for it came back empty, no Signal
|
|
311
311
|
// session was ever built, and group sends were rejected with ack 479.
|
|
312
312
|
const server = _serverForAgent(agent);
|
|
313
|
+
// The device must survive stringification. Everything downstream keys off
|
|
314
|
+
// the string form: fetchBundles maps bundles by it, so five devices all
|
|
315
|
+
// rendering as "user@server" collapsed into one entry and left the rest
|
|
316
|
+
// without a session; and jidToAddress parses the ":device" out of it, so
|
|
317
|
+
// dropping it addressed every Signal session as device 0 — which is why a
|
|
318
|
+
// message from a linked device could not be decrypted either.
|
|
313
319
|
return {
|
|
314
320
|
user, agent, device, server,
|
|
315
|
-
toString() { return `${user}@${server}`; }
|
|
321
|
+
toString() { return device ? `${user}:${device}@${server}` : `${user}@${server}`; }
|
|
316
322
|
};
|
|
317
323
|
}
|
|
318
324
|
}
|
package/lib/Client.js
CHANGED
|
@@ -11,6 +11,11 @@ const { checkIfRegistered, checkNumberStatus, requestSmsCode, verifyCode, assert
|
|
|
11
11
|
const { getDeviceConfig } = require('./DeviceConfig');
|
|
12
12
|
const { createNewStore, saveStore, loadStore, toSixParts, fromSixParts } = require('./Store');
|
|
13
13
|
const { BinaryNode } = require('./BinaryNode');
|
|
14
|
+
|
|
15
|
+
// How many times a message may be re-sent in answer to retry receipts before
|
|
16
|
+
// the client stops. Each resend can itself draw retries, so this is what keeps
|
|
17
|
+
// an undecryptable message from turning into a flood.
|
|
18
|
+
const MAX_RETRY_RESENDS = 2;
|
|
14
19
|
const { NodeCache } = require('@cacheable/node-cache');
|
|
15
20
|
|
|
16
21
|
// Group metadata cache lifetime — matches DeviceManager's device cache.
|
|
@@ -877,6 +882,23 @@ class WhalibmobClient extends EventEmitter {
|
|
|
877
882
|
return;
|
|
878
883
|
}
|
|
879
884
|
|
|
885
|
+
// Consume the entry before doing anything else. Every device that failed to
|
|
886
|
+
// decrypt sends its own retry receipt for the same message, and a group
|
|
887
|
+
// resend already re-addresses all of them — without this, each receipt
|
|
888
|
+
// started its own resend.
|
|
889
|
+
this._sentMsgCache.delete(msgId);
|
|
890
|
+
|
|
891
|
+
// Bound the chain. A resend that still cannot be decrypted draws fresh
|
|
892
|
+
// retry receipts of its own, so an unbounded handler answers each one with
|
|
893
|
+
// another send and floods the chat. Give up after a few rounds and leave
|
|
894
|
+
// the message undelivered rather than keep spamming.
|
|
895
|
+
const depth = (cached.retryDepth || 0) + 1;
|
|
896
|
+
if (depth > MAX_RETRY_RESENDS) {
|
|
897
|
+
_whaDbg('[DBG] RETRY_RECV msgId=' + msgId + ' — giving up after ' +
|
|
898
|
+
cached.retryDepth + ' resends\n');
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
901
|
+
|
|
880
902
|
const fromStr = attrs.from ? String(attrs.from) : '';
|
|
881
903
|
const isGroup = fromStr.endsWith('@g.us');
|
|
882
904
|
|
|
@@ -912,13 +934,18 @@ class WhalibmobClient extends EventEmitter {
|
|
|
912
934
|
// SenderKey distribution, which only _sendGroupMessage builds. Drop the
|
|
913
935
|
// "already distributed" record first, or the resend would carry the same
|
|
914
936
|
// bare skmsg the recipient just told us it could not read.
|
|
915
|
-
|
|
937
|
+
// Carry the round count into the resend so its own retry receipts continue
|
|
938
|
+
// the same chain instead of restarting it at zero.
|
|
939
|
+
const opts = Object.assign({}, cached.options, { _retryDepth: depth });
|
|
940
|
+
|
|
916
941
|
if (cached.isGroup) {
|
|
942
|
+
const resendId = generateMessageId();
|
|
917
943
|
if (this._signal && this._signal.senderKeyStore) {
|
|
918
944
|
this._signal.senderKeyStore.invalidateSKDM(cached.toJid);
|
|
919
945
|
}
|
|
946
|
+
_whaDbg('[DBG] RETRY_RESEND group round=' + depth + ' newId=' + resendId + '\n');
|
|
920
947
|
this._sender._sendGroupMessage(
|
|
921
|
-
cached.toJid, resendId, cached.plaintext, cached.mediaType,
|
|
948
|
+
cached.toJid, resendId, cached.plaintext, cached.mediaType, opts
|
|
922
949
|
)
|
|
923
950
|
.then(r => _whaDbg('[DBG] RETRY_RESEND group ok id=' + (r && r.id) + '\n'))
|
|
924
951
|
.catch(e => _whaDbg('[DBG] RETRY_RESEND_ERR: ' + e.message));
|
|
@@ -926,7 +953,7 @@ class WhalibmobClient extends EventEmitter {
|
|
|
926
953
|
}
|
|
927
954
|
|
|
928
955
|
this._sender._sendDMMessage(
|
|
929
|
-
cached.toJid, cached.msgId, cached.plaintext, cached.mediaType,
|
|
956
|
+
cached.toJid, cached.msgId, cached.plaintext, cached.mediaType, opts
|
|
930
957
|
)
|
|
931
958
|
.then(r => _whaDbg('[DBG] RETRY_RESEND ok id=' + r.id + '\n'))
|
|
932
959
|
.catch(e => _whaDbg('[DBG] RETRY_RESEND_ERR: ' + e.message));
|
|
@@ -554,6 +554,21 @@ class MessageSender {
|
|
|
554
554
|
return this._sendGroupMessage(toJid, msgId, plaintext, mediaType,
|
|
555
555
|
Object.assign({}, options, { _479retry: true }));
|
|
556
556
|
}
|
|
557
|
+
// No session could be built for a single member — usually a prekey IQ
|
|
558
|
+
// that timed out, leaving the device list cached with nothing behind
|
|
559
|
+
// it. Purge the cached devices and sessions so the retry re-fetches
|
|
560
|
+
// bundles and opens fresh pkmsg sessions instead of failing again.
|
|
561
|
+
if (err && /NO_OPEN_SESSION/.test(err.message) && !options._noSessionRetry) {
|
|
562
|
+
_whaDbg('[DBG] NO_OPEN_SESSION group ' + toJid + ' — forcing fresh pkmsg\n');
|
|
563
|
+
await this._forceSessionRebuild(
|
|
564
|
+
(this._client._getGroupMembers ? this._client._getGroupMembers(toJid) : [])
|
|
565
|
+
);
|
|
566
|
+
if (this._signal && this._signal.senderKeyStore) {
|
|
567
|
+
this._signal.senderKeyStore.invalidateSKDM(toJid);
|
|
568
|
+
}
|
|
569
|
+
return this._sendGroupMessage(toJid, msgId, plaintext, mediaType,
|
|
570
|
+
Object.assign({}, options, { _noSessionRetry: true }));
|
|
571
|
+
}
|
|
557
572
|
throw err;
|
|
558
573
|
}
|
|
559
574
|
}
|
|
@@ -587,11 +602,41 @@ class MessageSender {
|
|
|
587
602
|
return this._sendDMMessage(toJid, retryMsgId, plaintext, mediaType,
|
|
588
603
|
Object.assign({}, options, { _479retry: true }));
|
|
589
604
|
}
|
|
605
|
+
// ── No session at all ────────────────────────────────────────────────────
|
|
606
|
+
// Nothing could be encrypted for any device, so there is no ciphertext to
|
|
607
|
+
// send. This is what a timed-out prekey IQ leaves behind: the device list
|
|
608
|
+
// is cached but no session sits behind it, and every later send fails the
|
|
609
|
+
// same way because the cache hides the gap. Purge both and retry once, so
|
|
610
|
+
// the next attempt re-fetches bundles and opens fresh pkmsg sessions.
|
|
611
|
+
if (err && /NO_OPEN_SESSION/.test(err.message) && !options._noSessionRetry) {
|
|
612
|
+
_whaDbg('[DBG] NO_OPEN_SESSION dm ' + toJid + ' — forcing fresh pkmsg\n');
|
|
613
|
+
await this._forceSessionRebuild([toJid]);
|
|
614
|
+
return this._sendDMMessage(toJid, generateMessageId(), plaintext, mediaType,
|
|
615
|
+
Object.assign({}, options, { _noSessionRetry: true }));
|
|
616
|
+
}
|
|
590
617
|
// ─────────────────────────────────────────────────────────────────────────
|
|
591
618
|
throw err;
|
|
592
619
|
}
|
|
593
620
|
}
|
|
594
621
|
|
|
622
|
+
// Drop every cached device list and Signal session for these JIDs so the next
|
|
623
|
+
// send is forced to re-query devices, re-fetch prekey bundles and open new
|
|
624
|
+
// pkmsg sessions from scratch.
|
|
625
|
+
async _forceSessionRebuild(jids) {
|
|
626
|
+
const phones = [...new Set((jids || []).map(phoneFromJid).filter(Boolean))];
|
|
627
|
+
for (const phone of phones) {
|
|
628
|
+
await this._clearSignalSessionsForRecipient(phone).catch(() => {});
|
|
629
|
+
}
|
|
630
|
+
if (this._devMgr && phones.length) {
|
|
631
|
+
this._devMgr.clearCache(phones);
|
|
632
|
+
// LID device lists are cached under a separate key.
|
|
633
|
+
if (typeof this._devMgr._dcDel === 'function') {
|
|
634
|
+
this._devMgr._dcDel(phones.map(p => 'lid:' + p));
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
_whaDbg('[DBG] FORCE_SESSION_REBUILD purged ' + phones.join(', ') + '\n');
|
|
638
|
+
}
|
|
639
|
+
|
|
595
640
|
// ─── Clear all Signal sessions for a phone-number recipient ─────────────────
|
|
596
641
|
//
|
|
597
642
|
// Called on error 479 (stale session). Deletes:
|
|
@@ -772,7 +817,8 @@ class MessageSender {
|
|
|
772
817
|
// Cache plaintext so Client can re-send with fresh session on recipient retry
|
|
773
818
|
if (this._client._sentMsgCache) {
|
|
774
819
|
this._client._sentMsgCache.set(msgId, {
|
|
775
|
-
plaintext, toJid, msgId, mediaType, options, recipientPhone
|
|
820
|
+
plaintext, toJid, msgId, mediaType, options, recipientPhone,
|
|
821
|
+
retryDepth: options._retryDepth || 0
|
|
776
822
|
});
|
|
777
823
|
// Keep cache bounded — evict oldest entries beyond 200
|
|
778
824
|
if (this._client._sentMsgCache.size > 200) {
|
|
@@ -980,7 +1026,8 @@ class MessageSender {
|
|
|
980
1026
|
// then silently never arrived.
|
|
981
1027
|
if (this._client._sentMsgCache) {
|
|
982
1028
|
this._client._sentMsgCache.set(msgId, {
|
|
983
|
-
plaintext, toJid: groupJid, msgId, mediaType, options, isGroup: true
|
|
1029
|
+
plaintext, toJid: groupJid, msgId, mediaType, options, isGroup: true,
|
|
1030
|
+
retryDepth: options._retryDepth || 0
|
|
984
1031
|
});
|
|
985
1032
|
if (this._client._sentMsgCache.size > 200) {
|
|
986
1033
|
const firstKey = this._client._sentMsgCache.keys().next().value;
|
|
@@ -320,6 +320,14 @@ function jidToAddress(jid) {
|
|
|
320
320
|
} else if (jid && jid.user) {
|
|
321
321
|
user = jid.user;
|
|
322
322
|
deviceId = jid.device || 0;
|
|
323
|
+
// A @lid JID is encoded as JID_PAIR with the device kept inside the user
|
|
324
|
+
// part ("112713111982325:49"), so an object can carry its device there
|
|
325
|
+
// rather than in .device. Without this the address came out as device 0.
|
|
326
|
+
const colonIdx = String(user).indexOf(':');
|
|
327
|
+
if (colonIdx >= 0) {
|
|
328
|
+
if (!deviceId) deviceId = parseInt(String(user).slice(colonIdx + 1), 10) || 0;
|
|
329
|
+
user = String(user).slice(0, colonIdx);
|
|
330
|
+
}
|
|
323
331
|
} else {
|
|
324
332
|
user = String(jid);
|
|
325
333
|
}
|