whalibmob 5.5.63 → 5.5.64
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 +31 -1
- package/lib/Client.js +90 -20
- 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);
|
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,6 +138,11 @@ 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
|
|
@@ -1153,6 +1162,10 @@ class WhalibmobClient extends EventEmitter {
|
|
|
1153
1162
|
_processGroupUpdate(node) {
|
|
1154
1163
|
const groupJid = node.attrs && node.attrs.from;
|
|
1155
1164
|
if (!groupJid) return;
|
|
1165
|
+
// Membership/settings just changed — the cached metadata is now stale.
|
|
1166
|
+
// The live _groupMembers set below stays authoritative for this session;
|
|
1167
|
+
// dropping the cache makes the next getGroupMetadata() re-read the server.
|
|
1168
|
+
this.invalidateGroupMetadata(groupJid);
|
|
1156
1169
|
if (!this._groupMembers.has(groupJid)) this._groupMembers.set(groupJid, new Set());
|
|
1157
1170
|
const members = this._groupMembers.get(groupJid);
|
|
1158
1171
|
const actor = (node.attrs && node.attrs.participant) ? String(node.attrs.participant) : null;
|
|
@@ -1447,7 +1460,7 @@ class WhalibmobClient extends EventEmitter {
|
|
|
1447
1460
|
this._groupMembers.set(jid, new Set(participants.map(p => p.jid)));
|
|
1448
1461
|
this._groupAddressingMode.set(jid, addressingMode);
|
|
1449
1462
|
|
|
1450
|
-
|
|
1463
|
+
const meta = {
|
|
1451
1464
|
jid,
|
|
1452
1465
|
subject: a.subject || '',
|
|
1453
1466
|
creation: parseInt(a.creation, 10) || 0,
|
|
@@ -1461,27 +1474,71 @@ class WhalibmobClient extends EventEmitter {
|
|
|
1461
1474
|
addressingMode,
|
|
1462
1475
|
participants
|
|
1463
1476
|
};
|
|
1477
|
+
|
|
1478
|
+
// Warm the cache from every parse — fetchAllGroups() then primes metadata
|
|
1479
|
+
// for every group at once, so the first send to any of them skips its IQ.
|
|
1480
|
+
if (this._groupMetaCache) this._groupMetaCache.set(jid, meta);
|
|
1481
|
+
return meta;
|
|
1464
1482
|
}
|
|
1465
1483
|
|
|
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)]);
|
|
1484
|
+
// Query metadata for a single group — returns parsed object.
|
|
1485
|
+
// Served from cache when available; pass { force: true } to bypass it.
|
|
1486
|
+
async getGroupMetadata(groupJid, opts) {
|
|
1487
|
+
const jid = makeJid(groupJid);
|
|
1476
1488
|
|
|
1477
|
-
|
|
1478
|
-
|
|
1489
|
+
if (!opts || !opts.force) {
|
|
1490
|
+
const cached = this._groupMetaCache.get(jid);
|
|
1491
|
+
if (cached) {
|
|
1492
|
+
_whaDbg('[DBG] GROUP_META cache hit ' + jid);
|
|
1493
|
+
// Re-seed the member/addressing maps — a reconnect clears them while
|
|
1494
|
+
// the metadata cache is still warm.
|
|
1495
|
+
this._groupMembers.set(jid, new Set(cached.participants.map(p => p.jid)));
|
|
1496
|
+
this._groupAddressingMode.set(jid, cached.addressingMode);
|
|
1497
|
+
return cached;
|
|
1498
|
+
}
|
|
1499
|
+
// Coalesce concurrent lookups for the same group onto one IQ.
|
|
1500
|
+
const inflight = this._groupMetaInflight.get(jid);
|
|
1501
|
+
if (inflight) {
|
|
1502
|
+
_whaDbg('[DBG] GROUP_META joining in-flight query for ' + jid);
|
|
1503
|
+
return inflight;
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1479
1506
|
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1507
|
+
const promise = (async () => {
|
|
1508
|
+
const id = this._genMsgId();
|
|
1509
|
+
const node = new BinaryNode('iq', {
|
|
1510
|
+
id,
|
|
1511
|
+
to: jid,
|
|
1512
|
+
type: 'get',
|
|
1513
|
+
xmlns: 'w:g2'
|
|
1514
|
+
}, [new BinaryNode('query', { request: 'interactive' }, null)]);
|
|
1515
|
+
|
|
1516
|
+
const response = await this._sendIq(node);
|
|
1517
|
+
if (!response) return null;
|
|
1518
|
+
|
|
1519
|
+
// Response may be the group node itself or wrap it
|
|
1520
|
+
const groupNode = (response.description === 'group')
|
|
1521
|
+
? response
|
|
1522
|
+
: findChild(response, 'group');
|
|
1523
|
+
const meta = this._parseGroupNode(groupNode);
|
|
1524
|
+
if (meta) this._groupMetaCache.set(jid, meta);
|
|
1525
|
+
return meta;
|
|
1526
|
+
})();
|
|
1527
|
+
|
|
1528
|
+
this._groupMetaInflight.set(jid, promise);
|
|
1529
|
+
try {
|
|
1530
|
+
return await promise;
|
|
1531
|
+
} finally {
|
|
1532
|
+
this._groupMetaInflight.delete(jid);
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
// Drop cached metadata for a group so the next read re-queries the server.
|
|
1537
|
+
invalidateGroupMetadata(groupJid) {
|
|
1538
|
+
const jid = makeJid(groupJid);
|
|
1539
|
+
this._groupMetaCache.del(jid);
|
|
1540
|
+
this._groupMetaInflight.delete(jid);
|
|
1541
|
+
_whaDbg('[DBG] GROUP_META invalidated ' + jid);
|
|
1485
1542
|
}
|
|
1486
1543
|
|
|
1487
1544
|
// Fetch ALL groups the connected account participates in
|
|
@@ -2909,10 +2966,23 @@ class WhalibmobClient extends EventEmitter {
|
|
|
2909
2966
|
type: 'set'
|
|
2910
2967
|
}, [new BinaryNode('create', baseAttrs, createChildren)]);
|
|
2911
2968
|
const resp = await this._sendIq(node);
|
|
2912
|
-
|
|
2969
|
+
// These used to return null indistinguishably, which surfaced to the CLI as
|
|
2970
|
+
// a bare "no response" no matter what actually went wrong.
|
|
2971
|
+
if (!resp) {
|
|
2972
|
+
throw new Error('createGroup: no reply from server (IQ timed out)');
|
|
2973
|
+
}
|
|
2974
|
+
if (resp.attrs && resp.attrs.type === 'error') {
|
|
2975
|
+
const errNode = findChild(resp, 'error');
|
|
2976
|
+
const code = errNode && errNode.attrs && errNode.attrs.code;
|
|
2977
|
+
const text = errNode && errNode.attrs && errNode.attrs.text;
|
|
2978
|
+
throw new Error('createGroup: server rejected the request' +
|
|
2979
|
+
(code ? ' (' + code + (text ? ' ' + text : '') + ')' : ''));
|
|
2980
|
+
}
|
|
2913
2981
|
const createNode = findChild(resp, 'create');
|
|
2914
2982
|
const groupNode = createNode ? findChild(createNode, 'group') : findChildDeep(resp, 'group');
|
|
2915
|
-
if (!groupNode)
|
|
2983
|
+
if (!groupNode) {
|
|
2984
|
+
throw new Error('createGroup: server reply contained no <group> node');
|
|
2985
|
+
}
|
|
2916
2986
|
return this._parseGroupNode(groupNode);
|
|
2917
2987
|
}
|
|
2918
2988
|
|