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 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
- _writeString(value, parts);
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
- if (jid.agent || jid.device) {
102
- parts.push(Buffer.from([TAG.AD_JID, jid.agent || 0, jid.device || 0]));
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
- return { user, agent, device, server: 's.whatsapp.net', toString() { return `${user}@s.whatsapp.net`; } };
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, retryCount}
143
- this._retryPreKeyIdx = 0; // rotating index for assigning unique prekeys to retries
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
- // Assign a stable, unique prekey index per msgId on first retry
961
- let pkIdx = existing ? existing.pkIdx : -1;
962
- if (pkIdx < 0 && this._signal) {
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
- pkIdx = this._retryPreKeyIdx++ % Math.max(1, allKeys.length);
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, pkIdx });
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[pkIdx] || allPreKeys[0];
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
- return {
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
- async getGroupMetadata(groupJid) {
1468
- const jid = makeJid(groupJid);
1469
- const id = this._genMsgId();
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
- const response = await this._sendIq(node);
1478
- if (!response) return null;
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
- // Response may be the group node itself or wrap it
1481
- const groupNode = (response.description === 'group')
1482
- ? response
1483
- : findChild(response, 'group');
1484
- return this._parseGroupNode(groupNode);
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
- if (!resp) return null;
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) return null;
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "whalibmob",
3
- "version": "5.5.63",
3
+ "version": "5.5.65",
4
4
  "description": "WhatsApp library for interaction with WhatsApp Mobile API no web",
5
5
  "author": "Kunboruto50",
6
6
  "main": "index.js",