whalibmob 5.12.8 → 5.12.10
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 +24 -0
- package/cli.js +20 -1
- package/lib/Client.js +54 -5
- package/lib/Registration.js +41 -15
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -678,6 +678,11 @@ wa> /about Available 24/7 for support
|
|
|
678
678
|
about updated
|
|
679
679
|
```
|
|
680
680
|
|
|
681
|
+
`about updated` now means the about was read back from the server and matches
|
|
682
|
+
what you typed. When the server accepts the stanza but still reports something
|
|
683
|
+
else, it says so instead — and if the about is stored but hidden, it names the
|
|
684
|
+
privacy setting that is hiding it.
|
|
685
|
+
|
|
681
686
|
#### CLI Change Profile Picture
|
|
682
687
|
|
|
683
688
|
Reads the image from disk, crops it square, scales it to 640×640 and uploads it
|
|
@@ -3402,6 +3407,9 @@ const results = await client.hasWhatsapp(['919634847671', '12345678901'])
|
|
|
3402
3407
|
```js
|
|
3403
3408
|
const about = await client.queryAbout('919634847671@s.whatsapp.net')
|
|
3404
3409
|
console.log(about)
|
|
3410
|
+
|
|
3411
|
+
// your own, asked the same way
|
|
3412
|
+
console.log(await client.queryOwnAbout())
|
|
3405
3413
|
```
|
|
3406
3414
|
|
|
3407
3415
|
### Fetch Profile Picture
|
|
@@ -3435,6 +3443,22 @@ client.changeName('My Bot')
|
|
|
3435
3443
|
|
|
3436
3444
|
```js
|
|
3437
3445
|
await client.changeAbout('Available 24/7')
|
|
3446
|
+
|
|
3447
|
+
// what the server actually has now — the set IQ is answered before the new
|
|
3448
|
+
// text has to be visible anywhere, so this is the part worth reading
|
|
3449
|
+
const stored = await client.queryOwnAbout()
|
|
3450
|
+
```
|
|
3451
|
+
|
|
3452
|
+
`changeAbout` throws when the server refuses the change or never answers; it
|
|
3453
|
+
used to hand back the reply unread, so a refusal looked exactly like a success.
|
|
3454
|
+
An about is limited to 139 characters — a longer one is dropped rather than
|
|
3455
|
+
refused, and that too is now a thrown error instead of silence.
|
|
3456
|
+
|
|
3457
|
+
An about the server stores and nobody can see is a privacy setting rather than a
|
|
3458
|
+
failed write. The category that governs who may read it is `status`:
|
|
3459
|
+
|
|
3460
|
+
```js
|
|
3461
|
+
await client.changePrivacySetting('status', 'all')
|
|
3438
3462
|
```
|
|
3439
3463
|
|
|
3440
3464
|
### Change Profile Picture
|
package/cli.js
CHANGED
|
@@ -1411,7 +1411,26 @@ async function handleLine(line) {
|
|
|
1411
1411
|
const text = p.slice(1).join(' ');
|
|
1412
1412
|
if (!text) { fail('usage: /about <text>'); break; }
|
|
1413
1413
|
await _client.changeAbout(text);
|
|
1414
|
-
|
|
1414
|
+
|
|
1415
|
+
// The server answers the set IQ before the new about has to be visible
|
|
1416
|
+
// anywhere, so "result" is not the same as "changed" — this used to
|
|
1417
|
+
// report success on the strength of that empty reply alone. Read it
|
|
1418
|
+
// back and say what is actually stored.
|
|
1419
|
+
let stored = null;
|
|
1420
|
+
try { stored = await _client.queryOwnAbout(); } catch (_) {}
|
|
1421
|
+
if (stored === text) out('about updated');
|
|
1422
|
+
else if (stored) out('sent, but the server still reports: ' + stored);
|
|
1423
|
+
else out('sent, and the server accepted it — it reports no about for this number yet');
|
|
1424
|
+
|
|
1425
|
+
// An about that is stored and still nowhere to be seen is a privacy
|
|
1426
|
+
// setting rather than a failed write: on the wire the category that
|
|
1427
|
+
// governs who may read it is "status".
|
|
1428
|
+
try {
|
|
1429
|
+
const priv = await _client.queryPrivacySettings({ force: true });
|
|
1430
|
+
if (priv && priv.status && priv.status !== 'all') {
|
|
1431
|
+
out(' about visibility is "' + priv.status + '" — /privacy status all shows it to everyone');
|
|
1432
|
+
}
|
|
1433
|
+
} catch (_) {}
|
|
1415
1434
|
break;
|
|
1416
1435
|
}
|
|
1417
1436
|
|
package/lib/Client.js
CHANGED
|
@@ -59,6 +59,10 @@ const INVITE_LINK_PREFIX = 'https://chat.whatsapp.com/';
|
|
|
59
59
|
// with a different query.
|
|
60
60
|
const MEX_QUERY_REACHOUT_TIMELOCK = '23983697327930364';
|
|
61
61
|
|
|
62
|
+
// How long an about is allowed to be. The apps stop you at this length; the
|
|
63
|
+
// server takes a longer one, answers <iq type="result"/> and keeps the old text.
|
|
64
|
+
const ABOUT_MAX_LENGTH = 139;
|
|
65
|
+
|
|
62
66
|
// The names privacy settings actually go by on the wire, and the friendly
|
|
63
67
|
// aliases this library has always accepted for them. The wire names are the
|
|
64
68
|
// right-hand column.
|
|
@@ -4446,17 +4450,62 @@ class WhalibmobClient extends EventEmitter {
|
|
|
4446
4450
|
}
|
|
4447
4451
|
|
|
4448
4452
|
// Change bio/about text
|
|
4453
|
+
//
|
|
4454
|
+
// The <status> child is byte-for-byte what Baileys and whatsmeow send. The
|
|
4455
|
+
// envelope was not: both of them address this IQ to the server as a *JID*
|
|
4456
|
+
// with nobody in the user part — Baileys writes '@s.whatsapp.net', whatsmeow
|
|
4457
|
+
// uses types.ServerJID — which goes out as JID_PAIR + empty user + the server
|
|
4458
|
+
// token. This wrote the bare string 's.whatsapp.net', and a bare string is
|
|
4459
|
+
// encoded as a plain dictionary token, so where every other client puts an
|
|
4460
|
+
// address the server was reading text. That is the whole of the difference,
|
|
4461
|
+
// and it is invisible in a stanza log, which prints the node before it is
|
|
4462
|
+
// encoded: both spellings read `to="s.whatsapp.net"` on screen.
|
|
4463
|
+
//
|
|
4464
|
+
// The reply was thrown away as well. _sendIq answers null on a timeout and
|
|
4465
|
+
// hands back error stanzas unread, so a refusal, a silence and a success all
|
|
4466
|
+
// returned the same thing — which is how "about updated" came to be printed
|
|
4467
|
+
// for a change that never happened.
|
|
4449
4468
|
async changeAbout(text) {
|
|
4450
|
-
const
|
|
4469
|
+
const body = String(text == null ? '' : text);
|
|
4470
|
+
// The limit the apps impose. A longer one is dropped rather than refused,
|
|
4471
|
+
// so it would otherwise look exactly like this bug.
|
|
4472
|
+
if (Array.from(body).length > ABOUT_MAX_LENGTH) {
|
|
4473
|
+
throw new Error('changeAbout: an about is limited to ' + ABOUT_MAX_LENGTH +
|
|
4474
|
+
' characters — the server drops a longer one instead of refusing it');
|
|
4475
|
+
}
|
|
4476
|
+
|
|
4451
4477
|
const node = new BinaryNode('iq', {
|
|
4452
|
-
id,
|
|
4478
|
+
id: this._genMsgId(),
|
|
4453
4479
|
xmlns: 'status',
|
|
4454
|
-
to: 's.whatsapp.net',
|
|
4480
|
+
to: jidStrToObj('@s.whatsapp.net'),
|
|
4455
4481
|
type: 'set'
|
|
4456
4482
|
}, [
|
|
4457
|
-
new BinaryNode('status', {}, Buffer.from(
|
|
4483
|
+
new BinaryNode('status', {}, Buffer.from(body, 'utf8'))
|
|
4458
4484
|
]);
|
|
4459
|
-
|
|
4485
|
+
|
|
4486
|
+
const resp = await this._sendIq(node);
|
|
4487
|
+
if (!resp) throw new Error('changeAbout: no reply from server (IQ timed out)');
|
|
4488
|
+
if (resp.attrs && resp.attrs.type === 'error') {
|
|
4489
|
+
const errNode = findChild(resp, 'error');
|
|
4490
|
+
const code = errNode && errNode.attrs && errNode.attrs.code;
|
|
4491
|
+
const errText = errNode && errNode.attrs && errNode.attrs.text;
|
|
4492
|
+
throw new Error('changeAbout: server rejected the change' +
|
|
4493
|
+
(code ? ' (' + code + (errText ? ' ' + errText : '') + ')' : ''));
|
|
4494
|
+
}
|
|
4495
|
+
return resp;
|
|
4496
|
+
}
|
|
4497
|
+
|
|
4498
|
+
// The about this account currently has, as the server reports it.
|
|
4499
|
+
//
|
|
4500
|
+
// The set IQ is answered before the new text has to be visible anywhere, so
|
|
4501
|
+
// an empty result proves only that the stanza was understood. Reading it back
|
|
4502
|
+
// is the only way to tell an about the server stored from one it took and
|
|
4503
|
+
// dropped — and it is asked exactly the way a contact's is.
|
|
4504
|
+
async queryOwnAbout() {
|
|
4505
|
+
const me = this._store &&
|
|
4506
|
+
(this._store.phoneNumber || (this._store.me && this._store.me.id));
|
|
4507
|
+
if (!me) throw new Error('queryOwnAbout: not connected');
|
|
4508
|
+
return this.queryAbout(String(me));
|
|
4460
4509
|
}
|
|
4461
4510
|
|
|
4462
4511
|
// Change profile picture
|
package/lib/Registration.js
CHANGED
|
@@ -4,10 +4,45 @@ const crypto = require('crypto');
|
|
|
4
4
|
const https = require('https');
|
|
5
5
|
const tls = require('tls');
|
|
6
6
|
const curveJs = require('curve25519-js');
|
|
7
|
+
const { v4: uuidv4 } = require('uuid');
|
|
7
8
|
const { getDeviceConfig } = require('./DeviceConfig');
|
|
8
9
|
const attestation = require('./Attestation');
|
|
9
10
|
const { dbg: _whaDbg } = require('./logger');
|
|
10
11
|
|
|
12
|
+
// ---------- Request envelope ----------
|
|
13
|
+
//
|
|
14
|
+
// The registration server reads which platform is calling out of the
|
|
15
|
+
// User-Agent. There is no form field for it — neither this library nor Cobalt
|
|
16
|
+
// sends one — so a request whose User-Agent does not name a platform it knows is
|
|
17
|
+
// refused with {"param":"platform","reason":"bad_param"} on every endpoint. The
|
|
18
|
+
// giveaway is that /client_log is refused the same way, and its body carries no
|
|
19
|
+
// device fields at all: nothing about the platform is in the body to begin with.
|
|
20
|
+
//
|
|
21
|
+
// Android used to be sent as `WhatsApp/<version> A`, which names nothing, and
|
|
22
|
+
// was refused everywhere. iOS has always sent the full form and has always
|
|
23
|
+
// worked, which is why only one of the two platforms was broken.
|
|
24
|
+
//
|
|
25
|
+
// The three extra Android headers come from Cobalt's AndroidClientRegistration,
|
|
26
|
+
// which records them as observed on a live native Android registration. Its iOS
|
|
27
|
+
// counterpart documents that the native iOS client omits all three, so the iOS
|
|
28
|
+
// envelope stays exactly as it was.
|
|
29
|
+
function registrationHeaders(device, waVersion) {
|
|
30
|
+
if (device.os === 'android') {
|
|
31
|
+
return {
|
|
32
|
+
'User-Agent': `WhatsApp/${waVersion} Android/${device.osVersion} Device/${device.manufacturer}-${device.modelId}`,
|
|
33
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
34
|
+
'Accept': 'text/json',
|
|
35
|
+
'WaMsysRequest': '1',
|
|
36
|
+
// A fresh one per request, the way the native client mints it.
|
|
37
|
+
'request_token': uuidv4()
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
'User-Agent': `WhatsApp/${waVersion} iOS/${device.osVersion} Device/${device.model}`,
|
|
42
|
+
'Content-Type': 'application/x-www-form-urlencoded'
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
11
46
|
// ---------- SOCKS4 / SOCKS5 / Tor support ----------
|
|
12
47
|
//
|
|
13
48
|
// Set SOCKS_PROXY (or TOR_PROXY) to route registration traffic through a proxy:
|
|
@@ -125,15 +160,11 @@ async function httpPostViaSocks(path, body, waVersion, proxyUrl, authHeader) {
|
|
|
125
160
|
tlsSocket.once('error', rej);
|
|
126
161
|
});
|
|
127
162
|
|
|
128
|
-
const
|
|
129
|
-
const userAgent = _dev.os === 'android'
|
|
130
|
-
? `WhatsApp/${waVersion} A`
|
|
131
|
-
: `WhatsApp/${waVersion} iOS/${_dev.osVersion} Device/${_dev.model}`;
|
|
163
|
+
const headers = registrationHeaders(getDeviceConfig(), waVersion);
|
|
132
164
|
const reqLines = [
|
|
133
165
|
`POST /v2${path} HTTP/1.1`,
|
|
134
166
|
`Host: ${dHost}`,
|
|
135
|
-
|
|
136
|
-
`Content-Type: application/x-www-form-urlencoded`,
|
|
167
|
+
...Object.keys(headers).map(k => `${k}: ${headers[k]}`),
|
|
137
168
|
`Content-Length: ${Buffer.byteLength(body)}`
|
|
138
169
|
];
|
|
139
170
|
if (authHeader) reqLines.push(`Authorization: ${authHeader}`);
|
|
@@ -703,16 +734,11 @@ function httpPost(path, body, waVersion, authHeader) {
|
|
|
703
734
|
if (proxy) return httpPostViaSocks(path, body, waVersion, proxy, authHeader);
|
|
704
735
|
|
|
705
736
|
return new Promise((resolve, reject) => {
|
|
706
|
-
const _httpDev = getDeviceConfig();
|
|
707
|
-
const userAgent = _httpDev.os === 'android'
|
|
708
|
-
? `WhatsApp/${waVersion} A`
|
|
709
|
-
: `WhatsApp/${waVersion} iOS/${_httpDev.osVersion} Device/${_httpDev.model}`;
|
|
710
737
|
const bodyBuf = Buffer.from(body, 'utf8');
|
|
711
|
-
const headers =
|
|
712
|
-
|
|
713
|
-
'Content-
|
|
714
|
-
|
|
715
|
-
};
|
|
738
|
+
const headers = Object.assign(
|
|
739
|
+
registrationHeaders(getDeviceConfig(), waVersion),
|
|
740
|
+
{ 'Content-Length': bodyBuf.length }
|
|
741
|
+
);
|
|
716
742
|
// Attestation cert-chain / App Attest header (Android cert chain, iOS
|
|
717
743
|
// <attestation>|<keyId>). Only present when body attestation is available.
|
|
718
744
|
if (authHeader) headers['Authorization'] = authHeader;
|