whalibmob 5.6.8 → 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 +77 -8
- package/lib/Client.js +203 -22
- package/lib/Registration.js +17 -1
- package/package.json +1 -1
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
|
|
@@ -580,6 +584,23 @@ function attachEvents(client) {
|
|
|
580
584
|
notConnected();
|
|
581
585
|
});
|
|
582
586
|
|
|
587
|
+
// Someone else opened a session for this number. Reconnecting would fight
|
|
588
|
+
// them for it, so the client stands down and says so.
|
|
589
|
+
client.on('connection_replaced', (r) => {
|
|
590
|
+
_rl && _rl.pause();
|
|
591
|
+
fail('connection replaced');
|
|
592
|
+
out(' ' + r.message);
|
|
593
|
+
_rl && _rl.resume();
|
|
594
|
+
notConnected();
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
// A stanza whose shape we mishandled. Not fatal — the connection kept going.
|
|
598
|
+
client.on('node_error', (e) => {
|
|
599
|
+
_rl && _rl.pause();
|
|
600
|
+
fail('could not handle a <' + e.tag + '>: ' + (e.err && e.err.message));
|
|
601
|
+
_rl && (_rl.resume(), _rl.prompt(true));
|
|
602
|
+
});
|
|
603
|
+
|
|
583
604
|
// The server declined the client, not the session. Nothing to re-register.
|
|
584
605
|
client.on('client_rejected', (r) => {
|
|
585
606
|
_rl && _rl.pause();
|
|
@@ -760,7 +781,22 @@ async function doConnect(phone) {
|
|
|
760
781
|
out(' checking whether the number is still registered...');
|
|
761
782
|
try {
|
|
762
783
|
const probe = await client.checkSessionAlive();
|
|
763
|
-
|
|
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) {
|
|
764
800
|
out(' the number IS still registered, but this device was logged out.');
|
|
765
801
|
out(' register it again: /reg code ' + phone);
|
|
766
802
|
} else if (probe.error) {
|
|
@@ -769,6 +805,8 @@ async function doConnect(phone) {
|
|
|
769
805
|
out(' the registration is gone server-side (' + (probe.status || 'no status') + ').');
|
|
770
806
|
out(' register it again: /reg code ' + phone);
|
|
771
807
|
}
|
|
808
|
+
// keep the client around so /fixnumber has a store to work from
|
|
809
|
+
_pendingFix = probe.mismatch ? { client, probe } : null;
|
|
772
810
|
} catch (probeErr) {
|
|
773
811
|
out(' status check failed: ' + probeErr.message);
|
|
774
812
|
}
|
|
@@ -861,6 +899,21 @@ async function handleLine(line) {
|
|
|
861
899
|
break;
|
|
862
900
|
}
|
|
863
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
|
+
|
|
864
917
|
case '/pair': {
|
|
865
918
|
const ph = p[1] || _phone;
|
|
866
919
|
if (!ph) {
|
|
@@ -1665,9 +1718,18 @@ async function handleLine(line) {
|
|
|
1665
1718
|
const finalStore = r.store || store;
|
|
1666
1719
|
finalStore.registered = true;
|
|
1667
1720
|
finalStore.codePending = false;
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
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);
|
|
1671
1733
|
} else {
|
|
1672
1734
|
fail('verification failed ' + JSON.stringify(r));
|
|
1673
1735
|
}
|
|
@@ -2040,7 +2102,7 @@ async function main() {
|
|
|
2040
2102
|
}
|
|
2041
2103
|
|
|
2042
2104
|
if (flags.register !== undefined) {
|
|
2043
|
-
|
|
2105
|
+
let ph = phone || normalizePhone(pos[0] || '');
|
|
2044
2106
|
const code = flags.code;
|
|
2045
2107
|
if (!ph) { fail('phone number required'); process.exit(1); }
|
|
2046
2108
|
if (!code) { fail('--code is required'); process.exit(1); }
|
|
@@ -2054,9 +2116,16 @@ async function main() {
|
|
|
2054
2116
|
const finalStore = r.store || store;
|
|
2055
2117
|
finalStore.registered = true;
|
|
2056
2118
|
finalStore.codePending = false;
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
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;
|
|
2060
2129
|
} else {
|
|
2061
2130
|
out(' status ' + (r && r.status ? r.status : JSON.stringify(r)));
|
|
2062
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.
|
|
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.
|
|
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
|
-
|
|
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() {
|
|
@@ -1019,6 +1115,29 @@ class WhalibmobClient extends EventEmitter {
|
|
|
1019
1115
|
// We do NOT decrypt the patches — that requires the full LTHASH machinery.
|
|
1020
1116
|
// We DO update our version counters from the server response so that we
|
|
1021
1117
|
// never re-request the same snapshot twice.
|
|
1118
|
+
/**
|
|
1119
|
+
* Acknowledge a dirty bit so the server stops announcing it.
|
|
1120
|
+
*
|
|
1121
|
+
* `<ib><dirty type="..."/></ib>` is the server saying something changed while
|
|
1122
|
+
* we were away. Whatever the client does about it, the bit has to be cleared
|
|
1123
|
+
* or the same announcement arrives on every connection.
|
|
1124
|
+
*
|
|
1125
|
+
* @param {string} type the dirty type, e.g. 'groups'
|
|
1126
|
+
* @param {number|string} [fromTs] the timestamp the announcement carried
|
|
1127
|
+
*/
|
|
1128
|
+
_cleanDirtyBits(type, fromTs) {
|
|
1129
|
+
if (!this._socket || !this._connected) return;
|
|
1130
|
+
const attrs = { type: String(type) };
|
|
1131
|
+
if (fromTs) attrs.timestamp = String(fromTs);
|
|
1132
|
+
_whaDbg('[DBG] CLEAN_DIRTY type=' + type + (fromTs ? ' t=' + fromTs : ''));
|
|
1133
|
+
this._socket.sendNode(new BinaryNode('iq', {
|
|
1134
|
+
id: this._genMsgId(),
|
|
1135
|
+
to: 's.whatsapp.net',
|
|
1136
|
+
type: 'set',
|
|
1137
|
+
xmlns: 'urn:xmpp:whatsapp:dirty'
|
|
1138
|
+
}, [new BinaryNode('clean', attrs, null)]));
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1022
1141
|
_sendAppStateSyncForTypes(dirtyTypes) {
|
|
1023
1142
|
if (!this._socket || !this._connected) return;
|
|
1024
1143
|
|
|
@@ -1037,7 +1156,11 @@ class WhalibmobClient extends EventEmitter {
|
|
|
1037
1156
|
for (const type of dirtyTypes) {
|
|
1038
1157
|
const mapped = COLLECTION_MAP[type];
|
|
1039
1158
|
if (!mapped) {
|
|
1040
|
-
|
|
1159
|
+
// "groups" is not an app-state collection, but it is still a dirty bit
|
|
1160
|
+
// the server expects to be cleared. Ignoring it meant the server
|
|
1161
|
+
// re-announced it on every single connection, forever.
|
|
1162
|
+
_whaDbg('[DBG] DIRTY type=' + type + ' — not an app-state collection, clearing the bit');
|
|
1163
|
+
this._cleanDirtyBits(type);
|
|
1041
1164
|
continue;
|
|
1042
1165
|
}
|
|
1043
1166
|
for (const c of mapped) collections.add(c);
|
|
@@ -1134,18 +1257,31 @@ class WhalibmobClient extends EventEmitter {
|
|
|
1134
1257
|
try { _whaDbg('[DBG] IQ_ERROR content=' + JSON.stringify(node.content)); } catch(_) {}
|
|
1135
1258
|
}
|
|
1136
1259
|
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1260
|
+
// One malformed stanza must not take the connection down with it.
|
|
1261
|
+
//
|
|
1262
|
+
// A throw here propagates out through the socket's frame reader, which
|
|
1263
|
+
// reports it as a socket error — so a single unhandled shape in one
|
|
1264
|
+
// notification looked like the transport failing, and every stanza queued
|
|
1265
|
+
// behind it in the same offline batch was lost. Contain it: log the stanza
|
|
1266
|
+
// that did it, tell the caller, carry on reading.
|
|
1267
|
+
try {
|
|
1268
|
+
if (tag === 'iq') this._handleIq(node);
|
|
1269
|
+
else if (tag === 'message') this._handleMessage(node);
|
|
1270
|
+
else if (tag === 'receipt') this._handleReceipt(node);
|
|
1271
|
+
else if (tag === 'ack') this._handleAck(node);
|
|
1272
|
+
else if (tag === 'notification') this._handleNotification(node);
|
|
1273
|
+
else if (tag === 'presence') this._handlePresence(node);
|
|
1274
|
+
else if (tag === 'call') this._handleCall(node);
|
|
1275
|
+
else if (tag === 'ib') this._handleIb(node);
|
|
1276
|
+
else if (tag === 'success') this.emit('session_refresh', { node }); // late success (re-auth)
|
|
1277
|
+
else if (tag === 'failure') this._handleFailure(node);
|
|
1278
|
+
else if (tag === 'stream:error') this._handleStreamError(node);
|
|
1279
|
+
else this.emit('node', node);
|
|
1280
|
+
} catch (err) {
|
|
1281
|
+
_whaDbg('[DBG] NODE_HANDLER_ERR tag=' + tag + ' err=' + (err && err.message) +
|
|
1282
|
+
' attrs=' + JSON.stringify(node.attrs || {}));
|
|
1283
|
+
this.emit('node_error', { tag, node, err });
|
|
1284
|
+
}
|
|
1149
1285
|
}
|
|
1150
1286
|
|
|
1151
1287
|
// A stream error ends the connection; the reconnect in _onClose picks it up.
|
|
@@ -1157,16 +1293,55 @@ class WhalibmobClient extends EventEmitter {
|
|
|
1157
1293
|
_handleStreamError(node) {
|
|
1158
1294
|
const attrs = (node && node.attrs) || {};
|
|
1159
1295
|
const code = attrs.code ? String(attrs.code) : null;
|
|
1160
|
-
const reason = attrs.reason || code || 'unknown';
|
|
1161
1296
|
|
|
1162
1297
|
if (code === '515') {
|
|
1163
1298
|
_whaDbg('[DBG] STREAM_ERROR 515 — restart required after pairing, reconnecting');
|
|
1164
|
-
this.emit('restart_required', { reason });
|
|
1299
|
+
this.emit('restart_required', { reason: '515' });
|
|
1300
|
+
return;
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
// Most stream errors carry no code at all — the reason is a child element,
|
|
1304
|
+
// and for <conflict> the type attribute on it says which kind. Reading only
|
|
1305
|
+
// the attributes reported every one of these as "code=null reason=unknown",
|
|
1306
|
+
// which is the same message for a session someone else took over and a
|
|
1307
|
+
// session that was deleted.
|
|
1308
|
+
const child = Array.isArray(node.content) && node.content[0] ? node.content[0] : null;
|
|
1309
|
+
const childTag = child && child.description ? child.description : null;
|
|
1310
|
+
const conflictType = childTag === 'conflict' && child.attrs
|
|
1311
|
+
? String(child.attrs.type || '') : null;
|
|
1312
|
+
|
|
1313
|
+
if (conflictType === 'replaced') {
|
|
1314
|
+
// Another client opened a session for this account and the server handed
|
|
1315
|
+
// it over. Reconnecting immediately just takes it back, and the two ends
|
|
1316
|
+
// trade the session until one of them is stopped — so this one stands
|
|
1317
|
+
// down and says why.
|
|
1318
|
+
_whaDbg('[DBG] STREAM_ERROR conflict/replaced — another client took the session');
|
|
1319
|
+
this._fatal = true;
|
|
1320
|
+
this.emit('connection_replaced', {
|
|
1321
|
+
message: 'Another client connected with this number and took over the ' +
|
|
1322
|
+
'session. Reconnecting would take it back and the two would ' +
|
|
1323
|
+
'keep displacing each other, so this connection has stopped. ' +
|
|
1324
|
+
'Close the other client, then connect again.'
|
|
1325
|
+
});
|
|
1326
|
+
this.disconnect();
|
|
1327
|
+
return;
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
if (conflictType === 'device_removed') {
|
|
1331
|
+
// The companion was unlinked from the phone. Same meaning as a 401.
|
|
1332
|
+
_whaDbg('[DBG] STREAM_ERROR conflict/device_removed — unlinked');
|
|
1333
|
+
this._fatal = true;
|
|
1334
|
+
this.emit('auth_failure', {
|
|
1335
|
+
reason: '401', location: null, loggedOut: true, node
|
|
1336
|
+
});
|
|
1337
|
+
this.disconnect();
|
|
1165
1338
|
return;
|
|
1166
1339
|
}
|
|
1167
1340
|
|
|
1168
|
-
|
|
1169
|
-
|
|
1341
|
+
const reason = attrs.reason || conflictType || childTag || code || 'unknown';
|
|
1342
|
+
_whaDbg('[DBG] STREAM_ERROR code=' + code + ' child=' + childTag +
|
|
1343
|
+
' conflict=' + conflictType + ' reason=' + reason);
|
|
1344
|
+
this.emit('stream_error', { reason, code, conflictType, childTag, node });
|
|
1170
1345
|
}
|
|
1171
1346
|
|
|
1172
1347
|
// ─── IQ handling ──────────────────────────────────────────────────────────
|
|
@@ -1956,7 +2131,13 @@ class WhalibmobClient extends EventEmitter {
|
|
|
1956
2131
|
// Server push: a contact's linked device list changed (they linked or
|
|
1957
2132
|
// unlinked a tablet, desktop, etc.). Invalidate that phone's cache entry
|
|
1958
2133
|
// so the next send triggers a fresh usync IQ and reaches all their devices.
|
|
1959
|
-
|
|
2134
|
+
// BinaryNode decodes an AD-encoded JID into an object, not a string, so
|
|
2135
|
+
// this has to be stringified before it can be split. It was not, and a
|
|
2136
|
+
// <notification type="devices"> from a LID-addressed contact threw
|
|
2137
|
+
// "fromJid.split is not a function" out of the node handler — taking the
|
|
2138
|
+
// whole dispatch down with it, so every notification behind it in the
|
|
2139
|
+
// offline queue was dropped too.
|
|
2140
|
+
const fromJid = String(attrs.from || '');
|
|
1960
2141
|
const fromPhone = fromJid.split('@')[0].split(':')[0];
|
|
1961
2142
|
if (fromPhone && this._devMgr) {
|
|
1962
2143
|
this._devMgr._dcDel([fromPhone]);
|
|
@@ -1980,7 +2161,7 @@ class WhalibmobClient extends EventEmitter {
|
|
|
1980
2161
|
// Server push: a contact re-registered WhatsApp (new identity key / new phone).
|
|
1981
2162
|
// Their old Signal sessions are no longer valid — clear them and the device
|
|
1982
2163
|
// cache so the next send builds a fresh pkmsg session from scratch.
|
|
1983
|
-
const fromJid = attrs.from || '';
|
|
2164
|
+
const fromJid = String(attrs.from || '');
|
|
1984
2165
|
const fromPhone = fromJid.split('@')[0].split(':')[0];
|
|
1985
2166
|
if (fromPhone && this._devMgr) {
|
|
1986
2167
|
this._devMgr._dcDel([fromPhone]);
|
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') {
|