whalibmob 5.7.0 → 5.7.1

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/README.md CHANGED
@@ -150,6 +150,7 @@ npm install -g whalibmob
150
150
  - [Handling Reconnects](#handling-reconnects)
151
151
  - [Knowing Which Mode You Are In](#knowing-which-mode-you-are-in)
152
152
  - [Two Sessions on One Number](#two-sessions-on-one-number)
153
+ - [The Number WhatsApp Files Your Account Under](#the-number-whatsapp-files-your-account-under)
153
154
  - [Routing Registration Through a Proxy](#routing-registration-through-a-proxy)
154
155
  - [Saving & Restoring Sessions](#saving--restoring-sessions)
155
156
  - [Signal Store Utilities](#signal-store-utilities)
@@ -807,6 +808,53 @@ await client.connectWeb(PHONE) // web / companion, over WebSocket
807
808
 
808
809
  Use two `WhalibmobClient` instances if you want both at once.
809
810
 
811
+ ## The Number WhatsApp Files Your Account Under
812
+
813
+ WhatsApp does not always keep an account under the number you type. Brazilian mobiles are the standing example: they gained a ninth digit, and WhatsApp keeps older accounts under the eight-digit form. `5596976042705` and `559676042705` are the same account, but only the second is the one the server recognises.
814
+
815
+ This matters because the number in your session is sent as the username on every connection. Get the form wrong and the server has no registration matching it, so the login is refused with `401` — a code that says "logged out" and gives no hint that the number was the problem.
816
+
817
+ whalibmob handles this on its own, in both directions:
818
+
819
+ **When you register**, the canonical form is read from the server's reply and the session is saved under it. Numbers registered through whalibmob cannot end up in this state.
820
+
821
+ **When you connect** an older session that is in this state, the first login is refused, whalibmob asks the server which form it uses, re-files the session, and connects. You see one line:
822
+
823
+ ```
824
+ connecting to +5596976042705...
825
+ this account is registered as +559676042705 (not +5596976042705) — session updated
826
+ connected as +559676042705
827
+ ```
828
+
829
+ The rename keeps the registration and the Signal keys — it moves the session files, it does not re-register anything.
830
+
831
+ ```js
832
+ client.on('number_corrected', ({ from, to }) => {
833
+ console.log('session re-filed:', from, '→', to)
834
+ })
835
+ ```
836
+
837
+ To be told rather than fixed:
838
+
839
+ ```js
840
+ const client = new WhalibmobClient({ sessionDir, autoFixNumber: false })
841
+ ```
842
+
843
+ Then the `401` is thrown as-is, and `checkSessionAlive()` tells you what the server calls the account:
844
+
845
+ ```js
846
+ const probe = await client.checkSessionAlive()
847
+ // { alive: true, status: 'ok', current: '5596976042705',
848
+ // canonical: '559676042705', mismatch: true }
849
+
850
+ if (probe.mismatch) {
851
+ await client.adoptCanonicalNumber(probe.canonical)
852
+ }
853
+ ```
854
+
855
+ > [!NOTE]
856
+ > The check costs nothing when things work — it runs only after a login has already been refused.
857
+
810
858
  ## Routing Registration Through a Proxy
811
859
 
812
860
  Registration is the part of the protocol most likely to be refused from a datacenter IP. If you are seeing security blocks or an undeterminable number status, route it through a SOCKS proxy — Tor, or a residential provider.
package/cli.js CHANGED
@@ -747,6 +747,14 @@ async function doConnect(phone) {
747
747
  const client = new WhalibmobClient({ sessionDir: _sessDir });
748
748
  attachEvents(client);
749
749
 
750
+ // The session may be re-filed mid-connect when WhatsApp turns out to keep the
751
+ // account under a different form of the number. Everything from here on has
752
+ // to use the corrected one, including the prompt.
753
+ client.on('number_corrected', ({ from, to }) => {
754
+ phone = to;
755
+ out(' this account is registered as +' + to + ' (not +' + from + ') — session updated');
756
+ });
757
+
750
758
  client.once('connected', () => {
751
759
  _client = client;
752
760
  _phone = phone;
@@ -783,19 +791,17 @@ async function doConnect(phone) {
783
791
  const probe = await client.checkSessionAlive();
784
792
 
785
793
  if (probe.mismatch) {
786
- // The session is filed under a number the server does not use for
787
- // this account, so the handshake has been sending a username no
788
- // registration matches. Renaming fixes it; re-registering would not.
794
+ // Reached only when the automatic correction inside init() could not
795
+ // run — a session already exists under the canonical number, or the
796
+ // rename failed. Say what it would have done.
789
797
  hr();
790
798
  out(' the session is filed under a number WhatsApp does not use');
791
799
  out(' session : +' + probe.current);
792
800
  out(' server : +' + probe.canonical);
793
801
  hr();
794
- out(' WhatsApp knows this account by the second form, so every login');
795
- out(' has been sent under a number it has no registration for.');
796
- out('');
797
- out(' fix it in place: /fixnumber');
798
- out(' (renames the session files, keeps the registration)');
802
+ out(' this is normally corrected automatically; it could not be here.');
803
+ out(' check whether a session for +' + probe.canonical + ' already exists,');
804
+ out(' then: /fixnumber');
799
805
  } else if (probe.alive) {
800
806
  out(' the number IS still registered, but this device was logged out.');
801
807
  out(' register it again: /reg code ' + phone);
package/lib/Client.js CHANGED
@@ -203,6 +203,11 @@ class WhalibmobClient extends EventEmitter {
203
203
  // client the server will not accept. Suppresses the reconnect loop.
204
204
  this._fatal = false;
205
205
  this._closing = false;
206
+ // A session whose number is not in WhatsApp's canonical form is re-filed
207
+ // automatically on the first refused login. Set { autoFixNumber: false } to
208
+ // be told about it instead.
209
+ this._autoFixNumber = opts.autoFixNumber !== false;
210
+ this._numberFixAttempted = false;
206
211
  this._pingTimer = null;
207
212
  this._keepTimer = null;
208
213
  this._connected = false;
@@ -316,10 +321,83 @@ class WhalibmobClient extends EventEmitter {
316
321
  }
317
322
  _whaDbg('[DBG] LID_RESTORED count=' + Object.keys(persistedLid).length);
318
323
 
319
- await this._connectSocket();
324
+ try {
325
+ await this._connectSocket();
326
+ } catch (err) {
327
+ // A session filed under a number WhatsApp does not use fails here and
328
+ // only here, with a 401 that says nothing about the number. Since we can
329
+ // ask the server which form it uses, there is no reason to make anyone
330
+ // find that out by hand.
331
+ const fixed = await this._tryCorrectPhoneNumber(err);
332
+ if (!fixed) throw err;
333
+ return this.init(fixed);
334
+ }
320
335
  return this;
321
336
  }
322
337
 
338
+ /**
339
+ * Last resort before reporting a refused login: check whether the session is
340
+ * simply filed under the wrong form of the number, and re-file it if so.
341
+ *
342
+ * Returns the corrected number when it did something, null otherwise. Only
343
+ * ever runs once per client — a second 401 after a rename is a real one.
344
+ */
345
+ async _tryCorrectPhoneNumber(err) {
346
+ if (this._autoFixNumber === false) return null;
347
+ if (this._numberFixAttempted) return null;
348
+ if (!err || !err.loggedOut || String(err.code) !== '401') return null;
349
+ this._numberFixAttempted = true;
350
+
351
+ let probe;
352
+ try {
353
+ probe = await this.checkSessionAlive();
354
+ } catch (_) {
355
+ return null;
356
+ }
357
+ if (!probe || !probe.mismatch || !probe.canonical) return null;
358
+
359
+ _whaDbg('[DBG] AUTO_FIX_NUMBER ' + probe.current + ' → ' + probe.canonical);
360
+
361
+ // Let go of the session files before moving them. The Signal store writes
362
+ // on a debounce, so a pending write would recreate the old file moments
363
+ // after the rename and leave two half-sessions on disk.
364
+ this._releaseSessionFiles();
365
+
366
+ try {
367
+ await this.adoptCanonicalNumber(probe.canonical);
368
+ } catch (renameErr) {
369
+ _whaDbg('[DBG] AUTO_FIX_NUMBER failed: ' + renameErr.message);
370
+ return null;
371
+ }
372
+
373
+ this.emit('number_corrected', {
374
+ from: probe.current,
375
+ to: probe.canonical,
376
+ reason: 'WhatsApp files this account under a different form of the number'
377
+ });
378
+ return probe.canonical;
379
+ }
380
+
381
+ // Flush and detach whatever is holding the current session's files open, so
382
+ // they can be renamed without a queued write putting one of them back.
383
+ _releaseSessionFiles() {
384
+ for (const store of [this._signal && this._signal.store,
385
+ this._signal && this._signal.senderKeyStore]) {
386
+ if (!store) continue;
387
+ try {
388
+ if (typeof store.detach === 'function') store.detach();
389
+ else {
390
+ if (typeof store._flushSync === 'function') store._flushSync();
391
+ if (store._saveTimer) { clearTimeout(store._saveTimer); store._saveTimer = null; }
392
+ store._filePath = null;
393
+ }
394
+ } catch (_) {}
395
+ }
396
+ this._signal = null;
397
+ this._devMgr = null;
398
+ this._tcTokenStore = null;
399
+ }
400
+
323
401
  async _connectSocket() {
324
402
  if (this._mode === 'web') return this._connectWebSocket();
325
403
 
@@ -93,10 +93,26 @@ class SenderKeyStore {
93
93
  this._dirty = false;
94
94
  this._saveTimer = null;
95
95
 
96
- const flush = () => this._flushSync();
97
- process.once('exit', flush);
98
- process.once('SIGTERM', () => { flush(); process.exit(0); });
99
- process.once('SIGINT', () => { flush(); process.exit(0); });
96
+ // Detachable, for the same reason as in SignalStore: a replaced store must
97
+ // not keep three process listeners alive for the rest of the run.
98
+ this._onExit = () => this._flushSync();
99
+ this._onSigTerm = () => { this._flushSync(); process.exit(0); };
100
+ this._onSigInt = () => { this._flushSync(); process.exit(0); };
101
+ process.once('exit', this._onExit);
102
+ process.once('SIGTERM', this._onSigTerm);
103
+ process.once('SIGINT', this._onSigInt);
104
+ }
105
+
106
+ /** Flush, stop writing, and stop holding the process open. */
107
+ detach() {
108
+ this._flushSync();
109
+ if (this._saveTimer) { clearTimeout(this._saveTimer); this._saveTimer = null; }
110
+ try {
111
+ process.removeListener('exit', this._onExit);
112
+ process.removeListener('SIGTERM', this._onSigTerm);
113
+ process.removeListener('SIGINT', this._onSigInt);
114
+ } catch (_) {}
115
+ this._filePath = null;
100
116
  }
101
117
 
102
118
  attachFile(filePath) {
@@ -36,11 +36,30 @@ class SignalStore {
36
36
  this._dirty = false;
37
37
  this._saveTimer = null;
38
38
 
39
- // Flush synchronously on exit so no state is ever lost
40
- const flush = () => this._flushSync();
41
- process.once('exit', flush);
42
- process.once('SIGTERM', () => { flush(); process.exit(0); });
43
- process.once('SIGINT', () => { flush(); process.exit(0); });
39
+ // Flush synchronously on exit so no state is ever lost.
40
+ //
41
+ // Kept as references so they can be taken off again: a store that has been
42
+ // replaced — when a session is re-filed under a different number, say —
43
+ // would otherwise stay reachable from process forever, and every store ever
44
+ // built would add three more listeners.
45
+ this._onExit = () => this._flushSync();
46
+ this._onSigTerm = () => { this._flushSync(); process.exit(0); };
47
+ this._onSigInt = () => { this._flushSync(); process.exit(0); };
48
+ process.once('exit', this._onExit);
49
+ process.once('SIGTERM', this._onSigTerm);
50
+ process.once('SIGINT', this._onSigInt);
51
+ }
52
+
53
+ /** Flush, stop writing, and stop holding the process open. */
54
+ detach() {
55
+ this._flushSync();
56
+ if (this._saveTimer) { clearTimeout(this._saveTimer); this._saveTimer = null; }
57
+ try {
58
+ process.removeListener('exit', this._onExit);
59
+ process.removeListener('SIGTERM', this._onSigTerm);
60
+ process.removeListener('SIGINT', this._onSigInt);
61
+ } catch (_) {}
62
+ this._filePath = null;
44
63
  }
45
64
 
46
65
  init(identityKeyPair, registrationId) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "whalibmob",
3
- "version": "5.7.0",
3
+ "version": "5.7.1",
4
4
  "description": "WhatsApp library for interaction with WhatsApp Mobile API no web",
5
5
  "author": "Kunboruto50",
6
6
  "main": "index.js",