whalibmob 5.10.5 → 5.10.6
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 +42 -20
- package/cli.js +14 -12
- package/lib/Client.js +125 -24
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1460,11 +1460,17 @@ client.on('account_restriction', (r) => {
|
|
|
1460
1460
|
> was tried against a live server, which responded by dropping the stanza
|
|
1461
1461
|
> entirely and answering nothing.
|
|
1462
1462
|
>
|
|
1463
|
-
> Some
|
|
1464
|
-
> `false`
|
|
1465
|
-
>
|
|
1466
|
-
>
|
|
1467
|
-
>
|
|
1463
|
+
> Some servers answer tersely — a bare `false` or `true` rather than the object
|
|
1464
|
+
> with the expiry in it. `false` is taken as "no restriction", because it is a
|
|
1465
|
+
> definite answer rather than a missing one. `true` sets `active` with
|
|
1466
|
+
> `expiryUnknown: true`, since it says there is a restriction but not when it
|
|
1467
|
+
> ends; the countdown then comes from the announcement.
|
|
1468
|
+
>
|
|
1469
|
+
> An answer that is neither — `null`, or anything that says nothing either way —
|
|
1470
|
+
> throws with the decoded value on `err.mexDecoded`, rather than being reported
|
|
1471
|
+
> as "not restricted". An all-clear invented from an answer that never gave one
|
|
1472
|
+
> would have you send more messages and lengthen the very restriction you were
|
|
1473
|
+
> asking about.
|
|
1468
1474
|
>
|
|
1469
1475
|
> The `account_restriction` event does not depend on any of this. The server
|
|
1470
1476
|
> *announces* a restriction starting and lifting, and those announcements carry
|
|
@@ -2144,7 +2150,11 @@ Both methods accept a **Buffer** (use `fs.readFileSync` to load a file).
|
|
|
2144
2150
|
const fs = require('fs')
|
|
2145
2151
|
|
|
2146
2152
|
// change your own profile picture
|
|
2147
|
-
|
|
2153
|
+
// returns the new picture id, or 'remove' when it was taken down
|
|
2154
|
+
const picId = await client.changeProfilePicture(fs.readFileSync('./avatar.jpg'))
|
|
2155
|
+
|
|
2156
|
+
// pass null to remove it
|
|
2157
|
+
await client.changeProfilePicture(null)
|
|
2148
2158
|
|
|
2149
2159
|
// change a group's picture (you must be admin)
|
|
2150
2160
|
// returns the new picture id, or 'remove' when the picture was taken down
|
|
@@ -2199,6 +2209,32 @@ await client.changePrivacySetting('groups_add', 'contacts')
|
|
|
2199
2209
|
await client.changePrivacySetting('call_add', 'known')
|
|
2200
2210
|
```
|
|
2201
2211
|
|
|
2212
|
+
**Each setting takes its own values**, and they are not interchangeable:
|
|
2213
|
+
|
|
2214
|
+
| setting | accepts |
|
|
2215
|
+
|---|---|
|
|
2216
|
+
| `last_seen`, `profile_picture`, `status`, `groups_add` | `all` · `contacts` · `contact_blacklist` · `none` |
|
|
2217
|
+
| `read_receipts` | `all` · `none` |
|
|
2218
|
+
| `online` | `all` · `match_last_seen` |
|
|
2219
|
+
| `call_add` | `all` · `known` |
|
|
2220
|
+
| `messages` | `all` · `contacts` |
|
|
2221
|
+
| `defense` | `on_standard` · `off` |
|
|
2222
|
+
| `stickers` | `contacts` · `contact_allowlist` · `none` |
|
|
2223
|
+
|
|
2224
|
+
Friendlier words are translated: `on`, `off`, `everyone`, `nobody`,
|
|
2225
|
+
`my_contacts`, `contacts_except`, `contact_whitelist`. `on` becomes `all` — or
|
|
2226
|
+
`on_standard` for `defense`, which spells its on-state differently.
|
|
2227
|
+
|
|
2228
|
+
```js
|
|
2229
|
+
await client.changePrivacySetting('read_receipts', 'on') // sent as 'all'
|
|
2230
|
+
await client.changePrivacySetting('read_receipts', 'off') // sent as 'none'
|
|
2231
|
+
```
|
|
2232
|
+
|
|
2233
|
+
A value the setting does not take throws before anything is sent. This matters
|
|
2234
|
+
more than it looks: the server does not refuse an unknown value, it drops the
|
|
2235
|
+
stanza and never answers, so the mistake would otherwise surface as a timeout
|
|
2236
|
+
that looks like a network problem.
|
|
2237
|
+
|
|
2202
2238
|
### Read Privacy Settings
|
|
2203
2239
|
|
|
2204
2240
|
```js
|
|
@@ -3286,20 +3322,6 @@ wa> /restriction --demo
|
|
|
3286
3322
|
`--demo 90` uses ninety seconds instead of the default five hours, which is
|
|
3287
3323
|
short enough to watch it reach zero and stop on its own.
|
|
3288
3324
|
|
|
3289
|
-
Some accounts get a reply that decodes cleanly but holds no restriction data.
|
|
3290
|
-
The command says that, rather than reading it as an all-clear:
|
|
3291
|
-
|
|
3292
|
-
```sh
|
|
3293
|
-
wa> /restriction
|
|
3294
|
-
checking account restriction...
|
|
3295
|
-
error: mex query 23983697327930364: the server answered false rather than a result — this query returns no data for this account
|
|
3296
|
-
|
|
3297
|
-
The reply was read successfully — it simply contains no restriction
|
|
3298
|
-
data. This query returns nothing usable for your account.
|
|
3299
|
-
|
|
3300
|
-
You will still be told about a restriction: the server announces one when
|
|
3301
|
-
it starts and when it lifts, and those announcements carry the countdown.
|
|
3302
|
-
```
|
|
3303
3325
|
|
|
3304
3326
|
An account that is fine says so and returns immediately:
|
|
3305
3327
|
|
package/cli.js
CHANGED
|
@@ -447,7 +447,7 @@ const HELP = `
|
|
|
447
447
|
Profile
|
|
448
448
|
/name <text> change display name
|
|
449
449
|
/about <text> change own bio / about text
|
|
450
|
-
/photo <file
|
|
450
|
+
/photo <file>|remove change or remove own profile picture (JPEG)
|
|
451
451
|
/privacy [<type> <value>] show or change privacy settings
|
|
452
452
|
types: last_seen profile_picture status
|
|
453
453
|
online read_receipts groups_add
|
|
@@ -1387,6 +1387,9 @@ async function handleLine(line) {
|
|
|
1387
1387
|
out(' types: last_seen profile_picture status online read_receipts');
|
|
1388
1388
|
out(' groups_add call_add messages defense stickers');
|
|
1389
1389
|
out(' values: all contacts contact_blacklist contact_allowlist none');
|
|
1390
|
+
out(' not every setting takes every value — read_receipts is');
|
|
1391
|
+
out(' all/none, online is all/match_last_seen. on and off are');
|
|
1392
|
+
out(' accepted and translated.');
|
|
1390
1393
|
out(' match_last_seen known on_standard off');
|
|
1391
1394
|
break;
|
|
1392
1395
|
}
|
|
@@ -1398,11 +1401,12 @@ async function handleLine(line) {
|
|
|
1398
1401
|
case '/photo': {
|
|
1399
1402
|
requireConn();
|
|
1400
1403
|
const file = p[1];
|
|
1401
|
-
if (!file) { fail('usage: /photo <file>'); break; }
|
|
1402
|
-
if (!fs.existsSync(file)) { fail('file not found: ' + file); break; }
|
|
1403
|
-
const buf = fs.readFileSync(file);
|
|
1404
|
-
await _client.changeProfilePicture(buf);
|
|
1405
|
-
out('profile picture
|
|
1404
|
+
if (!file) { fail('usage: /photo <file> (or /photo remove)'); break; }
|
|
1405
|
+
if (file !== 'remove' && !fs.existsSync(file)) { fail('file not found: ' + file); break; }
|
|
1406
|
+
const buf = file === 'remove' ? null : fs.readFileSync(file);
|
|
1407
|
+
const picId = await _client.changeProfilePicture(buf);
|
|
1408
|
+
out(picId === 'remove' ? 'profile picture removed'
|
|
1409
|
+
: 'profile picture updated' + (picId ? ' id=' + picId : ''));
|
|
1406
1410
|
break;
|
|
1407
1411
|
}
|
|
1408
1412
|
|
|
@@ -1518,13 +1522,11 @@ async function handleLine(line) {
|
|
|
1518
1522
|
fail(e.message);
|
|
1519
1523
|
if (e.mexDecoded !== undefined) {
|
|
1520
1524
|
out('');
|
|
1521
|
-
out(' The reply was read
|
|
1522
|
-
out('
|
|
1525
|
+
out(' The reply was read, but it says nothing about a restriction either');
|
|
1526
|
+
out(' way — so it is not being reported as an all-clear.');
|
|
1523
1527
|
out('');
|
|
1524
|
-
out(' You will still be told
|
|
1525
|
-
out('
|
|
1526
|
-
out(' Leave the session connected and watch for:');
|
|
1527
|
-
out(' ACCOUNT RESTRICTED — <reason>, HH:MM:SS remaining');
|
|
1528
|
+
out(' You will still be told if one starts: the server announces that, and');
|
|
1529
|
+
out(' the announcement carries the countdown.');
|
|
1528
1530
|
} else if (e.mexFormat) {
|
|
1529
1531
|
out('');
|
|
1530
1532
|
out(' The query worked, but the reply is in an encoding that could not be');
|
package/lib/Client.js
CHANGED
|
@@ -74,6 +74,46 @@ const PRIVACY_NAME_ALIASES = {
|
|
|
74
74
|
stickers: 'stickers'
|
|
75
75
|
};
|
|
76
76
|
|
|
77
|
+
// What each setting will actually accept. The server does not argue with a
|
|
78
|
+
// value it does not know — it drops the stanza and never answers, so a typo
|
|
79
|
+
// looks exactly like a network problem. Checking here turns that into a
|
|
80
|
+
// sentence naming the choices.
|
|
81
|
+
const PRIVACY_ALLOWED_VALUES = {
|
|
82
|
+
last: ['all', 'contacts', 'contact_blacklist', 'none'],
|
|
83
|
+
profile: ['all', 'contacts', 'contact_blacklist', 'none'],
|
|
84
|
+
status: ['all', 'contacts', 'contact_blacklist', 'none'],
|
|
85
|
+
groupadd: ['all', 'contacts', 'contact_blacklist', 'none'],
|
|
86
|
+
readreceipts: ['all', 'none'],
|
|
87
|
+
online: ['all', 'match_last_seen'],
|
|
88
|
+
calladd: ['all', 'known'],
|
|
89
|
+
messages: ['all', 'contacts'],
|
|
90
|
+
defense: ['on_standard', 'off'],
|
|
91
|
+
stickers: ['contacts', 'contact_allowlist', 'none']
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// The words people reach for, mapped to the ones the protocol uses. "on" and
|
|
95
|
+
// "off" are the obvious things to type for read receipts and mean nothing on
|
|
96
|
+
// the wire, which is exactly the mistake worth absorbing.
|
|
97
|
+
const PRIVACY_VALUE_ALIASES = {
|
|
98
|
+
everyone: 'all',
|
|
99
|
+
my_contacts: 'contacts',
|
|
100
|
+
contacts_except: 'contact_blacklist',
|
|
101
|
+
contact_whitelist: 'contact_allowlist',
|
|
102
|
+
nobody: 'none',
|
|
103
|
+
off: 'none',
|
|
104
|
+
on: 'all',
|
|
105
|
+
same_as_last_seen: 'match_last_seen',
|
|
106
|
+
last_seen: 'match_last_seen',
|
|
107
|
+
standard: 'on_standard'
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// Where a word means something different for one setting than for the rest.
|
|
111
|
+
// "on" is "all" everywhere except defense, whose on-state has its own name.
|
|
112
|
+
const PRIVACY_VALUE_ALIASES_BY_NAME = {
|
|
113
|
+
defense: { on: 'on_standard' },
|
|
114
|
+
online: { on: 'all', off: 'match_last_seen' }
|
|
115
|
+
};
|
|
116
|
+
|
|
77
117
|
// Wire name → the key it is reported under in the settings object.
|
|
78
118
|
const PRIVACY_WIRE_TO_KEY = {
|
|
79
119
|
last: 'lastSeen',
|
|
@@ -4288,19 +4328,45 @@ class WhalibmobClient extends EventEmitter {
|
|
|
4288
4328
|
|
|
4289
4329
|
// Change profile picture
|
|
4290
4330
|
// buf: Buffer with JPEG image data
|
|
4331
|
+
// Change your own profile picture.
|
|
4332
|
+
//
|
|
4333
|
+
// buf: a Buffer of JPEG bytes, or null to remove the current picture.
|
|
4334
|
+
// Returns the new picture id, or 'remove' when it was taken down.
|
|
4335
|
+
//
|
|
4336
|
+
// The picture is not addressed by sending the IQ *to* yourself: it goes to
|
|
4337
|
+
// the server with the account named in `target`, the same shape a group
|
|
4338
|
+
// picture uses. Addressed the other way the server files it nowhere and never
|
|
4339
|
+
// answers, which looked from the outside like the picture had been set — the
|
|
4340
|
+
// stanza went out, nothing came back, and nothing checked.
|
|
4291
4341
|
async changeProfilePicture(buf) {
|
|
4292
4342
|
if (!this._store) throw new Error('Not connected');
|
|
4293
|
-
const selfJid =
|
|
4294
|
-
|
|
4343
|
+
const selfJid = this._ownDeviceJid
|
|
4344
|
+
? toNonAdJid(this._ownDeviceJid())
|
|
4345
|
+
: `${this._store.phoneNumber}@s.whatsapp.net`;
|
|
4346
|
+
|
|
4295
4347
|
const node = new BinaryNode('iq', {
|
|
4296
|
-
id,
|
|
4297
|
-
xmlns:
|
|
4298
|
-
to:
|
|
4299
|
-
|
|
4300
|
-
|
|
4301
|
-
|
|
4302
|
-
|
|
4303
|
-
|
|
4348
|
+
id: this._genMsgId(),
|
|
4349
|
+
xmlns: 'w:profile:picture',
|
|
4350
|
+
to: 's.whatsapp.net',
|
|
4351
|
+
target: jidStrToObj(selfJid),
|
|
4352
|
+
type: 'set'
|
|
4353
|
+
}, buf ? [new BinaryNode('picture', { type: 'image' }, buf)] : null);
|
|
4354
|
+
|
|
4355
|
+
const resp = await this._sendIq(node);
|
|
4356
|
+
if (!resp) throw new Error('changeProfilePicture: no reply from server (IQ timed out)');
|
|
4357
|
+
if (resp.attrs && resp.attrs.type === 'error') {
|
|
4358
|
+
const errNode = findChild(resp, 'error');
|
|
4359
|
+
const code = errNode && errNode.attrs && errNode.attrs.code;
|
|
4360
|
+
throw new Error('changeProfilePicture: ' +
|
|
4361
|
+
(String(code) === '406' ? 'the image was rejected (must be a JPEG)'
|
|
4362
|
+
: String(code) === '401' ? 'not authorised to change this picture'
|
|
4363
|
+
: 'server rejected the request' + (code ? ' (' + code + ')' : '')));
|
|
4364
|
+
}
|
|
4365
|
+
|
|
4366
|
+
if (!buf) return 'remove';
|
|
4367
|
+
const picNode = findChild(resp, 'picture');
|
|
4368
|
+
return (picNode && picNode.attrs && picNode.attrs.id)
|
|
4369
|
+
? String(picNode.attrs.id) : null;
|
|
4304
4370
|
}
|
|
4305
4371
|
|
|
4306
4372
|
// ─── Privacy settings ─────────────────────────────────────────────────────
|
|
@@ -4373,6 +4439,8 @@ class WhalibmobClient extends EventEmitter {
|
|
|
4373
4439
|
Object.keys(PRIVACY_NAME_ALIASES).join(', '));
|
|
4374
4440
|
}
|
|
4375
4441
|
|
|
4442
|
+
const wireValue = this._privacyWireValue(name, value);
|
|
4443
|
+
|
|
4376
4444
|
// WhatsApp Web does not put <user> children on this IQ; the exclusion list
|
|
4377
4445
|
// is maintained separately. Kept for callers that pass it,
|
|
4378
4446
|
// and only emitted when they do.
|
|
@@ -4390,7 +4458,7 @@ class WhalibmobClient extends EventEmitter {
|
|
|
4390
4458
|
type: 'set'
|
|
4391
4459
|
}, [
|
|
4392
4460
|
new BinaryNode('privacy', {}, [
|
|
4393
|
-
new BinaryNode('category', { name, value:
|
|
4461
|
+
new BinaryNode('category', { name, value: wireValue },
|
|
4394
4462
|
children.length > 0 ? children : null)
|
|
4395
4463
|
])
|
|
4396
4464
|
]);
|
|
@@ -4401,7 +4469,7 @@ class WhalibmobClient extends EventEmitter {
|
|
|
4401
4469
|
const errNode = findChild(resp, 'error');
|
|
4402
4470
|
const code = errNode && errNode.attrs && errNode.attrs.code;
|
|
4403
4471
|
const text = errNode && errNode.attrs && errNode.attrs.text;
|
|
4404
|
-
throw new Error('changePrivacySetting: server rejected ' + name + '=' +
|
|
4472
|
+
throw new Error('changePrivacySetting: server rejected ' + name + '=' + wireValue +
|
|
4405
4473
|
(code ? ' (' + code + (text ? ' ' + text : '') + ')' : ''));
|
|
4406
4474
|
}
|
|
4407
4475
|
|
|
@@ -4409,10 +4477,31 @@ class WhalibmobClient extends EventEmitter {
|
|
|
4409
4477
|
// rather than making the caller re-query.
|
|
4410
4478
|
const key = PRIVACY_WIRE_TO_KEY[name];
|
|
4411
4479
|
this._privacySettings = Object.assign({}, this._privacySettings || {},
|
|
4412
|
-
{ [key]:
|
|
4480
|
+
{ [key]: wireValue });
|
|
4413
4481
|
return this._privacySettings;
|
|
4414
4482
|
}
|
|
4415
4483
|
|
|
4484
|
+
// Resolve what the caller typed to what this particular setting accepts.
|
|
4485
|
+
//
|
|
4486
|
+
// The allowed set differs per setting — read receipts are all-or-none, online
|
|
4487
|
+
// is all-or-match-last-seen — so "contacts" is right for one and meaningless
|
|
4488
|
+
// for another. Sending a meaningless one gets no reply at all.
|
|
4489
|
+
_privacyWireValue(name, value) {
|
|
4490
|
+
const allowed = PRIVACY_ALLOWED_VALUES[name];
|
|
4491
|
+
const raw = String(value == null ? '' : value).trim().toLowerCase();
|
|
4492
|
+
const perName = PRIVACY_VALUE_ALIASES_BY_NAME[name] || {};
|
|
4493
|
+
const mapped = allowed && allowed.includes(raw)
|
|
4494
|
+
? raw
|
|
4495
|
+
: (perName[raw] || PRIVACY_VALUE_ALIASES[raw] || raw);
|
|
4496
|
+
|
|
4497
|
+
if (allowed && !allowed.includes(mapped)) {
|
|
4498
|
+
throw new Error('changePrivacySetting: "' + value + '" is not a value ' +
|
|
4499
|
+
name + ' accepts — use one of ' + allowed.join(', ') +
|
|
4500
|
+
'. The server ignores anything else without answering.');
|
|
4501
|
+
}
|
|
4502
|
+
return mapped;
|
|
4503
|
+
}
|
|
4504
|
+
|
|
4416
4505
|
// ──────────────────────────────────────────────────────────────────────────
|
|
4417
4506
|
// MESSAGING — editMessage, deleteMessage, sendStatus, forwardMessage
|
|
4418
4507
|
// ──────────────────────────────────────────────────────────────────────────
|
|
@@ -4901,8 +4990,26 @@ class WhalibmobClient extends EventEmitter {
|
|
|
4901
4990
|
async fetchReachoutTimelock() {
|
|
4902
4991
|
const payload = await this._mexQuery(MEX_QUERY_REACHOUT_TIMELOCK, {},
|
|
4903
4992
|
'xwa2_fetch_account_reachout_timelock');
|
|
4993
|
+
|
|
4994
|
+
// Some servers answer this one tersely: a bare boolean rather than the
|
|
4995
|
+
// object with the expiry in it. `false` is a definite "no restriction" and
|
|
4996
|
+
// is taken at face value; `true` says there is one but not when it ends, so
|
|
4997
|
+
// it is marked as an expiry we do not know and the announcement supplies
|
|
4998
|
+
// the countdown.
|
|
4999
|
+
if (typeof payload === 'boolean') {
|
|
5000
|
+
return this._setReachoutTimelock(
|
|
5001
|
+
ReachoutTimelock.parseTimelockPayload({ is_active: payload }), 'query');
|
|
5002
|
+
}
|
|
5003
|
+
|
|
5004
|
+
if (payload == null || typeof payload !== 'object') {
|
|
5005
|
+
const err = new Error('the server answered ' + JSON.stringify(payload) +
|
|
5006
|
+
', which says nothing about a restriction either way');
|
|
5007
|
+
err.mexDecoded = payload;
|
|
5008
|
+
throw err;
|
|
5009
|
+
}
|
|
5010
|
+
|
|
4904
5011
|
return this._setReachoutTimelock(
|
|
4905
|
-
ReachoutTimelock.parseTimelockPayload(payload
|
|
5012
|
+
ReachoutTimelock.parseTimelockPayload(payload), 'query');
|
|
4906
5013
|
}
|
|
4907
5014
|
|
|
4908
5015
|
/**
|
|
@@ -5035,16 +5142,10 @@ class WhalibmobClient extends EventEmitter {
|
|
|
5035
5142
|
return dataPath ? decoded[dataPath] : decoded;
|
|
5036
5143
|
}
|
|
5037
5144
|
|
|
5038
|
-
//
|
|
5039
|
-
//
|
|
5040
|
-
//
|
|
5041
|
-
|
|
5042
|
-
JSON.stringify(decoded) + ' rather than a result — this query returns ' +
|
|
5043
|
-
'no data for this account');
|
|
5044
|
-
err.mexFormat = format ? String(format) : null;
|
|
5045
|
-
err.mexPayload = raw;
|
|
5046
|
-
err.mexDecoded = decoded;
|
|
5047
|
-
throw err;
|
|
5145
|
+
// Not a GraphQL envelope but still an answer — a bare boolean, most
|
|
5146
|
+
// often. Handed back as-is for the caller to interpret, since only the
|
|
5147
|
+
// caller knows what its own query means.
|
|
5148
|
+
return decoded;
|
|
5048
5149
|
}
|
|
5049
5150
|
|
|
5050
5151
|
if (format && String(format).toLowerCase() !== 'json') {
|