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 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);
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
- return {
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
- 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)]);
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
- const response = await this._sendIq(node);
1478
- if (!response) return null;
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
- // 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);
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
- if (!resp) return null;
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) return null;
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "whalibmob",
3
- "version": "5.5.63",
3
+ "version": "5.5.64",
4
4
  "description": "WhatsApp library for interaction with WhatsApp Mobile API no web",
5
5
  "author": "Kunboruto50",
6
6
  "main": "index.js",