whalibmob 5.6.3 → 5.6.8

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/.env.example CHANGED
@@ -61,11 +61,23 @@
61
61
 
62
62
 
63
63
  # ─── Proxy ───────────────────────────────────────────────────────────────────
64
- # Route registration traffic through a SOCKS5 proxy, given as host:port.
65
- # TOR_PROXY takes precedence over SOCKS_PROXY when both are set.
66
- # Default Tor SOCKS port is 9050.
64
+ # Route registration traffic through a SOCKS proxy. Requires the `socks`
65
+ # package: npm install socks
66
+ #
67
+ # A bare host:port is assumed to be socks5. Add a scheme to be explicit, and
68
+ # credentials if the proxy needs them. Percent-encode any @ or : in a password.
69
+ #
70
+ # TOR_PROXY takes precedence over SOCKS_PROXY when both are set.
71
+ # The default Tor SOCKS port is 9050; the SOCKS default is 1080.
72
+ #
67
73
  # TOR_PROXY=127.0.0.1:9050
68
- # SOCKS_PROXY=127.0.0.1:1080
74
+ # TOR_PROXY=socks5://127.0.0.1:9050
75
+ # SOCKS_PROXY=socks5://user:pass@proxy.example.com:1080
76
+ # SOCKS_PROXY=socks5h://proxy.example.com:1080
77
+ # SOCKS_PROXY=socks4://proxy.example.com:1080
78
+ #
79
+ # Only if `socks` lives somewhere require() cannot find it:
80
+ # WA_SOCKS_LIB=/absolute/path/to/socks/build/index.js
69
81
 
70
82
 
71
83
  # ─── Frida attestation bridge ────────────────────────────────────────────────
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
+ - [Routing Registration Through a Proxy](#routing-registration-through-a-proxy)
153
154
  - [Saving & Restoring Sessions](#saving--restoring-sessions)
154
155
  - [Signal Store Utilities](#signal-store-utilities)
155
156
  - [makeCacheableSignalKeyStore](#makecacheablesignalkeystore)
@@ -806,6 +807,40 @@ await client.connectWeb(PHONE) // web / companion, over WebSocket
806
807
 
807
808
  Use two `WhalibmobClient` instances if you want both at once.
808
809
 
810
+ ## Routing Registration Through a Proxy
811
+
812
+ 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.
813
+
814
+ Install the optional dependency and set one environment variable:
815
+
816
+ ```sh
817
+ npm install socks
818
+ ```
819
+
820
+ ```sh
821
+ # Tor
822
+ export TOR_PROXY=socks5://127.0.0.1:9050
823
+
824
+ # a residential proxy that needs credentials
825
+ export SOCKS_PROXY=socks5://user:pass@proxy.example.com:1080
826
+ ```
827
+
828
+ A bare `host:port` is assumed to be SOCKS5, so `TOR_PROXY=127.0.0.1:9050` works too. `socks5`, `socks5h`, `socks4` and `socks4a` are all accepted, and `TOR_PROXY` wins if both variables are set.
829
+
830
+ If the password contains an `@` or a `:`, percent-encode it — `p@ss:word` becomes `p%40ss%3Aword`. whalibmob decodes it before handing it to the proxy.
831
+
832
+ ```js
833
+ // or from code, before you call any registration function
834
+ process.env.SOCKS_PROXY = 'socks5://user:pass@proxy.example.com:1080'
835
+
836
+ await requestSmsCode(phone, store)
837
+ ```
838
+
839
+ > [!NOTE]
840
+ > This covers the registration traffic to `v.whatsapp.net`. The message socket and media transfers are not proxied — if you need those behind a proxy as well, open an issue describing the setup.
841
+
842
+ If `socks` is not installed, or a proxy is unreachable, you get a message saying so rather than a silent failure. In the rare case the package lives somewhere `require()` cannot find it, `WA_SOCKS_LIB` takes an absolute path to it.
843
+
809
844
  ## Saving & Restoring Sessions
810
845
 
811
846
  Sessions are automatically persisted to disk as JSON files under the `sessionDir` you provide. The file is named `<phone>.json`. On the next `client.init()` call the session is restored and no re-registration is needed.
package/cli.js CHANGED
@@ -576,7 +576,8 @@ function attachEvents(client) {
576
576
  ? 'link again with /pair'
577
577
  : 're-register with /reg code';
578
578
  fail('session revoked: ' + f.reason + ' — ' + how);
579
- _rl && (_rl.resume(), _rl.prompt(true));
579
+ _rl && _rl.resume();
580
+ notConnected();
580
581
  });
581
582
 
582
583
  // The server declined the client, not the session. Nothing to re-register.
@@ -585,7 +586,8 @@ function attachEvents(client) {
585
586
  fail('rejected by WhatsApp (' + r.reason +
586
587
  (r.location ? ', edge ' + r.location : '') + ')');
587
588
  out(' ' + r.message);
588
- _rl && (_rl.resume(), _rl.prompt(true));
589
+ _rl && _rl.resume();
590
+ notConnected();
589
591
  });
590
592
 
591
593
  client.on('error', (e) => {
@@ -715,7 +717,7 @@ async function doConnectWeb(phone, opts) {
715
717
  client.once('auth_failure', () => clearInterval(keepAlive));
716
718
  } catch (e) {
717
719
  fail(e.message);
718
- _rl.prompt();
720
+ notConnected();
719
721
  }
720
722
  }
721
723
 
@@ -735,7 +737,7 @@ async function doConnect(phone) {
735
737
  client.once('auth_failure', () => {
736
738
  fail('auth failed — session revoked');
737
739
  out('use /reg code ' + phone + ' to re-register');
738
- _rl.prompt();
740
+ notConnected();
739
741
  });
740
742
 
741
743
  try {
@@ -745,11 +747,48 @@ async function doConnect(phone) {
745
747
  client.once('auth_failure', () => clearInterval(keepAlive));
746
748
  } catch (e) {
747
749
  fail(e.message);
748
- out('register first with: /reg code ' + phone);
749
- _rl.prompt();
750
+ if (/No (primary )?session for/i.test(e.message)) {
751
+ if (hasWebSession(phone)) {
752
+ out(' this number is linked as a companion — use: /pair ' + phone);
753
+ } else {
754
+ out(' register it: /reg code ' + phone + ' then /reg confirm ' + phone + ' <code>');
755
+ out(' or link it to an account already on a phone: /pair ' + phone);
756
+ }
757
+ } else if (e.code === '401' || /auth failure 401/i.test(e.message)) {
758
+ // 401 says the credentials were refused; it does not say whether the
759
+ // number itself is still registered. Ask, so the next step is obvious.
760
+ out(' checking whether the number is still registered...');
761
+ try {
762
+ const probe = await client.checkSessionAlive();
763
+ if (probe.alive) {
764
+ out(' the number IS still registered, but this device was logged out.');
765
+ out(' register it again: /reg code ' + phone);
766
+ } else if (probe.error) {
767
+ out(' could not reach the registration endpoint: ' + probe.error);
768
+ } else {
769
+ out(' the registration is gone server-side (' + (probe.status || 'no status') + ').');
770
+ out(' register it again: /reg code ' + phone);
771
+ }
772
+ } catch (probeErr) {
773
+ out(' status check failed: ' + probeErr.message);
774
+ }
775
+ }
776
+ notConnected();
750
777
  }
751
778
  }
752
779
 
780
+ // Put the shell back in its disconnected state.
781
+ //
782
+ // The prompt is the only standing indication of whether there is a session, so
783
+ // it has to be right after a failure: a prompt reading "wa +<number>>" under an
784
+ // error saying there is no session for that number is the shell contradicting
785
+ // itself.
786
+ function notConnected() {
787
+ _client = null;
788
+ _phone = null;
789
+ if (_rl) { _rl.setPrompt('wa> '); _rl.prompt(); }
790
+ }
791
+
753
792
  // ─── guard ────────────────────────────────────────────────────────────────────
754
793
 
755
794
  function requireConn() {
@@ -2039,7 +2078,11 @@ async function main() {
2039
2078
  const method = flags.pair || forced === 'pair' || forced === 'pairing' ? 'pairing'
2040
2079
  : flags.sms || forced === 'sms' ? 'sms'
2041
2080
  : await askLoginMethod(phone);
2042
- openShell('wa +' + phone + '> ');
2081
+ // Plain `wa>` until the connection actually opens. Naming the number in the
2082
+ // prompt before that says "connected as this number" while the line above
2083
+ // it says there is no session, which is the opposite of what happened —
2084
+ // doConnect sets the real prompt from the 'connected' event.
2085
+ openShell();
2043
2086
  out('connecting to +' + phone + '...');
2044
2087
  if (method === 'pairing') await doConnectWeb(phone);
2045
2088
  else await doConnect(phone);
@@ -2053,7 +2096,7 @@ async function main() {
2053
2096
  if (!phone) { fail('phone number required'); process.exit(1); }
2054
2097
  const custom = (sub ? pos[0] : pos[1]) ||
2055
2098
  (typeof flags.code === 'string' ? flags.code : undefined);
2056
- openShell('wa +' + phone + '> ');
2099
+ openShell();
2057
2100
  out('linking +' + phone + ' to an existing WhatsApp account...');
2058
2101
  await doConnectWeb(phone, { customCode: custom });
2059
2102
  return;
package/lib/Client.js CHANGED
@@ -4,6 +4,7 @@ const { dbg: _whaDbg, configureLogger: _whaConfigLogger } = require('./logger');
4
4
 
5
5
  const EventEmitter = require('events');
6
6
  const path = require('path');
7
+ const fs = require('fs');
7
8
  const crypto = require('crypto');
8
9
  const { NoiseSocket } = require('./noise');
9
10
  const { MessageSender, generateMessageId, makeJid, buildOrGetAdvIdentity } = require('./messages/MessageSender');
@@ -278,7 +279,19 @@ class WhalibmobClient extends EventEmitter {
278
279
  const sessionFile = path.join(this._sessionDir, `${phoneNumber}.json`);
279
280
  this._store = loadStore(sessionFile);
280
281
  if (!this._store) {
281
- throw new Error(`No session for ${phoneNumber}. Run 'wa registration -R <code>' first.`);
282
+ // Say what is missing, and say it accurately. The old text pointed at
283
+ // 'wa registration -R', a flag this CLI has never accepted, so anyone who
284
+ // followed it got a second error instead of a registered number. A
285
+ // library has no business teaching CLI syntax either — the caller knows
286
+ // which interface it is; this only has to name the state.
287
+ const hasWeb = fs.existsSync(path.join(this._sessionDir, `${phoneNumber}.web.json`));
288
+ throw new Error(
289
+ hasWeb
290
+ ? `No primary session for ${phoneNumber}, but it is linked as a ` +
291
+ 'companion device — use connectWeb() rather than init().'
292
+ : `No session for ${phoneNumber} — register the number first, or ` +
293
+ 'link it to an existing account with connectWeb().'
294
+ );
282
295
  }
283
296
 
284
297
  const signalFile = path.join(this._sessionDir, `${phoneNumber}.signal.json`);
@@ -766,6 +779,28 @@ class WhalibmobClient extends EventEmitter {
766
779
  this._closing = false;
767
780
  }
768
781
 
782
+ /**
783
+ * Ask the server whether this session's registration is still alive.
784
+ *
785
+ * The read-only /exist probe, run with the credentials already on disk. It
786
+ * answers the question a 401 leaves open: whether the number is still
787
+ * registered and only this device was cut loose, or whether the registration
788
+ * is gone entirely. Sends nothing to the phone and requests no code, so it
789
+ * is safe to run on a failure.
790
+ *
791
+ * @returns {Promise<{alive: boolean, status: string|null, raw: object|null, error?: string}>}
792
+ */
793
+ async checkSessionAlive() {
794
+ if (!this._store) throw new Error('No session loaded');
795
+ try {
796
+ const res = await checkIfRegistered(this._store);
797
+ const status = (res && (res.status || res.reason)) || null;
798
+ return { alive: status === 'ok', status, raw: res };
799
+ } catch (err) {
800
+ return { alive: false, status: null, raw: null, error: err.message };
801
+ }
802
+ }
803
+
769
804
  // ─── Reconnection ─────────────────────────────────────────────────────────
770
805
 
771
806
  _scheduleReconnect() {
@@ -2140,10 +2175,17 @@ class WhalibmobClient extends EventEmitter {
2140
2175
  return;
2141
2176
  }
2142
2177
 
2143
- _whaDbg('[DBG] FAILURE reason=' + reason + ' location=' + loc);
2144
- this.emit('auth_failure', { reason, location: loc, node });
2145
- // The session is no longer valid — stop reconnecting and disconnect
2146
- this._fatal = true;
2178
+ // Not every failure kills the session. whatsmeow's IsLoggedOut() marks the
2179
+ // ones that do — 401, 403, 405, 406, 409 — and treats the rest as worth
2180
+ // another attempt. A 500 or a 503 is the server having a bad minute; giving
2181
+ // up on the session for that would be throwing away a working registration.
2182
+ const LOGGED_OUT = ['401', '403', '405', '406', '409'];
2183
+ const spent = LOGGED_OUT.includes(reason);
2184
+
2185
+ _whaDbg('[DBG] FAILURE reason=' + reason + ' location=' + loc +
2186
+ ' loggedOut=' + spent);
2187
+ this.emit('auth_failure', { reason, location: loc, loggedOut: spent, node });
2188
+ this._fatal = spent;
2147
2189
  this._reconnecting = false;
2148
2190
  this.disconnect();
2149
2191
  }
@@ -901,7 +901,10 @@ class DeviceManager {
901
901
  }
902
902
  }
903
903
 
904
- const deviceIds = (this._dcGet(cacheKey) || []).filter(d => d !== 0);
904
+ // Same rule as the phone-addressed path: skip the device we are, not the
905
+ // device that happens to be numbered zero.
906
+ const selfDevice = this._ownDeviceIndex();
907
+ const deviceIds = (this._dcGet(cacheKey) || []).filter(d => d !== selfDevice);
905
908
  const deviceJids = deviceIds.map(d => makeDeviceJid(lidUser, d, 'lid'));
906
909
  if (deviceJids.length === 0) return [];
907
910
 
@@ -1085,10 +1088,30 @@ class DeviceManager {
1085
1088
  // On the first call we query usync, on all subsequent calls we return
1086
1089
  // from cache — zero extra network round-trips.
1087
1090
  //
1091
+ // Which device index this client occupies.
1092
+ //
1093
+ // A device registered over SMS is the account's primary and is always 0. A
1094
+ // companion linked by pairing code occupies a numbered slot — and then
1095
+ // device 0 is the owner's phone, which is a recipient like any other.
1096
+ _ownDeviceIndex() {
1097
+ const store = this._client && this._client._store;
1098
+ return (store && store.deviceIndex) || 0;
1099
+ }
1100
+
1088
1101
  async ensureOwnDeviceSessions(ownPhone, signalProto, allowPkmsg = true) {
1102
+ // Everything the account owns except the device doing the sending.
1103
+ //
1104
+ // This used to drop device 0 unconditionally, which is right only while we
1105
+ // are device 0. As a companion it silently excluded the owner's own phone
1106
+ // from every message we sent: the recipient got it, the phone did not, and
1107
+ // it sat there showing "waiting for this message" forever. It also put our
1108
+ // participant list out of step with the server's, which is what the phash
1109
+ // mismatch on the ack was reporting.
1110
+ const selfDevice = this._ownDeviceIndex();
1111
+
1089
1112
  // Use cached own device list if it hasn't expired (5-min TTL matches _deviceCache)
1090
1113
  if (this._ownDeviceJids !== null && Date.now() < this._ownDeviceExpiry) {
1091
- const others = this._ownDeviceJids.filter(j => stripUser(j).device !== 0);
1114
+ const others = this._ownDeviceJids.filter(j => stripUser(j).device !== selfDevice);
1092
1115
  return this._ensureSessions(others, signalProto, allowPkmsg);
1093
1116
  }
1094
1117
 
@@ -1097,7 +1120,7 @@ class DeviceManager {
1097
1120
  this._ownDeviceJids = deviceJids;
1098
1121
  this._ownDeviceExpiry = Date.now() + 5 * 60 * 1000;
1099
1122
 
1100
- const others = deviceJids.filter(j => stripUser(j).device !== 0);
1123
+ const others = deviceJids.filter(j => stripUser(j).device !== selfDevice);
1101
1124
  return this._ensureSessions(others, signalProto, allowPkmsg);
1102
1125
  }
1103
1126
 
@@ -7,23 +7,109 @@ const curveJs = require('curve25519-js');
7
7
  const { getDeviceConfig } = require('./DeviceConfig');
8
8
  const attestation = require('./Attestation');
9
9
 
10
- // ---------- SOCKS5 / Tor support ----------
11
- // Set TOR_PROXY=socks5://127.0.0.1:9050 (or any socks5 host) to route all
12
- // WhatsApp registration traffic through Tor / a residential proxy.
13
- const SOCKS_LIB = '/home/runner/workspace/.config/npm/node_global/lib/node_modules/socks/build/index.js';
10
+ // ---------- SOCKS4 / SOCKS5 / Tor support ----------
11
+ //
12
+ // Set SOCKS_PROXY (or TOR_PROXY) to route registration traffic through a proxy:
13
+ //
14
+ // socks5://127.0.0.1:9050 Tor
15
+ // socks5://user:pass@host:1080 authenticated residential proxy
16
+ // socks5h://host:1080 resolve the destination at the proxy
17
+ // socks4://host:1080 older proxies
18
+ //
19
+ // Requires the `socks` package: npm install socks
20
+
21
+ const SOCKS_SCHEMES = {
22
+ 'socks:': 5,
23
+ 'socks5:': 5,
24
+ 'socks5h:': 5,
25
+ 'socks4:': 4,
26
+ 'socks4a:': 4
27
+ };
28
+
29
+ // Resolve the library the way every other dependency is resolved.
30
+ //
31
+ // This used to be a hardcoded absolute path into one particular machine's
32
+ // global npm directory. It resolved there and nowhere else, so proxy support
33
+ // was dead for everybody who installed whalibmob normally — the require threw
34
+ // MODULE_NOT_FOUND the moment a proxy was configured. WA_SOCKS_LIB stays as an
35
+ // escape hatch for unusual layouts, but it is an override now, not the only
36
+ // way in.
37
+ function loadSocks() {
38
+ const override = process.env.WA_SOCKS_LIB;
39
+ if (override) {
40
+ try { return require(override); }
41
+ catch (err) {
42
+ throw new Error('WA_SOCKS_LIB points at ' + override +
43
+ ' but it could not be loaded: ' + err.message);
44
+ }
45
+ }
46
+ try {
47
+ return require('socks');
48
+ } catch (_) {
49
+ throw new Error(
50
+ "A SOCKS proxy is configured but the 'socks' package is not installed. " +
51
+ 'Run: npm install socks'
52
+ );
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Parse a proxy URL into what SocksClient expects.
58
+ *
59
+ * Accepts the schemes above, with or without credentials, and defaults the
60
+ * port to 1080 the way every SOCKS client does.
61
+ */
62
+ function parseSocksProxy(proxyUrl) {
63
+ let raw = String(proxyUrl || '').trim();
64
+ if (!raw) throw new Error('No SOCKS proxy configured');
65
+
66
+ // A bare host:port is what .env.example has always shown and what most people
67
+ // type. Without a scheme `new URL` reads "127.0.0.1:" as the protocol and the
68
+ // rest as the path, so it has to be filled in before parsing.
69
+ if (!/^[a-z0-9+.-]+:\/\//i.test(raw)) raw = 'socks5://' + raw;
70
+
71
+ let url;
72
+ try { url = new URL(raw); }
73
+ catch (_) { throw new Error('Invalid SOCKS proxy URL: ' + proxyUrl); }
74
+
75
+ const type = SOCKS_SCHEMES[url.protocol];
76
+ if (!type) {
77
+ throw new Error('Unsupported proxy scheme "' + url.protocol + '" — use ' +
78
+ Object.keys(SOCKS_SCHEMES).map(s => s.replace(':', '')).join(', '));
79
+ }
80
+ if (!url.hostname) throw new Error('SOCKS proxy URL has no host: ' + proxyUrl);
81
+
82
+ const proxy = {
83
+ host: url.hostname,
84
+ port: parseInt(url.port, 10) || 1080,
85
+ type
86
+ };
87
+ // Credentials may be percent-encoded in a URL; a password with an @ or a :
88
+ // in it is common enough on paid proxies to be worth decoding.
89
+ if (url.username) proxy.userId = decodeURIComponent(url.username);
90
+ if (url.password) proxy.password = decodeURIComponent(url.password);
91
+ return proxy;
92
+ }
93
+
94
+ // TOR_PROXY wins when both are set — the precedence .env.example has always
95
+ // documented.
96
+ function socksProxyUrl() {
97
+ return process.env.TOR_PROXY || process.env.SOCKS_PROXY || '';
98
+ }
14
99
 
15
100
  async function httpPostViaSocks(path, body, waVersion, proxyUrl, authHeader) {
16
- const url = new URL(proxyUrl);
17
- const pHost = url.hostname;
18
- const pPort = parseInt(url.port) || 1080;
19
- const dHost = 'v.whatsapp.net';
20
- const dPort = 443;
101
+ const proxy = parseSocksProxy(proxyUrl);
102
+ const dHost = 'v.whatsapp.net';
103
+ const dPort = 443;
21
104
 
22
- const { SocksClient } = require(SOCKS_LIB);
105
+ const { SocksClient } = loadSocks();
23
106
  const { socket: rawSocket } = await SocksClient.createConnection({
24
- proxy: { host: pHost, port: pPort, type: 5 },
107
+ proxy,
25
108
  command: 'connect',
26
- destination: { host: dHost, port: dPort }
109
+ destination: { host: dHost, port: dPort },
110
+ // Without this a proxy that accepts the TCP connection and then says
111
+ // nothing leaves the whole registration hanging with no error.
112
+ timeout: 20000
27
113
  });
28
114
 
29
115
  const tlsSocket = tls.connect({
@@ -50,16 +136,25 @@ async function httpPostViaSocks(path, body, waVersion, proxyUrl, authHeader) {
50
136
  `Content-Length: ${Buffer.byteLength(body)}`
51
137
  ];
52
138
  if (authHeader) reqLines.push(`Authorization: ${authHeader}`);
53
- reqLines.push(`Connection: keep-alive`, '', body);
139
+ // close, not keep-alive: the response is read by waiting for the server to
140
+ // end the stream. Asking it to keep the connection open means that never
141
+ // happens and the request hangs until something else times out.
142
+ reqLines.push(`Connection: close`, '', body);
54
143
  const req = reqLines.join('\r\n');
55
144
 
56
145
  tlsSocket.write(req);
57
146
 
58
147
  const chunks = [];
59
148
  await new Promise((res, rej) => {
60
- tlsSocket.on('data', d => chunks.push(d));
61
- tlsSocket.on('end', res);
62
- tlsSocket.on('error', rej);
149
+ const timer = setTimeout(() => {
150
+ tlsSocket.destroy();
151
+ rej(new Error('SOCKS proxy timed out reading ' + path));
152
+ }, 30000);
153
+ const finish = (fn, arg) => { clearTimeout(timer); fn(arg); };
154
+ tlsSocket.on('data', d => chunks.push(d));
155
+ tlsSocket.on('end', () => finish(res));
156
+ tlsSocket.on('close', () => finish(res));
157
+ tlsSocket.on('error', e => finish(rej, e));
63
158
  });
64
159
 
65
160
  const raw = Buffer.concat(chunks);
@@ -510,7 +605,7 @@ function encryptPayload(plaintext) {
510
605
  // ---------- HTTP ----------
511
606
 
512
607
  function httpPost(path, body, waVersion, authHeader) {
513
- const proxy = process.env.TOR_PROXY || process.env.SOCKS_PROXY || '';
608
+ const proxy = socksProxyUrl();
514
609
  if (proxy) return httpPostViaSocks(path, body, waVersion, proxy, authHeader);
515
610
 
516
611
  return new Promise((resolve, reject) => {
@@ -841,4 +936,4 @@ async function verifyCode(store, code) {
841
936
  throw new Error(`Verification failed: ${reason || JSON.stringify(result)}`);
842
937
  }
843
938
 
844
- module.exports = { checkIfRegistered, checkNumberStatus, requestSmsCode, verifyCode, fetchIosVersion, fetchAndroidVersion, fetchWaVersion, parsePhone, getCountryMeta, assertRegistrationKeys };
939
+ module.exports = { checkIfRegistered, checkNumberStatus, requestSmsCode, verifyCode, fetchIosVersion, fetchAndroidVersion, fetchWaVersion, parsePhone, getCountryMeta, assertRegistrationKeys, parseSocksProxy, socksProxyUrl };
@@ -1259,8 +1259,8 @@ class MessageSender {
1259
1259
  : Promise.resolve([]),
1260
1260
  // Our own linked devices, in the address family the stanza is routed with.
1261
1261
  // ensureOwnDeviceSessions hands back @s.whatsapp.net JIDs, which cannot go
1262
- // into a stanza carrying addressing_mode='lid'; both helpers already leave
1263
- // out device 0.
1262
+ // into a stanza carrying addressing_mode='lid'; both helpers leave out the
1263
+ // device we are, which is device 0 only while we are the primary.
1264
1264
  useLid
1265
1265
  ? this._devMgr.ensureOwnLidDeviceSessions(myLidUser, ownPhone, this._signal)
1266
1266
  : this._devMgr.ensureOwnDeviceSessions(ownPhone, this._signal)
@@ -1275,8 +1275,17 @@ class MessageSender {
1275
1275
  const s = String(jid);
1276
1276
  return s.endsWith('@hosted') || s.endsWith('@hosted.lid');
1277
1277
  };
1278
+ // The sending device counts in the enumeration even though it is not a
1279
+ // recipient — and that is *this* device, not device 0. On a companion link
1280
+ // the bare JID names the owner's phone, which arrives through ownTargets
1281
+ // like every other device; adding it here again while leaving our own slot
1282
+ // out would hash a set the server never computes.
1283
+ const ownSelfJid = (useLid
1284
+ ? this._client._ownDeviceLidJid()
1285
+ : this._client._ownDeviceJid()) || selfJid;
1286
+
1278
1287
  const enumeratedDevices = [...new Set(
1279
- [...memberDevices, ...lidDevices, ...ownTargets, selfJid]
1288
+ [...memberDevices, ...lidDevices, ...ownTargets, ownSelfJid]
1280
1289
  )].filter(jid => !isHostedJid(jid));
1281
1290
 
1282
1291
  // The participant hash the server checks the fan-out against. whatsmeow puts
package/lib/noise.js CHANGED
@@ -444,9 +444,44 @@ class NoiseSocket extends EventEmitter {
444
444
 
445
445
  if (tag === 'failure') {
446
446
  const attrs = (node && node.attrs) || {};
447
- const reason = attrs.reason || attrs.location || '401';
448
- const err = new Error('WhatsApp auth failure: ' + reason);
447
+ const reason = String(attrs.reason || attrs.location || '401');
448
+ // Say what the code means. "auth failure: 401" sends people looking
449
+ // for a bug in the client; the credentials were rejected by the
450
+ // server, which is a different thing and has different remedies.
451
+ //
452
+ // The names and numbers are whatsmeow's ConnectFailureReason, which
453
+ // is the only place they are written down — WhatsApp publishes none
454
+ // of this. Codes marked "wipe" are the ones its IsLoggedOut() covers:
455
+ // the session is finished and retrying with it is pointless.
456
+ const FAILURE = {
457
+ '400': { wipe: false, text: 'generic failure.' },
458
+ '401': { wipe: true, text: 'logged out. The session was unpaired ' +
459
+ 'server-side — nothing in the session file can revive it. ' +
460
+ 'For a linked device this means it was removed under ' +
461
+ 'Linked Devices; for a registered number it means the ' +
462
+ 'registration was taken over, usually by verifying the ' +
463
+ 'same number on the official app.' },
464
+ '402': { wipe: false, text: 'temporarily banned.' },
465
+ '403': { wipe: true, text: "the account's primary device is gone " +
466
+ '— it is no longer a multi-device account.' },
467
+ '405': { wipe: true, text: 'client outdated — the announced ' +
468
+ 'version is not one the server accepts.' },
469
+ '406': { wipe: true, text: 'banned.' },
470
+ '409': { wipe: true, text: 'bad user agent — the client identified ' +
471
+ 'itself in a way the server rejects.' },
472
+ '413': { wipe: false, text: 'auth token expired.' },
473
+ '414': { wipe: false, text: 'auth token invalid.' },
474
+ '415': { wipe: false, text: 'not found.' },
475
+ '500': { wipe: false, text: 'server error — worth retrying.' },
476
+ '503': { wipe: false, text: 'service unavailable — worth retrying.' }
477
+ };
478
+ const info = FAILURE[reason];
479
+ const err = new Error('WhatsApp auth failure ' + reason +
480
+ (info ? ' — ' + info.text : ''));
449
481
  err.code = reason;
482
+ // True when the session is spent and the caller should register or
483
+ // link again rather than retry.
484
+ err.loggedOut = !!(info && info.wipe);
450
485
  if (this._connectReject) {
451
486
  this._connectReject(err);
452
487
  this._connectResolve = null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "whalibmob",
3
- "version": "5.6.3",
3
+ "version": "5.6.8",
4
4
  "description": "WhatsApp library for interaction with WhatsApp Mobile API no web",
5
5
  "author": "Kunboruto50",
6
6
  "main": "index.js",
@@ -51,6 +51,7 @@
51
51
  "ws": "^8.21.1"
52
52
  },
53
53
  "optionalDependencies": {
54
- "jimp": "^1.6.0"
54
+ "jimp": "^1.6.0",
55
+ "socks": "^2.8.9"
55
56
  }
56
57
  }