whalibmob 5.14.6 → 5.14.7

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.
Files changed (2) hide show
  1. package/lib/Client.js +141 -17
  2. package/package.json +1 -1
package/lib/Client.js CHANGED
@@ -171,6 +171,17 @@ const PING_INTERVAL_MS = 25000;
171
171
  const KEEPALIVE_INTERVAL = 20000;
172
172
  const RECONNECT_BACKOFF = [1000, 2000, 4000, 8000, 15000, 30000];
173
173
 
174
+ // When the server is down to this many one-time keys or fewer, send it more.
175
+ // The number is small on purpose: the server tells us when it is running low,
176
+ // and this is the floor for the case where that notice never arrives.
177
+ const PRE_KEY_SERVER_MIN = 5;
178
+
179
+ // How often to ask the server how many it has left. The drain is invisible from
180
+ // here — every bundle handed out is a key gone, and none of that reaches this
181
+ // side — so on a session that stays up for days this is the only thing that
182
+ // notices before the pool is empty and the account stops being reachable.
183
+ const PRE_KEY_CHECK_INTERVAL = 30 * 60 * 1000; // 30 minutes
184
+
174
185
  // ─── Key helpers ──────────────────────────────────────────────────────────────
175
186
 
176
187
  function intToBytes(n, byteLen) {
@@ -1234,7 +1245,9 @@ class WhalibmobClient extends EventEmitter {
1234
1245
  this.setOnline(true);
1235
1246
 
1236
1247
  this._requestMediaConnection();
1237
- this._uploadPreKeys();
1248
+ // Ask what the server still has rather than uploading blind. It answers
1249
+ // with a number this side has no other way of learning.
1250
+ this._ensureServerPreKeys('login');
1238
1251
 
1239
1252
  // Same telemetry ping the web client sends on every completed login.
1240
1253
  this._sendUnifiedSession();
@@ -1500,9 +1513,23 @@ class WhalibmobClient extends EventEmitter {
1500
1513
  }
1501
1514
 
1502
1515
  if (collections.size === 0) return;
1503
- this.syncAppState([...collections]).catch(err => {
1504
- _whaDbg('[DBG] APP_STATE sync failed: ' + (err && err.message));
1505
- });
1516
+
1517
+ // The bit has to be cleared whatever came of the sync. A session that
1518
+ // cannot sync at all — no key has ever been shared with it, which is every
1519
+ // session that registered its own number — would otherwise leave it set
1520
+ // forever and be told about the same change on every single connection.
1521
+ const clearAll = () => {
1522
+ for (const type of dirtyTypes) {
1523
+ if (COLLECTION_MAP[type]) this._cleanDirtyBits(type);
1524
+ }
1525
+ };
1526
+
1527
+ this.syncAppState([...collections])
1528
+ .then(clearAll)
1529
+ .catch(err => {
1530
+ _whaDbg('[DBG] APP_STATE sync failed: ' + (err && err.message));
1531
+ clearAll();
1532
+ });
1506
1533
  }
1507
1534
 
1508
1535
  /**
@@ -1920,11 +1947,20 @@ class WhalibmobClient extends EventEmitter {
1920
1947
  }, [new BinaryNode('ping', {}, null)]));
1921
1948
  }
1922
1949
  }, KEEPALIVE_INTERVAL);
1950
+
1951
+ // The server is supposed to say when it is running out of one-time keys,
1952
+ // and mostly does. This is for when it does not: a session that stays up
1953
+ // long enough can be drained without a word, and the first sign of it is
1954
+ // that nobody can reach the account any more.
1955
+ this._preKeyTimer = setInterval(() => {
1956
+ if (this._socket && this._connected) this._ensureServerPreKeys('periodic');
1957
+ }, PRE_KEY_CHECK_INTERVAL);
1923
1958
  }
1924
1959
 
1925
1960
  _stopTimers() {
1926
- if (this._pingTimer) { clearInterval(this._pingTimer); this._pingTimer = null; }
1927
- if (this._keepTimer) { clearInterval(this._keepTimer); this._keepTimer = null; }
1961
+ if (this._pingTimer) { clearInterval(this._pingTimer); this._pingTimer = null; }
1962
+ if (this._keepTimer) { clearInterval(this._keepTimer); this._keepTimer = null; }
1963
+ if (this._preKeyTimer) { clearInterval(this._preKeyTimer); this._preKeyTimer = null; }
1928
1964
  }
1929
1965
 
1930
1966
  // ─── Node dispatch ────────────────────────────────────────────────────────
@@ -3005,15 +3041,28 @@ class WhalibmobClient extends EventEmitter {
3005
3041
  // whole dispatch down with it, so every notification behind it in the
3006
3042
  // offline queue was dropped too.
3007
3043
  const fromJid = String(attrs.from || '');
3008
- const fromPhone = fromJid.split('@')[0].split(':')[0];
3009
- if (fromPhone && this._devMgr) {
3010
- this._devMgr._dcDel([fromPhone]);
3011
- _whaDbg('[DBG] NOTIF_DEVICES invalidated cache for ' + fromPhone);
3044
+ const fromUser = fromJid.split('@')[0].split(':')[0];
3045
+ // The user segment of a LID is not a phone number, and usync only takes
3046
+ // phone numbers — asking it about a LID comes back "invalid" and the
3047
+ // device list is never actually refreshed. Translate first when we know
3048
+ // the mapping, and when we do not, invalidate the cache and stop there
3049
+ // rather than spending an IQ on a question that cannot be answered.
3050
+ const isLid = fromJid.endsWith('@lid');
3051
+ const mappedPn = isLid && this._lidToPn ? this._lidToPn.get(fromUser) : null;
3052
+ const fromPhone = isLid ? mappedPn : fromUser;
3053
+ if (fromUser && this._devMgr) {
3054
+ this._devMgr._dcDel(isLid ? [fromUser, 'lid:' + fromUser] : [fromUser]);
3055
+ if (mappedPn) this._devMgr._dcDel([mappedPn]);
3056
+ _whaDbg('[DBG] NOTIF_DEVICES invalidated cache for ' + fromUser +
3057
+ (isLid ? (mappedPn ? ' (lid → ' + mappedPn + ')' : ' (lid, no phone known)') : ''));
3012
3058
  // Also process any inline <devices> list the server included
3013
3059
  const devicesNode = findChildDeep(node, 'devices');
3014
3060
  if (devicesNode) this._processDeviceUpdate(devicesNode);
3015
- // Background re-usync so the cache is warm before the next send
3016
- if (this._signal) {
3061
+ // Background re-usync so the cache is warm before the next send. Only
3062
+ // when there is a phone number to ask about — the cache is already
3063
+ // cleared, so a LID with no known number simply gets looked up the next
3064
+ // time something sends to it.
3065
+ if (this._signal && fromPhone) {
3017
3066
  setImmediate(() => {
3018
3067
  if (!this._connected || !this._devMgr) return;
3019
3068
  this._devMgr.bulkEnsureSessions([fromPhone], this._signal, false)
@@ -3320,15 +3369,90 @@ class WhalibmobClient extends EventEmitter {
3320
3369
  ]));
3321
3370
  }
3322
3371
 
3323
- _checkAndReplenishPreKeys() {
3372
+ // How many one-time keys the server is still holding for us.
3373
+ //
3374
+ // This is the number that matters and the one we could not see. The local
3375
+ // store only loses a key when a pkmsg using it is actually decrypted, while
3376
+ // the server hands one out to everybody who asks for a bundle — including the
3377
+ // ones who then never write. So the two drift apart, and the local count can
3378
+ // sit at its full 800 while the server has none left. With none left nobody
3379
+ // can open a session with this account at all: the messages are not delayed,
3380
+ // they are never sent, which is why nothing arrives and nothing is queued.
3381
+ //
3382
+ // The two client families ask differently, so each is asked its own way. A
3383
+ // companion asks for a count and is told a number. A primary asks for the
3384
+ // digest and is given the ids it holds, which is the same answer with the
3385
+ // arithmetic left to us.
3386
+ async _queryServerPreKeyCount() {
3387
+ if (!this._socket || !this._connected) return null;
3388
+
3389
+ const web = this._mode === 'web';
3390
+ const resp = await this._sendIq(new BinaryNode('iq', {
3391
+ id: this._genMsgId(),
3392
+ to: 's.whatsapp.net',
3393
+ type: 'get',
3394
+ xmlns: 'encrypt'
3395
+ }, [new BinaryNode(web ? 'count' : 'digest', {}, null)]));
3396
+
3397
+ if (!resp) return null; // timed out
3398
+ if (resp.attrs && resp.attrs.type === 'error') return null;
3399
+
3400
+ if (web) {
3401
+ const countNode = findChild(resp, 'count');
3402
+ const value = countNode && countNode.attrs && countNode.attrs.value;
3403
+ const n = value === undefined ? NaN : Number(value);
3404
+ return Number.isFinite(n) ? n : null;
3405
+ }
3406
+
3407
+ // <digest><list><...one child per key we still have there...></list></digest>
3408
+ const digest = findChild(resp, 'digest');
3409
+ const list = digest && findChild(digest, 'list');
3410
+ if (!list) return null;
3411
+ return Array.isArray(list.content) ? list.content.length : 0;
3412
+ }
3413
+
3414
+ // Top the server back up when it is running out. Never throws: a session that
3415
+ // cannot ask is left exactly as it was rather than losing its connection over
3416
+ // a bookkeeping query.
3417
+ async _ensureServerPreKeys(reason) {
3324
3418
  if (!this._signal) return;
3325
- const count = this._signal.preKeyCount();
3326
- if (count < 20) {
3327
- const newKeys = this._signal.replenishPreKeys();
3328
- if (newKeys.length > 0) this._uploadPreKeys();
3419
+ try {
3420
+ const serverCount = await this._queryServerPreKeyCount();
3421
+ if (serverCount === null) {
3422
+ // No usable answer. Fall back to the old behaviour — an upload we may
3423
+ // not have needed costs nothing next to an account nobody can reach.
3424
+ _whaDbg('[DBG] PREKEY server count unavailable (' + reason + ') — uploading anyway');
3425
+ this._replenishLocalPreKeys();
3426
+ this._uploadPreKeys();
3427
+ return;
3428
+ }
3429
+
3430
+ _whaDbg('[DBG] PREKEY server holds ' + serverCount + ' (' + reason + ')');
3431
+ if (serverCount > PRE_KEY_SERVER_MIN) return;
3432
+
3433
+ _whaDbg('[DBG] PREKEY server low (' + serverCount + ' <= ' + PRE_KEY_SERVER_MIN +
3434
+ ') — uploading');
3435
+ this._replenishLocalPreKeys();
3436
+ this._uploadPreKeys();
3437
+ } catch (err) {
3438
+ _whaDbg('[DBG] PREKEY check failed (' + reason + '): ' + (err && err.message));
3329
3439
  }
3330
3440
  }
3331
3441
 
3442
+ // Make sure there is something to upload before uploading.
3443
+ _replenishLocalPreKeys() {
3444
+ if (!this._signal) return;
3445
+ if (this._signal.preKeyCount() < 20) this._signal.replenishPreKeys();
3446
+ }
3447
+
3448
+ _checkAndReplenishPreKeys() {
3449
+ if (!this._signal) return;
3450
+ // The server said it is running low, so ask it how low and act on that
3451
+ // rather than on the local count, which does not move when the server
3452
+ // hands a key out.
3453
+ this._ensureServerPreKeys('server asked');
3454
+ }
3455
+
3332
3456
  // ─── IQ helper (send + await response) ────────────────────────────────────
3333
3457
 
3334
3458
  _sendIq(node) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "whalibmob",
3
- "version": "5.14.6",
3
+ "version": "5.14.7",
4
4
  "description": "WhatsApp library for interaction with WhatsApp Mobile API and web ",
5
5
  "author": "Kunboruto20",
6
6
  "main": "index.js",