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.
Files changed (4) hide show
  1. package/README.md +42 -20
  2. package/cli.js +14 -12
  3. package/lib/Client.js +125 -24
  4. 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 accounts get an answer with no restriction data in it at all — a bare
1464
- > `false` rather than a result. That throws, with the decoded value on
1465
- > `err.mexDecoded`, rather than being reported as "not restricted": an
1466
- > all-clear invented from an answer that never said so would have you send more
1467
- > messages and lengthen the very restriction you were asking about.
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
- await client.changeProfilePicture(fs.readFileSync('./avatar.jpg'))
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> change own profile picture (JPEG)
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 updated');
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 successfully — it simply contains no restriction');
1522
- out(' data. This query returns nothing usable for your account.');
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 about a restriction: the server announces one when');
1525
- out(' it starts and when it lifts, and those announcements carry the countdown.');
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 = `${this._store.phoneNumber}@s.whatsapp.net`;
4294
- const id = this._genMsgId();
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: 'w:profile:picture',
4298
- to: selfJid,
4299
- type: 'set'
4300
- }, [
4301
- new BinaryNode('picture', { type: 'image' }, buf)
4302
- ]);
4303
- return this._sendIq(node);
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: String(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 + '=' + value +
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]: String(value) });
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 || {}), 'query');
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
- // The server answered something that is not a GraphQL result at all —
5039
- // a bare boolean, most often. There is no data in it to return, and
5040
- // saying so beats handing back a shape the caller will misread.
5041
- const err = new Error('mex query ' + queryId + ': the server answered ' +
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') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "whalibmob",
3
- "version": "5.10.5",
3
+ "version": "5.10.6",
4
4
  "description": "WhatsApp library for interaction with WhatsApp Mobile API no web",
5
5
  "author": "Kunboruto20",
6
6
  "main": "index.js",