whalibmob 5.6.9 → 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 +48 -0
- package/cli.js +66 -8
- package/lib/Client.js +178 -4
- package/lib/Registration.js +17 -1
- package/lib/signal/SenderKey.js +20 -4
- package/lib/signal/SignalStore.js +24 -5
- package/package.json +1 -1
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
|
@@ -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
|
|
@@ -743,6 +747,14 @@ async function doConnect(phone) {
|
|
|
743
747
|
const client = new WhalibmobClient({ sessionDir: _sessDir });
|
|
744
748
|
attachEvents(client);
|
|
745
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
|
+
|
|
746
758
|
client.once('connected', () => {
|
|
747
759
|
_client = client;
|
|
748
760
|
_phone = phone;
|
|
@@ -777,7 +789,20 @@ async function doConnect(phone) {
|
|
|
777
789
|
out(' checking whether the number is still registered...');
|
|
778
790
|
try {
|
|
779
791
|
const probe = await client.checkSessionAlive();
|
|
780
|
-
|
|
792
|
+
|
|
793
|
+
if (probe.mismatch) {
|
|
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.
|
|
797
|
+
hr();
|
|
798
|
+
out(' the session is filed under a number WhatsApp does not use');
|
|
799
|
+
out(' session : +' + probe.current);
|
|
800
|
+
out(' server : +' + probe.canonical);
|
|
801
|
+
hr();
|
|
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');
|
|
805
|
+
} else if (probe.alive) {
|
|
781
806
|
out(' the number IS still registered, but this device was logged out.');
|
|
782
807
|
out(' register it again: /reg code ' + phone);
|
|
783
808
|
} else if (probe.error) {
|
|
@@ -786,6 +811,8 @@ async function doConnect(phone) {
|
|
|
786
811
|
out(' the registration is gone server-side (' + (probe.status || 'no status') + ').');
|
|
787
812
|
out(' register it again: /reg code ' + phone);
|
|
788
813
|
}
|
|
814
|
+
// keep the client around so /fixnumber has a store to work from
|
|
815
|
+
_pendingFix = probe.mismatch ? { client, probe } : null;
|
|
789
816
|
} catch (probeErr) {
|
|
790
817
|
out(' status check failed: ' + probeErr.message);
|
|
791
818
|
}
|
|
@@ -878,6 +905,21 @@ async function handleLine(line) {
|
|
|
878
905
|
break;
|
|
879
906
|
}
|
|
880
907
|
|
|
908
|
+
case '/fixnumber': {
|
|
909
|
+
if (!_pendingFix) {
|
|
910
|
+
fail('nothing to fix — run /connect <phone> first and let it report a mismatch');
|
|
911
|
+
break;
|
|
912
|
+
}
|
|
913
|
+
const { client, probe } = _pendingFix;
|
|
914
|
+
out('renaming session +' + probe.current + ' → +' + probe.canonical + '...');
|
|
915
|
+
const r = await client.adoptCanonicalNumber(probe.canonical);
|
|
916
|
+
out(' moved ' + r.files.length + ' file(s)');
|
|
917
|
+
_pendingFix = null;
|
|
918
|
+
out('now connect with the number WhatsApp uses:');
|
|
919
|
+
out(' /connect ' + r.phoneNumber);
|
|
920
|
+
break;
|
|
921
|
+
}
|
|
922
|
+
|
|
881
923
|
case '/pair': {
|
|
882
924
|
const ph = p[1] || _phone;
|
|
883
925
|
if (!ph) {
|
|
@@ -1682,9 +1724,18 @@ async function handleLine(line) {
|
|
|
1682
1724
|
const finalStore = r.store || store;
|
|
1683
1725
|
finalStore.registered = true;
|
|
1684
1726
|
finalStore.codePending = false;
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1727
|
+
// Save under the number WhatsApp filed the account as, not the one
|
|
1728
|
+
// that was typed — they differ often enough to matter.
|
|
1729
|
+
const savedPhone = String(finalStore.phoneNumber || ph);
|
|
1730
|
+
const savedFile = path.join(_sessDir, `${savedPhone}.json`);
|
|
1731
|
+
saveStore(finalStore, savedFile);
|
|
1732
|
+
if (r.canonicalPhoneNumber) {
|
|
1733
|
+
out('note: WhatsApp knows this account as +' + r.canonicalPhoneNumber +
|
|
1734
|
+
', not +' + r.typedPhoneNumber);
|
|
1735
|
+
out(' the session is saved under that number');
|
|
1736
|
+
}
|
|
1737
|
+
out('registered session saved to ' + savedFile);
|
|
1738
|
+
out('now run: /connect ' + savedPhone);
|
|
1688
1739
|
} else {
|
|
1689
1740
|
fail('verification failed ' + JSON.stringify(r));
|
|
1690
1741
|
}
|
|
@@ -2057,7 +2108,7 @@ async function main() {
|
|
|
2057
2108
|
}
|
|
2058
2109
|
|
|
2059
2110
|
if (flags.register !== undefined) {
|
|
2060
|
-
|
|
2111
|
+
let ph = phone || normalizePhone(pos[0] || '');
|
|
2061
2112
|
const code = flags.code;
|
|
2062
2113
|
if (!ph) { fail('phone number required'); process.exit(1); }
|
|
2063
2114
|
if (!code) { fail('--code is required'); process.exit(1); }
|
|
@@ -2071,9 +2122,16 @@ async function main() {
|
|
|
2071
2122
|
const finalStore = r.store || store;
|
|
2072
2123
|
finalStore.registered = true;
|
|
2073
2124
|
finalStore.codePending = false;
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2125
|
+
const savedPhone = String(finalStore.phoneNumber || ph);
|
|
2126
|
+
const savedFile = path.join(_sessDir, `${savedPhone}.json`);
|
|
2127
|
+
saveStore(finalStore, savedFile);
|
|
2128
|
+
if (r.canonicalPhoneNumber) {
|
|
2129
|
+
out('note: WhatsApp knows this account as +' + r.canonicalPhoneNumber +
|
|
2130
|
+
', not +' + r.typedPhoneNumber);
|
|
2131
|
+
}
|
|
2132
|
+
out('registered session saved to ' + savedFile);
|
|
2133
|
+
out('run: wa connect ' + savedPhone);
|
|
2134
|
+
ph = savedPhone;
|
|
2077
2135
|
} else {
|
|
2078
2136
|
out(' status ' + (r && r.status ? r.status : JSON.stringify(r)));
|
|
2079
2137
|
}
|
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
|
-
|
|
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
|
|
|
@@ -332,7 +410,7 @@ class WhalibmobClient extends EventEmitter {
|
|
|
332
410
|
socket.on('open', (sn) => this._onOpen(sn));
|
|
333
411
|
socket.on('node', node => this._onNode(node));
|
|
334
412
|
socket.on('close', () => this._onClose());
|
|
335
|
-
socket.on('error', err => this.
|
|
413
|
+
socket.on('error', err => this._onSocketError(err));
|
|
336
414
|
|
|
337
415
|
// Pass reconnect attempt count into the Noise handshake ClientPayload
|
|
338
416
|
this._store.connectAttemptCount = this._reconnectTry;
|
|
@@ -478,12 +556,35 @@ class WhalibmobClient extends EventEmitter {
|
|
|
478
556
|
});
|
|
479
557
|
socket.on('node', node => this._onNode(node));
|
|
480
558
|
socket.on('close', () => this._onClose());
|
|
481
|
-
socket.on('error', err => this.
|
|
559
|
+
socket.on('error', err => this._onSocketError(err));
|
|
482
560
|
|
|
483
561
|
await socket.connect();
|
|
484
562
|
return socket;
|
|
485
563
|
}
|
|
486
564
|
|
|
565
|
+
/**
|
|
566
|
+
* A socket-level error, which includes the server refusing the handshake.
|
|
567
|
+
*
|
|
568
|
+
* The refusal arrives here rather than as a <failure> stanza — it happens
|
|
569
|
+
* before the stream is open, so _handleFailure never sees it and never set
|
|
570
|
+
* the flag that stops the reconnect. The result was a client that retried a
|
|
571
|
+
* rejected login every second, forever, printing the same 401 each time.
|
|
572
|
+
* Anything the handshake marked as a spent session stops the loop here.
|
|
573
|
+
*/
|
|
574
|
+
_onSocketError(err) {
|
|
575
|
+
if (err && err.loggedOut) {
|
|
576
|
+
_whaDbg('[DBG] HANDSHAKE_REJECTED code=' + err.code + ' — not retrying');
|
|
577
|
+
this._fatal = true;
|
|
578
|
+
this.emit('auth_failure', {
|
|
579
|
+
reason: String(err.code || '401'),
|
|
580
|
+
location: null,
|
|
581
|
+
loggedOut: true,
|
|
582
|
+
message: err.message
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
this.emit('error', err);
|
|
586
|
+
}
|
|
587
|
+
|
|
487
588
|
/**
|
|
488
589
|
* Ask WhatsApp for an 8-character pairing code.
|
|
489
590
|
*
|
|
@@ -795,12 +896,85 @@ class WhalibmobClient extends EventEmitter {
|
|
|
795
896
|
try {
|
|
796
897
|
const res = await checkIfRegistered(this._store);
|
|
797
898
|
const status = (res && (res.status || res.reason)) || null;
|
|
798
|
-
|
|
899
|
+
|
|
900
|
+
// The server answers with the number in its own canonical form. When that
|
|
901
|
+
// differs from what the session is filed under, the handshake has been
|
|
902
|
+
// presenting a username no registration matches — which is a 401 with a
|
|
903
|
+
// completely different remedy from "register again".
|
|
904
|
+
//
|
|
905
|
+
// Brazil is where this bites: mobile numbers gained a ninth digit, and
|
|
906
|
+
// WhatsApp keeps older accounts under the eight-digit form. Type the
|
|
907
|
+
// number as it appears on the phone and you get a session whose username
|
|
908
|
+
// the server has never heard of.
|
|
909
|
+
const canonical = res && res.login ? String(res.login).replace(/\D/g, '') : null;
|
|
910
|
+
const current = String(this._store.phoneNumber || '');
|
|
911
|
+
const mismatch = !!(canonical && canonical !== current);
|
|
912
|
+
|
|
913
|
+
return {
|
|
914
|
+
alive: status === 'ok',
|
|
915
|
+
status,
|
|
916
|
+
canonical,
|
|
917
|
+
current,
|
|
918
|
+
mismatch,
|
|
919
|
+
raw: res
|
|
920
|
+
};
|
|
799
921
|
} catch (err) {
|
|
800
922
|
return { alive: false, status: null, raw: null, error: err.message };
|
|
801
923
|
}
|
|
802
924
|
}
|
|
803
925
|
|
|
926
|
+
/**
|
|
927
|
+
* Re-file this session under the number the server actually knows it by.
|
|
928
|
+
*
|
|
929
|
+
* Renames the session and every file that hangs off it, then reloads. Only
|
|
930
|
+
* touches the primary (SMS) session — a companion link is keyed by the
|
|
931
|
+
* account, not by the form of the number.
|
|
932
|
+
*
|
|
933
|
+
* @param {string} canonical the digits the server returned as `login`
|
|
934
|
+
*/
|
|
935
|
+
async adoptCanonicalNumber(canonical) {
|
|
936
|
+
canonical = String(canonical || '').replace(/\D/g, '');
|
|
937
|
+
if (!canonical) throw new Error('adoptCanonicalNumber: no number given');
|
|
938
|
+
const current = String(this._store && this._store.phoneNumber || '');
|
|
939
|
+
if (canonical === current) return { renamed: false, phoneNumber: current };
|
|
940
|
+
|
|
941
|
+
const wasConnected = this._connected;
|
|
942
|
+
if (wasConnected) this.disconnect();
|
|
943
|
+
|
|
944
|
+
// Every per-session file shares the phone number as its stem.
|
|
945
|
+
const SUFFIXES = ['.json', '.signal.json', '.sk.json', '.tctoken.json',
|
|
946
|
+
'.device-cache.json', '.lid-mapping.json',
|
|
947
|
+
'.lid-reverse-mapping.json', '.history.json', '.messages.json'];
|
|
948
|
+
const moved = [];
|
|
949
|
+
for (const suffix of SUFFIXES) {
|
|
950
|
+
const from = path.join(this._sessionDir, current + suffix);
|
|
951
|
+
const to = path.join(this._sessionDir, canonical + suffix);
|
|
952
|
+
if (!fs.existsSync(from)) continue;
|
|
953
|
+
if (fs.existsSync(to)) {
|
|
954
|
+
throw new Error('Cannot rename session: ' + canonical + suffix +
|
|
955
|
+
' already exists. Move it aside first.');
|
|
956
|
+
}
|
|
957
|
+
fs.renameSync(from, to);
|
|
958
|
+
moved.push(suffix);
|
|
959
|
+
}
|
|
960
|
+
if (!moved.length) throw new Error('No session files found for ' + current);
|
|
961
|
+
|
|
962
|
+
// The number is inside the store as well as in the filename.
|
|
963
|
+
const sessionFile = path.join(this._sessionDir, canonical + '.json');
|
|
964
|
+
const store = loadStore(sessionFile);
|
|
965
|
+
if (!store) throw new Error('Session file unreadable after rename');
|
|
966
|
+
store.phoneNumber = canonical;
|
|
967
|
+
saveStore(store, sessionFile);
|
|
968
|
+
|
|
969
|
+
_whaDbg('[DBG] ADOPT_CANONICAL ' + current + ' → ' + canonical +
|
|
970
|
+
' files=' + moved.length);
|
|
971
|
+
|
|
972
|
+
this._store = store;
|
|
973
|
+
this._phoneNumber = canonical;
|
|
974
|
+
this._fatal = false;
|
|
975
|
+
return { renamed: true, phoneNumber: canonical, files: moved };
|
|
976
|
+
}
|
|
977
|
+
|
|
804
978
|
// ─── Reconnection ─────────────────────────────────────────────────────────
|
|
805
979
|
|
|
806
980
|
_scheduleReconnect() {
|
package/lib/Registration.js
CHANGED
|
@@ -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')
|
|
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/lib/signal/SenderKey.js
CHANGED
|
@@ -93,10 +93,26 @@ class SenderKeyStore {
|
|
|
93
93
|
this._dirty = false;
|
|
94
94
|
this._saveTimer = null;
|
|
95
95
|
|
|
96
|
-
|
|
97
|
-
process.
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
process
|
|
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) {
|