whalibmob 5.5.63 → 5.5.65
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 +58 -4
- package/lib/Client.js +126 -28
- package/package.json +1 -1
package/lib/BinaryNode.js
CHANGED
|
@@ -44,11 +44,41 @@ function _writeListSize(size, parts) {
|
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
// Servers that make a string an addressable JID rather than free text.
|
|
48
|
+
const _JID_SERVERS = new Set([
|
|
49
|
+
's.whatsapp.net', 'g.us', 'lid', 'broadcast', 'call', 'newsletter', 'c.us', 's.us'
|
|
50
|
+
]);
|
|
51
|
+
|
|
52
|
+
// "120363427770886227@g.us" / "40756469325:2@s.whatsapp.net" → JID object.
|
|
53
|
+
// Returns null for anything that is not a full JID, so plain strings (and
|
|
54
|
+
// bare server tokens like "g.us", which the dictionary already encodes as a
|
|
55
|
+
// valid address) keep their existing encoding untouched.
|
|
56
|
+
function _jidFromString(str) {
|
|
57
|
+
const at = str.indexOf('@');
|
|
58
|
+
if (at <= 0 || at === str.length - 1) return null;
|
|
59
|
+
const server = str.slice(at + 1);
|
|
60
|
+
if (!_JID_SERVERS.has(server)) return null;
|
|
61
|
+
const raw = str.slice(0, at);
|
|
62
|
+
const colon = raw.indexOf(':');
|
|
63
|
+
const user = colon >= 0 ? raw.slice(0, colon) : raw;
|
|
64
|
+
const device = colon >= 0 ? (parseInt(raw.slice(colon + 1), 10) || 0) : 0;
|
|
65
|
+
// Mirrors jidStrToObj in DeviceManager: AD_JID only for multi-device
|
|
66
|
+
// @s.whatsapp.net, and @lid keeps the colon inside the user part.
|
|
67
|
+
if (server === 's.whatsapp.net' && device > 0) return { user, agent: 0, device, server };
|
|
68
|
+
if (server === 'lid' && device > 0) return { user: raw, server };
|
|
69
|
+
return { user, server };
|
|
70
|
+
}
|
|
71
|
+
|
|
47
72
|
function _writeValue(value, parts) {
|
|
48
73
|
if (value === null || value === undefined) {
|
|
49
74
|
parts.push(Buffer.from([TAG.LIST_EMPTY]));
|
|
50
75
|
} else if (typeof value === 'string') {
|
|
51
|
-
|
|
76
|
+
// A full JID written as a plain string would go out as a raw text blob,
|
|
77
|
+
// which the server cannot route — IQs addressed that way simply never get
|
|
78
|
+
// an answer. Encode it as JID_PAIR/AD_JID like an object JID.
|
|
79
|
+
const jid = _jidFromString(value);
|
|
80
|
+
if (jid) _writeJid(jid, parts);
|
|
81
|
+
else _writeString(value, parts);
|
|
52
82
|
} else if (Buffer.isBuffer(value) || value instanceof Uint8Array) {
|
|
53
83
|
const buf = Buffer.isBuffer(value) ? value : Buffer.from(value);
|
|
54
84
|
_writeBytes(buf, parts);
|
|
@@ -97,9 +127,25 @@ function _writeBytes(buf, parts) {
|
|
|
97
127
|
parts.push(buf);
|
|
98
128
|
}
|
|
99
129
|
|
|
130
|
+
// AD_JID agent byte ↔ domain. 0 = phone JIDs, 1 = LIDs.
|
|
131
|
+
const _AGENT_SERVER = { 0: 's.whatsapp.net', 1: 'lid' };
|
|
132
|
+
function _serverForAgent(agent) {
|
|
133
|
+
return _AGENT_SERVER[agent] || 's.whatsapp.net';
|
|
134
|
+
}
|
|
135
|
+
function _agentForServer(server) {
|
|
136
|
+
return server === 'lid' ? 1 : 0;
|
|
137
|
+
}
|
|
138
|
+
|
|
100
139
|
function _writeJid(jid, parts) {
|
|
101
|
-
|
|
102
|
-
|
|
140
|
+
// Derive the agent from the domain only when this JID is going out as AD_JID
|
|
141
|
+
// anyway (explicit agent, or a device suffix). A plain LID keeps its
|
|
142
|
+
// JID_PAIR + 'lid' server-token encoding, which is what the server expects
|
|
143
|
+
// and what the rest of the codebase already produces.
|
|
144
|
+
const agent = (jid.agent !== undefined && jid.agent !== null)
|
|
145
|
+
? jid.agent
|
|
146
|
+
: (jid.device ? _agentForServer(jid.server) : 0);
|
|
147
|
+
if (agent || jid.device) {
|
|
148
|
+
parts.push(Buffer.from([TAG.AD_JID, agent || 0, jid.device || 0]));
|
|
103
149
|
_writeString(jid.user, parts);
|
|
104
150
|
} else {
|
|
105
151
|
parts.push(Buffer.from([TAG.JID_PAIR]));
|
|
@@ -259,7 +305,15 @@ class NodeDecoder {
|
|
|
259
305
|
const device = this._readByte();
|
|
260
306
|
const userToken = this._readByte();
|
|
261
307
|
const user = this._readString(userToken);
|
|
262
|
-
|
|
308
|
+
// The agent byte selects the domain — it is not decoration. Hardcoding
|
|
309
|
+
// s.whatsapp.net turned every LID participant (agent 1) into a phone JID
|
|
310
|
+
// that does not exist, so device queries for it came back empty, no Signal
|
|
311
|
+
// session was ever built, and group sends were rejected with ack 479.
|
|
312
|
+
const server = _serverForAgent(agent);
|
|
313
|
+
return {
|
|
314
|
+
user, agent, device, server,
|
|
315
|
+
toString() { return `${user}@${server}`; }
|
|
316
|
+
};
|
|
263
317
|
}
|
|
264
318
|
}
|
|
265
319
|
|
package/lib/Client.js
CHANGED
|
@@ -11,6 +11,10 @@ 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
|
+
const { NodeCache } = require('@cacheable/node-cache');
|
|
15
|
+
|
|
16
|
+
// Group metadata cache lifetime — matches DeviceManager's device cache.
|
|
17
|
+
const GROUP_META_CACHE_TTL = '5m';
|
|
14
18
|
const { SignalProtocol } = require('./signal/SignalProtocol');
|
|
15
19
|
const { DeviceManager } = require('./DeviceManager');
|
|
16
20
|
const {
|
|
@@ -134,13 +138,21 @@ class WhalibmobClient extends EventEmitter {
|
|
|
134
138
|
this._mediaConn = null;
|
|
135
139
|
this._pendingIqs = new Map(); // id → resolve fn
|
|
136
140
|
this._pendingAcks = new Map(); // msgId → resolve fn
|
|
141
|
+
// groupJid → parsed metadata. Mirrors DeviceManager's device cache: a TTL
|
|
142
|
+
// so membership changes we never saw a notification for still heal, plus
|
|
143
|
+
// in-flight dedup so a burst of sends to the same group issues one IQ.
|
|
144
|
+
this._groupMetaCache = new NodeCache({ ttl: GROUP_META_CACHE_TTL });
|
|
145
|
+
this._groupMetaInflight = new Map(); // groupJid → Promise<metadata>
|
|
137
146
|
this._groupMembers = new Map(); // groupJid → Set<memberJid>
|
|
138
147
|
this._lidToPn = new Map(); // LID user → phone number
|
|
139
148
|
this._pnToLid = new Map(); // phone number → LID user
|
|
140
149
|
this._myLid = null; // own LID JID received from <success> node
|
|
141
150
|
this._groupAddressingMode = new Map(); // groupJid → 'lid' | 'pn'
|
|
142
|
-
this._retryPending = new Map(); // msgId → {node,
|
|
143
|
-
this._retryPreKeyIdx = 0; // rotating index
|
|
151
|
+
this._retryPending = new Map(); // msgId → {node, count, pkId}
|
|
152
|
+
this._retryPreKeyIdx = 0; // rotating index, fallback when every prekey is reserved
|
|
153
|
+
// keyIds handed out by unresolved retries — never advertise one twice, or
|
|
154
|
+
// the second sender's pkmsg arrives after the key was consumed and deleted.
|
|
155
|
+
this._retryAdvertisedPreKeys = new Set();
|
|
144
156
|
this._appStateVersions = {}; // collectionName → version (int)
|
|
145
157
|
this._sentMsgCache = new Map(); // msgId → {plaintext, toJid, msgId, mediaType, options, recipientPhone}
|
|
146
158
|
this._tcTokenStore = null; // TcTokenStore — loaded in init()
|
|
@@ -677,6 +689,7 @@ class WhalibmobClient extends EventEmitter {
|
|
|
677
689
|
|
|
678
690
|
this._signal.decrypt(sigJid, encType, cipherBuf)
|
|
679
691
|
.then(async plaintext => {
|
|
692
|
+
this._resolveRetry(id);
|
|
680
693
|
const decoded = this._decodeMsg(plaintext);
|
|
681
694
|
_whaDbg('[DBG] DM_DECODED id=' + id + ' type=' + decoded.type + ' ptLen=' + (plaintext ? plaintext.length : 0) + ' hasSKDM=' + !!(decoded.skdm || decoded.type === 'senderKeyDistribution'));
|
|
682
695
|
|
|
@@ -747,6 +760,7 @@ class WhalibmobClient extends EventEmitter {
|
|
|
747
760
|
|
|
748
761
|
this._signal.senderKeyDecrypt(from, participant, cipherBuf)
|
|
749
762
|
.then(plaintext => {
|
|
763
|
+
this._resolveRetry(id);
|
|
750
764
|
const decoded = this._decodeMsg(plaintext);
|
|
751
765
|
if (decoded.type === 'senderKeyDistribution') return;
|
|
752
766
|
if (decoded.type === 'protocol') {
|
|
@@ -950,6 +964,17 @@ class WhalibmobClient extends EventEmitter {
|
|
|
950
964
|
// - <registration> 4-byte reg-id
|
|
951
965
|
// - <keys> block (from retry #1): type + identity + key + skey + device-identity
|
|
952
966
|
|
|
967
|
+
// A message we asked to be re-sent finally decrypted (or we gave up on it).
|
|
968
|
+
// Release its reserved prekey so the pool does not drain over a long session.
|
|
969
|
+
_resolveRetry(msgId) {
|
|
970
|
+
const pending = this._retryPending.get(msgId);
|
|
971
|
+
if (!pending) return;
|
|
972
|
+
if (pending.pkId !== null && pending.pkId !== undefined) {
|
|
973
|
+
this._retryAdvertisedPreKeys.delete(pending.pkId);
|
|
974
|
+
}
|
|
975
|
+
this._retryPending.delete(msgId);
|
|
976
|
+
}
|
|
977
|
+
|
|
953
978
|
_sendRetryRequest(msgId, origNode) {
|
|
954
979
|
const existing = this._retryPending.get(msgId);
|
|
955
980
|
const count = existing ? existing.count + 1 : 1;
|
|
@@ -957,13 +982,25 @@ class WhalibmobClient extends EventEmitter {
|
|
|
957
982
|
|
|
958
983
|
if (!this._socket || !this._connected) return;
|
|
959
984
|
|
|
960
|
-
//
|
|
961
|
-
|
|
962
|
-
|
|
985
|
+
// Pick a prekey that is not already advertised by another unresolved retry.
|
|
986
|
+
// A prekey is deleted the moment a pkmsg using it is decrypted, so handing
|
|
987
|
+
// the same one to two senders means the second arrives after the key is
|
|
988
|
+
// gone and dies with "Invalid PreKey ID". Indexing modulo the key count
|
|
989
|
+
// could not prevent that: the list shrinks as keys are consumed, so the
|
|
990
|
+
// rotating index wraps back onto ids that are still in flight.
|
|
991
|
+
let pkId = existing ? existing.pkId : null;
|
|
992
|
+
if (pkId === null && this._signal) {
|
|
963
993
|
const allKeys = this._signal.getPreKeysForUpload(800);
|
|
964
|
-
|
|
994
|
+
const free = allKeys.find(k => !this._retryAdvertisedPreKeys.has(k.keyId));
|
|
995
|
+
// If every key is already spoken for, fall back to rotating rather than
|
|
996
|
+
// sending no bundle at all — a stale id beats no retry.
|
|
997
|
+
const chosen = free || allKeys[this._retryPreKeyIdx++ % Math.max(1, allKeys.length)];
|
|
998
|
+
if (chosen) {
|
|
999
|
+
pkId = chosen.keyId;
|
|
1000
|
+
this._retryAdvertisedPreKeys.add(pkId);
|
|
1001
|
+
}
|
|
965
1002
|
}
|
|
966
|
-
this._retryPending.set(msgId, { node: origNode, count,
|
|
1003
|
+
this._retryPending.set(msgId, { node: origNode, count, pkId });
|
|
967
1004
|
|
|
968
1005
|
// Use original message timestamp (not current time)
|
|
969
1006
|
const origAttrs = (origNode && origNode.attrs) || {};
|
|
@@ -982,7 +1019,7 @@ class WhalibmobClient extends EventEmitter {
|
|
|
982
1019
|
const spk = this._signal.getSignedPreKeyForUpload();
|
|
983
1020
|
const identKey = this._signal.getIdentityKey();
|
|
984
1021
|
const allPreKeys = this._signal.getPreKeysForUpload(800);
|
|
985
|
-
const pk = allPreKeys
|
|
1022
|
+
const pk = allPreKeys.find(k => k.keyId === pkId) || allPreKeys[0];
|
|
986
1023
|
|
|
987
1024
|
if (spk && identKey && pk) {
|
|
988
1025
|
const keysChildren = [
|
|
@@ -1153,6 +1190,10 @@ class WhalibmobClient extends EventEmitter {
|
|
|
1153
1190
|
_processGroupUpdate(node) {
|
|
1154
1191
|
const groupJid = node.attrs && node.attrs.from;
|
|
1155
1192
|
if (!groupJid) return;
|
|
1193
|
+
// Membership/settings just changed — the cached metadata is now stale.
|
|
1194
|
+
// The live _groupMembers set below stays authoritative for this session;
|
|
1195
|
+
// dropping the cache makes the next getGroupMetadata() re-read the server.
|
|
1196
|
+
this.invalidateGroupMetadata(groupJid);
|
|
1156
1197
|
if (!this._groupMembers.has(groupJid)) this._groupMembers.set(groupJid, new Set());
|
|
1157
1198
|
const members = this._groupMembers.get(groupJid);
|
|
1158
1199
|
const actor = (node.attrs && node.attrs.participant) ? String(node.attrs.participant) : null;
|
|
@@ -1447,7 +1488,7 @@ class WhalibmobClient extends EventEmitter {
|
|
|
1447
1488
|
this._groupMembers.set(jid, new Set(participants.map(p => p.jid)));
|
|
1448
1489
|
this._groupAddressingMode.set(jid, addressingMode);
|
|
1449
1490
|
|
|
1450
|
-
|
|
1491
|
+
const meta = {
|
|
1451
1492
|
jid,
|
|
1452
1493
|
subject: a.subject || '',
|
|
1453
1494
|
creation: parseInt(a.creation, 10) || 0,
|
|
@@ -1461,27 +1502,71 @@ class WhalibmobClient extends EventEmitter {
|
|
|
1461
1502
|
addressingMode,
|
|
1462
1503
|
participants
|
|
1463
1504
|
};
|
|
1505
|
+
|
|
1506
|
+
// Warm the cache from every parse — fetchAllGroups() then primes metadata
|
|
1507
|
+
// for every group at once, so the first send to any of them skips its IQ.
|
|
1508
|
+
if (this._groupMetaCache) this._groupMetaCache.set(jid, meta);
|
|
1509
|
+
return meta;
|
|
1464
1510
|
}
|
|
1465
1511
|
|
|
1466
|
-
// Query metadata for a single group — returns parsed object
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
const
|
|
1470
|
-
const node = new BinaryNode('iq', {
|
|
1471
|
-
id,
|
|
1472
|
-
to: jid,
|
|
1473
|
-
type: 'get',
|
|
1474
|
-
xmlns: 'w:g2'
|
|
1475
|
-
}, [new BinaryNode('query', { request: 'interactive' }, null)]);
|
|
1512
|
+
// Query metadata for a single group — returns parsed object.
|
|
1513
|
+
// Served from cache when available; pass { force: true } to bypass it.
|
|
1514
|
+
async getGroupMetadata(groupJid, opts) {
|
|
1515
|
+
const jid = makeJid(groupJid);
|
|
1476
1516
|
|
|
1477
|
-
|
|
1478
|
-
|
|
1517
|
+
if (!opts || !opts.force) {
|
|
1518
|
+
const cached = this._groupMetaCache.get(jid);
|
|
1519
|
+
if (cached) {
|
|
1520
|
+
_whaDbg('[DBG] GROUP_META cache hit ' + jid);
|
|
1521
|
+
// Re-seed the member/addressing maps — a reconnect clears them while
|
|
1522
|
+
// the metadata cache is still warm.
|
|
1523
|
+
this._groupMembers.set(jid, new Set(cached.participants.map(p => p.jid)));
|
|
1524
|
+
this._groupAddressingMode.set(jid, cached.addressingMode);
|
|
1525
|
+
return cached;
|
|
1526
|
+
}
|
|
1527
|
+
// Coalesce concurrent lookups for the same group onto one IQ.
|
|
1528
|
+
const inflight = this._groupMetaInflight.get(jid);
|
|
1529
|
+
if (inflight) {
|
|
1530
|
+
_whaDbg('[DBG] GROUP_META joining in-flight query for ' + jid);
|
|
1531
|
+
return inflight;
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1479
1534
|
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1535
|
+
const promise = (async () => {
|
|
1536
|
+
const id = this._genMsgId();
|
|
1537
|
+
const node = new BinaryNode('iq', {
|
|
1538
|
+
id,
|
|
1539
|
+
to: jid,
|
|
1540
|
+
type: 'get',
|
|
1541
|
+
xmlns: 'w:g2'
|
|
1542
|
+
}, [new BinaryNode('query', { request: 'interactive' }, null)]);
|
|
1543
|
+
|
|
1544
|
+
const response = await this._sendIq(node);
|
|
1545
|
+
if (!response) return null;
|
|
1546
|
+
|
|
1547
|
+
// Response may be the group node itself or wrap it
|
|
1548
|
+
const groupNode = (response.description === 'group')
|
|
1549
|
+
? response
|
|
1550
|
+
: findChild(response, 'group');
|
|
1551
|
+
const meta = this._parseGroupNode(groupNode);
|
|
1552
|
+
if (meta) this._groupMetaCache.set(jid, meta);
|
|
1553
|
+
return meta;
|
|
1554
|
+
})();
|
|
1555
|
+
|
|
1556
|
+
this._groupMetaInflight.set(jid, promise);
|
|
1557
|
+
try {
|
|
1558
|
+
return await promise;
|
|
1559
|
+
} finally {
|
|
1560
|
+
this._groupMetaInflight.delete(jid);
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
// Drop cached metadata for a group so the next read re-queries the server.
|
|
1565
|
+
invalidateGroupMetadata(groupJid) {
|
|
1566
|
+
const jid = makeJid(groupJid);
|
|
1567
|
+
this._groupMetaCache.del(jid);
|
|
1568
|
+
this._groupMetaInflight.delete(jid);
|
|
1569
|
+
_whaDbg('[DBG] GROUP_META invalidated ' + jid);
|
|
1485
1570
|
}
|
|
1486
1571
|
|
|
1487
1572
|
// Fetch ALL groups the connected account participates in
|
|
@@ -2909,10 +2994,23 @@ class WhalibmobClient extends EventEmitter {
|
|
|
2909
2994
|
type: 'set'
|
|
2910
2995
|
}, [new BinaryNode('create', baseAttrs, createChildren)]);
|
|
2911
2996
|
const resp = await this._sendIq(node);
|
|
2912
|
-
|
|
2997
|
+
// These used to return null indistinguishably, which surfaced to the CLI as
|
|
2998
|
+
// a bare "no response" no matter what actually went wrong.
|
|
2999
|
+
if (!resp) {
|
|
3000
|
+
throw new Error('createGroup: no reply from server (IQ timed out)');
|
|
3001
|
+
}
|
|
3002
|
+
if (resp.attrs && resp.attrs.type === 'error') {
|
|
3003
|
+
const errNode = findChild(resp, 'error');
|
|
3004
|
+
const code = errNode && errNode.attrs && errNode.attrs.code;
|
|
3005
|
+
const text = errNode && errNode.attrs && errNode.attrs.text;
|
|
3006
|
+
throw new Error('createGroup: server rejected the request' +
|
|
3007
|
+
(code ? ' (' + code + (text ? ' ' + text : '') + ')' : ''));
|
|
3008
|
+
}
|
|
2913
3009
|
const createNode = findChild(resp, 'create');
|
|
2914
3010
|
const groupNode = createNode ? findChild(createNode, 'group') : findChildDeep(resp, 'group');
|
|
2915
|
-
if (!groupNode)
|
|
3011
|
+
if (!groupNode) {
|
|
3012
|
+
throw new Error('createGroup: server reply contained no <group> node');
|
|
3013
|
+
}
|
|
2916
3014
|
return this._parseGroupNode(groupNode);
|
|
2917
3015
|
}
|
|
2918
3016
|
|