whalibmob 5.6.9 → 5.7.0

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/cli.js CHANGED
@@ -316,6 +316,9 @@ function ts() {
316
316
  let _client = null;
317
317
  let _phone = null;
318
318
  let _sessDir = defaultSessionDir();
319
+ // Set when a failed connect turned out to be a number-form mismatch, so
320
+ // /fixnumber knows what it is fixing without probing the server again.
321
+ let _pendingFix = null;
319
322
  let _rl = null;
320
323
 
321
324
  // ─── shell help ───────────────────────────────────────────────────────────────
@@ -438,6 +441,7 @@ const HELP = `
438
441
  Connection
439
442
  /connect <phone> [sms|pair] connect to WhatsApp (asks which if unset)
440
443
  /pair <phone> [code] link to an existing account by 8-digit code
444
+ /fixnumber re-file a session under the number WhatsApp uses
441
445
  /disconnect disconnect
442
446
  /reconnect force reconnection
443
447
  /session show session info
@@ -777,7 +781,22 @@ async function doConnect(phone) {
777
781
  out(' checking whether the number is still registered...');
778
782
  try {
779
783
  const probe = await client.checkSessionAlive();
780
- if (probe.alive) {
784
+
785
+ 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.
789
+ hr();
790
+ out(' the session is filed under a number WhatsApp does not use');
791
+ out(' session : +' + probe.current);
792
+ out(' server : +' + probe.canonical);
793
+ 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)');
799
+ } else if (probe.alive) {
781
800
  out(' the number IS still registered, but this device was logged out.');
782
801
  out(' register it again: /reg code ' + phone);
783
802
  } else if (probe.error) {
@@ -786,6 +805,8 @@ async function doConnect(phone) {
786
805
  out(' the registration is gone server-side (' + (probe.status || 'no status') + ').');
787
806
  out(' register it again: /reg code ' + phone);
788
807
  }
808
+ // keep the client around so /fixnumber has a store to work from
809
+ _pendingFix = probe.mismatch ? { client, probe } : null;
789
810
  } catch (probeErr) {
790
811
  out(' status check failed: ' + probeErr.message);
791
812
  }
@@ -878,6 +899,21 @@ async function handleLine(line) {
878
899
  break;
879
900
  }
880
901
 
902
+ case '/fixnumber': {
903
+ if (!_pendingFix) {
904
+ fail('nothing to fix — run /connect <phone> first and let it report a mismatch');
905
+ break;
906
+ }
907
+ const { client, probe } = _pendingFix;
908
+ out('renaming session +' + probe.current + ' → +' + probe.canonical + '...');
909
+ const r = await client.adoptCanonicalNumber(probe.canonical);
910
+ out(' moved ' + r.files.length + ' file(s)');
911
+ _pendingFix = null;
912
+ out('now connect with the number WhatsApp uses:');
913
+ out(' /connect ' + r.phoneNumber);
914
+ break;
915
+ }
916
+
881
917
  case '/pair': {
882
918
  const ph = p[1] || _phone;
883
919
  if (!ph) {
@@ -1682,9 +1718,18 @@ async function handleLine(line) {
1682
1718
  const finalStore = r.store || store;
1683
1719
  finalStore.registered = true;
1684
1720
  finalStore.codePending = false;
1685
- saveStore(finalStore, file);
1686
- out('registered session saved to ' + file);
1687
- out('now run: /connect ' + ph);
1721
+ // Save under the number WhatsApp filed the account as, not the one
1722
+ // that was typed — they differ often enough to matter.
1723
+ const savedPhone = String(finalStore.phoneNumber || ph);
1724
+ const savedFile = path.join(_sessDir, `${savedPhone}.json`);
1725
+ saveStore(finalStore, savedFile);
1726
+ if (r.canonicalPhoneNumber) {
1727
+ out('note: WhatsApp knows this account as +' + r.canonicalPhoneNumber +
1728
+ ', not +' + r.typedPhoneNumber);
1729
+ out(' the session is saved under that number');
1730
+ }
1731
+ out('registered session saved to ' + savedFile);
1732
+ out('now run: /connect ' + savedPhone);
1688
1733
  } else {
1689
1734
  fail('verification failed ' + JSON.stringify(r));
1690
1735
  }
@@ -2057,7 +2102,7 @@ async function main() {
2057
2102
  }
2058
2103
 
2059
2104
  if (flags.register !== undefined) {
2060
- const ph = phone || normalizePhone(pos[0] || '');
2105
+ let ph = phone || normalizePhone(pos[0] || '');
2061
2106
  const code = flags.code;
2062
2107
  if (!ph) { fail('phone number required'); process.exit(1); }
2063
2108
  if (!code) { fail('--code is required'); process.exit(1); }
@@ -2071,9 +2116,16 @@ async function main() {
2071
2116
  const finalStore = r.store || store;
2072
2117
  finalStore.registered = true;
2073
2118
  finalStore.codePending = false;
2074
- saveStore(finalStore, file);
2075
- out('registered session saved to ' + file);
2076
- out('run: wa connect ' + ph);
2119
+ const savedPhone = String(finalStore.phoneNumber || ph);
2120
+ const savedFile = path.join(_sessDir, `${savedPhone}.json`);
2121
+ saveStore(finalStore, savedFile);
2122
+ if (r.canonicalPhoneNumber) {
2123
+ out('note: WhatsApp knows this account as +' + r.canonicalPhoneNumber +
2124
+ ', not +' + r.typedPhoneNumber);
2125
+ }
2126
+ out('registered session saved to ' + savedFile);
2127
+ out('run: wa connect ' + savedPhone);
2128
+ ph = savedPhone;
2077
2129
  } else {
2078
2130
  out(' status ' + (r && r.status ? r.status : JSON.stringify(r)));
2079
2131
  }
package/lib/Client.js CHANGED
@@ -332,7 +332,7 @@ class WhalibmobClient extends EventEmitter {
332
332
  socket.on('open', (sn) => this._onOpen(sn));
333
333
  socket.on('node', node => this._onNode(node));
334
334
  socket.on('close', () => this._onClose());
335
- socket.on('error', err => this.emit('error', err));
335
+ socket.on('error', err => this._onSocketError(err));
336
336
 
337
337
  // Pass reconnect attempt count into the Noise handshake ClientPayload
338
338
  this._store.connectAttemptCount = this._reconnectTry;
@@ -478,12 +478,35 @@ class WhalibmobClient extends EventEmitter {
478
478
  });
479
479
  socket.on('node', node => this._onNode(node));
480
480
  socket.on('close', () => this._onClose());
481
- socket.on('error', err => this.emit('error', err));
481
+ socket.on('error', err => this._onSocketError(err));
482
482
 
483
483
  await socket.connect();
484
484
  return socket;
485
485
  }
486
486
 
487
+ /**
488
+ * A socket-level error, which includes the server refusing the handshake.
489
+ *
490
+ * The refusal arrives here rather than as a <failure> stanza — it happens
491
+ * before the stream is open, so _handleFailure never sees it and never set
492
+ * the flag that stops the reconnect. The result was a client that retried a
493
+ * rejected login every second, forever, printing the same 401 each time.
494
+ * Anything the handshake marked as a spent session stops the loop here.
495
+ */
496
+ _onSocketError(err) {
497
+ if (err && err.loggedOut) {
498
+ _whaDbg('[DBG] HANDSHAKE_REJECTED code=' + err.code + ' — not retrying');
499
+ this._fatal = true;
500
+ this.emit('auth_failure', {
501
+ reason: String(err.code || '401'),
502
+ location: null,
503
+ loggedOut: true,
504
+ message: err.message
505
+ });
506
+ }
507
+ this.emit('error', err);
508
+ }
509
+
487
510
  /**
488
511
  * Ask WhatsApp for an 8-character pairing code.
489
512
  *
@@ -795,12 +818,85 @@ class WhalibmobClient extends EventEmitter {
795
818
  try {
796
819
  const res = await checkIfRegistered(this._store);
797
820
  const status = (res && (res.status || res.reason)) || null;
798
- return { alive: status === 'ok', status, raw: res };
821
+
822
+ // The server answers with the number in its own canonical form. When that
823
+ // differs from what the session is filed under, the handshake has been
824
+ // presenting a username no registration matches — which is a 401 with a
825
+ // completely different remedy from "register again".
826
+ //
827
+ // Brazil is where this bites: mobile numbers gained a ninth digit, and
828
+ // WhatsApp keeps older accounts under the eight-digit form. Type the
829
+ // number as it appears on the phone and you get a session whose username
830
+ // the server has never heard of.
831
+ const canonical = res && res.login ? String(res.login).replace(/\D/g, '') : null;
832
+ const current = String(this._store.phoneNumber || '');
833
+ const mismatch = !!(canonical && canonical !== current);
834
+
835
+ return {
836
+ alive: status === 'ok',
837
+ status,
838
+ canonical,
839
+ current,
840
+ mismatch,
841
+ raw: res
842
+ };
799
843
  } catch (err) {
800
844
  return { alive: false, status: null, raw: null, error: err.message };
801
845
  }
802
846
  }
803
847
 
848
+ /**
849
+ * Re-file this session under the number the server actually knows it by.
850
+ *
851
+ * Renames the session and every file that hangs off it, then reloads. Only
852
+ * touches the primary (SMS) session — a companion link is keyed by the
853
+ * account, not by the form of the number.
854
+ *
855
+ * @param {string} canonical the digits the server returned as `login`
856
+ */
857
+ async adoptCanonicalNumber(canonical) {
858
+ canonical = String(canonical || '').replace(/\D/g, '');
859
+ if (!canonical) throw new Error('adoptCanonicalNumber: no number given');
860
+ const current = String(this._store && this._store.phoneNumber || '');
861
+ if (canonical === current) return { renamed: false, phoneNumber: current };
862
+
863
+ const wasConnected = this._connected;
864
+ if (wasConnected) this.disconnect();
865
+
866
+ // Every per-session file shares the phone number as its stem.
867
+ const SUFFIXES = ['.json', '.signal.json', '.sk.json', '.tctoken.json',
868
+ '.device-cache.json', '.lid-mapping.json',
869
+ '.lid-reverse-mapping.json', '.history.json', '.messages.json'];
870
+ const moved = [];
871
+ for (const suffix of SUFFIXES) {
872
+ const from = path.join(this._sessionDir, current + suffix);
873
+ const to = path.join(this._sessionDir, canonical + suffix);
874
+ if (!fs.existsSync(from)) continue;
875
+ if (fs.existsSync(to)) {
876
+ throw new Error('Cannot rename session: ' + canonical + suffix +
877
+ ' already exists. Move it aside first.');
878
+ }
879
+ fs.renameSync(from, to);
880
+ moved.push(suffix);
881
+ }
882
+ if (!moved.length) throw new Error('No session files found for ' + current);
883
+
884
+ // The number is inside the store as well as in the filename.
885
+ const sessionFile = path.join(this._sessionDir, canonical + '.json');
886
+ const store = loadStore(sessionFile);
887
+ if (!store) throw new Error('Session file unreadable after rename');
888
+ store.phoneNumber = canonical;
889
+ saveStore(store, sessionFile);
890
+
891
+ _whaDbg('[DBG] ADOPT_CANONICAL ' + current + ' → ' + canonical +
892
+ ' files=' + moved.length);
893
+
894
+ this._store = store;
895
+ this._phoneNumber = canonical;
896
+ this._fatal = false;
897
+ return { renamed: true, phoneNumber: canonical, files: moved };
898
+ }
899
+
804
900
  // ─── Reconnection ─────────────────────────────────────────────────────────
805
901
 
806
902
  _scheduleReconnect() {
@@ -912,7 +912,23 @@ async function verifyCode(store, code) {
912
912
  const result = await sendRequest('/register', store, waVersion, true, ['code', normalized]);
913
913
 
914
914
  const status = result.status;
915
- if (status === 'ok' || status === 'sent' || status === 'verified') return result;
915
+ if (status === 'ok' || status === 'sent' || status === 'verified') {
916
+ // Adopt the number in the server's own form.
917
+ //
918
+ // WhatsApp answers with `login`, the canonical digits it filed the account
919
+ // under, and that is not always what was typed. Brazilian mobiles are the
920
+ // standing example: they gained a ninth digit, WhatsApp keeps the account
921
+ // under the eight-digit form, and a session saved under the typed number
922
+ // sends a username on every connection that matches no registration. The
923
+ // handshake then fails with 401 and nothing in the message hints at why.
924
+ const canonical = result.login ? String(result.login).replace(/\D/g, '') : null;
925
+ if (canonical && canonical !== String(store.phoneNumber)) {
926
+ result.canonicalPhoneNumber = canonical;
927
+ result.typedPhoneNumber = String(store.phoneNumber);
928
+ store.phoneNumber = canonical;
929
+ }
930
+ return result;
931
+ }
916
932
 
917
933
  const reason = result.reason || '';
918
934
  if (reason === 'missing') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "whalibmob",
3
- "version": "5.6.9",
3
+ "version": "5.7.0",
4
4
  "description": "WhatsApp library for interaction with WhatsApp Mobile API no web",
5
5
  "author": "Kunboruto50",
6
6
  "main": "index.js",