whalibmob 5.10.1 → 5.10.5

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 CHANGED
@@ -1449,20 +1449,26 @@ client.on('account_restriction', (r) => {
1449
1449
  > client-side shortens it; the only thing that helps is not sending more
1450
1450
  > unanswered first messages while it is on.
1451
1451
 
1452
- > [!WARNING]
1453
- > **Some servers answer this query in Argo, not JSON.** Argo is Meta's binary
1454
- > GraphQL encoding, and this library cannot read it. When that happens
1455
- > `fetchReachoutTimelock()` throws with `err.mexFormat === 'argo'` and the raw
1456
- > bytes on `err.mexPayload`.
1452
+ > [!NOTE]
1453
+ > **This transport answers in one of two encodings**, and the reply says which:
1454
+ > JSON, or Argo — Meta's compact binary encoding for GraphQL. Both are read.
1455
+ > WhatsApp sends Argo with its self-describing flag set, so it carries its own
1456
+ > field names and decodes without the GraphQL schema the query was written
1457
+ > against.
1458
+ >
1459
+ > Do not add a `format` attribute to the query in the hope of steering this — it
1460
+ > was tried against a live server, which responded by dropping the stanza
1461
+ > entirely and answering nothing.
1457
1462
  >
1458
- > It never guesses. Reporting "not restricted" from an answer nobody decoded
1459
- > would have you send more messages and make the restriction longer, so an
1460
- > unreadable reply is an error rather than an all-clear.
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.
1461
1468
  >
1462
- > The `account_restriction` event is unaffected: the server *announces* a
1463
- > restriction starting and lifting, and those announcements arrive as readable
1464
- > JSON on a different route. Keep a listener attached and you will still be
1465
- > told, with the countdown, without ever calling the query.
1469
+ > The `account_restriction` event does not depend on any of this. The server
1470
+ > *announces* a restriction starting and lifting, and those announcements carry
1471
+ > the countdown. Keep a listener attached and you are told either way.
1466
1472
 
1467
1473
  ### What Is Automatic vs What You Need to Do
1468
1474
 
@@ -3262,19 +3268,37 @@ For the numbers without the countdown:
3262
3268
  wa> /restriction --once
3263
3269
  ```
3264
3270
 
3265
- If your server encodes this answer in Argo, the command says so rather than
3266
- guessing — and points you at the announcements, which stay readable:
3271
+ To check the display without waiting to be restricted, `--demo` counts down a
3272
+ made-up restriction. Nothing is sent, nothing is asked of the server, and
3273
+ nothing is left behind when it ends:
3274
+
3275
+ ```sh
3276
+ wa> /restriction --demo
3277
+ ──────────────────────────────────────────────────
3278
+ status RESTRICTED (demo — not real)
3279
+ reason too many people you messaged blocked or reported you
3280
+ remaining 05:00:00
3281
+ ──────────────────────────────────────────────────
3282
+ press any key to stop watching
3283
+ restricted — 04:59:57 remaining
3284
+ ```
3285
+
3286
+ `--demo 90` uses ninety seconds instead of the default five hours, which is
3287
+ short enough to watch it reach zero and stop on its own.
3288
+
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:
3267
3291
 
3268
3292
  ```sh
3269
3293
  wa> /restriction
3270
3294
  checking account restriction...
3271
- error: mex query 23983697327930364: the server answered in "argo" rather than JSON, which this library cannot read (10 bytes: 0402300c000002010303)
3295
+ error: mex query 23983697327930364: the server answered false rather than a result — this query returns no data for this account
3272
3296
 
3273
- The query itself worked — your server encodes this answer in a binary
3274
- format this library cannot read yet, so the countdown cannot be polled.
3297
+ The reply was read successfully — it simply contains no restriction
3298
+ data. This query returns nothing usable for your account.
3275
3299
 
3276
3300
  You will still be told about a restriction: the server announces one when
3277
- it starts and when it lifts, and those announcements do arrive readable.
3301
+ it starts and when it lifts, and those announcements carry the countdown.
3278
3302
  ```
3279
3303
 
3280
3304
  An account that is fine says so and returns immediately:
@@ -3794,6 +3818,7 @@ wa> /quit
3794
3818
  | `/appstate --snapshot` | Re-read all app state from scratch |
3795
3819
  | `/restriction` | Account restriction status with a live countdown |
3796
3820
  | `/restriction --once` | Restriction status without the countdown |
3821
+ | `/restriction --demo [seconds]` | Fake countdown, to check the display |
3797
3822
  | `/limit` | Alias for `/restriction` |
3798
3823
  | `/ephemeral <jid> <seconds>` | Set disappearing messages timer for a chat |
3799
3824
  | `/ephemeral-default <seconds>` | Set global default ephemeral timer for new chats |
package/cli.js CHANGED
@@ -478,6 +478,7 @@ const HELP = `
478
478
  /appstate --snapshot re-read all of it from scratch
479
479
  /restriction account restriction + live countdown
480
480
  /restriction --once just the numbers, no countdown
481
+ /restriction --demo [seconds] fake countdown to check the display
481
482
  /ephemeral <jid> <seconds> set disappearing timer for chat
482
483
  /ephemeral-default <seconds> set default timer for ALL new chats
483
484
  /block <jid> block contact
@@ -1479,6 +1480,34 @@ async function handleLine(line) {
1479
1480
 
1480
1481
  case '/restriction':
1481
1482
  case '/limit': {
1483
+ // --demo feeds a made-up restriction into this session and counts it
1484
+ // down, so the display can be checked without waiting to be restricted.
1485
+ // Nothing is sent and nothing is asked of the server.
1486
+ const demoAt = p.indexOf('--demo');
1487
+ if (demoAt !== -1) {
1488
+ const secs = parseInt(p[demoAt + 1], 10) || 5 * 3600;
1489
+ const { parseTimelockPayload } = require('./lib/ReachoutTimelock');
1490
+ if (!_client) { fail('/restriction --demo needs a client; use /connect first'); break; }
1491
+ const fake = parseTimelockPayload({
1492
+ is_active: true,
1493
+ time_enforcement_ends: String(Math.floor(Date.now() / 1000) + secs),
1494
+ enforcement_type: 'BIZ_QUALITY'
1495
+ });
1496
+ const shown = _client._setReachoutTimelock(fake, 'demo');
1497
+ hr();
1498
+ kv('status', 'RESTRICTED (demo — not real)');
1499
+ kv('reason', shown.reason);
1500
+ kv('remaining', shown.remaining);
1501
+ hr();
1502
+ _rl && _rl.pause();
1503
+ await countdown(_client, shown);
1504
+ _rl && _rl.resume();
1505
+ // Leave nothing behind that a later check could mistake for real.
1506
+ _client._reachoutTimelock = null;
1507
+ out(' demo over — nothing was recorded');
1508
+ break;
1509
+ }
1510
+
1482
1511
  requireConn();
1483
1512
  const watch = !p.includes('--once');
1484
1513
  out('checking account restriction...');
@@ -1487,19 +1516,19 @@ async function handleLine(line) {
1487
1516
  st = await _client.fetchReachoutTimelock();
1488
1517
  } catch (e) {
1489
1518
  fail(e.message);
1490
- if (e.mexFormat) {
1519
+ if (e.mexDecoded !== undefined) {
1491
1520
  out('');
1492
- out(' The query itself worked — your server encodes this answer in a binary');
1493
- out(' format this library cannot read yet, so the countdown cannot be polled.');
1521
+ out(' The reply was read successfully — it simply contains no restriction');
1522
+ out(' data. This query returns nothing usable for your account.');
1494
1523
  out('');
1495
1524
  out(' You will still be told about a restriction: the server announces one when');
1496
- out(' it starts and when it lifts, and those announcements do arrive readable.');
1525
+ out(' it starts and when it lifts, and those announcements carry the countdown.');
1497
1526
  out(' Leave the session connected and watch for:');
1498
1527
  out(' ACCOUNT RESTRICTED — <reason>, HH:MM:SS remaining');
1528
+ } else if (e.mexFormat) {
1499
1529
  out('');
1500
- out(' The payload above is worth keeping. Run this again while the account IS');
1501
- out(' restricted: the two payloads differ only where the answer differs, which');
1502
- out(' is what makes this format readable without its schema.');
1530
+ out(' The query worked, but the reply is in an encoding that could not be');
1531
+ out(' decoded. Paste the bytes above when reporting this.');
1503
1532
  } else {
1504
1533
  out('');
1505
1534
  out(' If the reply is sketched above, paste that line when reporting this —');
package/index.js CHANGED
@@ -14,6 +14,7 @@ const { MessageSender, makeJid, generateMessageId } = require('./lib/messages/Me
14
14
  const { GroupParticipantResult, GroupJoinRequest } = require('./lib/GroupParticipant');
15
15
  const { AppStateStore, COLLECTIONS: APP_STATE_COLLECTIONS } = require('./lib/appstate/AppStateStore');
16
16
  const ReachoutTimelock = require('./lib/ReachoutTimelock');
17
+ const { decodeArgo, tryDecodeArgo } = require('./lib/argo/ArgoDecoder');
17
18
  const AppStateSync = require('./lib/appstate/AppStateSync');
18
19
  const { PATCH_INTEGRITY: LT_HASH } = require('./lib/appstate/LTHash');
19
20
  const {
@@ -113,6 +114,9 @@ module.exports = {
113
114
  LT_HASH,
114
115
  // Account restriction
115
116
  ReachoutTimelock,
117
+ // Argo (the binary encoding the GraphQL transport may answer in)
118
+ decodeArgo,
119
+ tryDecodeArgo,
116
120
  // Auth utilities
117
121
  makeCacheableSignalKeyStore,
118
122
  addTransactionCapability,
package/lib/Client.js CHANGED
@@ -14,6 +14,7 @@ const { createNewStore, saveStore, loadStore, toSixParts, fromSixParts } = requi
14
14
  const { BinaryNode } = require('./BinaryNode');
15
15
  const { AppStateStore, COLLECTIONS } = require('./appstate/AppStateStore');
16
16
  const ReachoutTimelock = require('./ReachoutTimelock');
17
+ const { decodeArgo } = require('./argo/ArgoDecoder');
17
18
  const AppStateSync = require('./appstate/AppStateSync');
18
19
  const SyncdProto = require('./appstate/SyncdProto');
19
20
  const AppMutations = require('./appstate/Mutations');
@@ -4961,32 +4962,18 @@ class WhalibmobClient extends EventEmitter {
4961
4962
  async _mexQuery(queryId, variables, dataPath) {
4962
4963
  const body = Buffer.from(JSON.stringify({ variables: variables || {} }), 'utf8');
4963
4964
 
4964
- const send = (askForJson) => {
4965
- const attrs = { query_id: String(queryId) };
4966
- // Newer servers answer this transport in Argo, Meta's binary GraphQL
4967
- // encoding, which nothing here can read. The reply labels its own format,
4968
- // so the format is evidently negotiable — ask for the one we can parse.
4969
- // A server that does not know the attribute ignores it, and one that
4970
- // refuses the stanza over it is handled below.
4971
- if (askForJson) attrs.format = 'json';
4972
- return this._sendIq(new BinaryNode('iq', {
4973
- id: this._genMsgId(),
4974
- to: 's.whatsapp.net',
4975
- type: 'get',
4976
- xmlns: 'w:mex'
4977
- }, [new BinaryNode('query', attrs, body)]));
4978
- };
4979
-
4980
- const hinted = this._mexJsonHintWorks !== false;
4981
- let resp = await send(hinted);
4982
-
4983
- // The hint is the only thing that changed, so a refusal means the server
4984
- // does not accept it. Drop it for this session and ask again plainly.
4985
- if (hinted && resp && resp.attrs && resp.attrs.type === 'error') {
4986
- this._mexJsonHintWorks = false;
4987
- _whaDbg('[DBG] MEX format=json hint refused — retrying without it');
4988
- resp = await send(false);
4989
- }
4965
+ // The <query> node carries the query id and nothing else.
4966
+ //
4967
+ // Asking for a format here was tried and is wrong: a `format="json"`
4968
+ // attribute makes the server drop the stanza on the floor — no reply at
4969
+ // all, where without it there is at least an answer. It does not ignore
4970
+ // what it does not recognise, so nothing speculative belongs on this node.
4971
+ const resp = await this._sendIq(new BinaryNode('iq', {
4972
+ id: this._genMsgId(),
4973
+ to: 's.whatsapp.net',
4974
+ type: 'get',
4975
+ xmlns: 'w:mex'
4976
+ }, [new BinaryNode('query', { query_id: String(queryId) }, body)]));
4990
4977
 
4991
4978
  if (!resp) throw new Error('mex query ' + queryId + ': no reply from server (IQ timed out)');
4992
4979
 
@@ -5013,15 +5000,56 @@ class WhalibmobClient extends EventEmitter {
5013
5000
  return dataPath ? (json.data && json.data[dataPath]) : json.data;
5014
5001
  }
5015
5002
 
5016
- // The query itself worked — this is purely about how the answer was
5017
- // encoded, and saying so is the difference between a fixable report and a
5018
- // shrug.
5003
+ // Not JSON, so the other encoding this transport uses: Argo, which is what
5004
+ // newer servers answer with. It carries its own field names, so it decodes
5005
+ // without the GraphQL schema the query was written against.
5019
5006
  const resultNode = findChildDeep(resp, 'result');
5020
5007
  const format = resultNode && resultNode.attrs && resultNode.attrs.format;
5008
+ const raw = resultNode ? getNodeContent(resultNode) : null;
5009
+
5010
+ // Only when the server said so. Guessing that unlabelled bytes are Argo
5011
+ // turns "this reply is not what I expected" into a misleading complaint
5012
+ // about a format that was never claimed.
5013
+ if (raw && raw.length && format && String(format).toLowerCase() === 'argo') {
5014
+ let decoded;
5015
+ try {
5016
+ decoded = decodeArgo(raw);
5017
+ } catch (e) {
5018
+ const err = new Error('mex query ' + queryId + ': the reply is ' +
5019
+ (format ? '"' + format + '"' : 'binary') + ' and could not be decoded — ' +
5020
+ e.message + ' (' + raw.length + ' bytes: ' + raw.toString('hex') + ')');
5021
+ err.mexFormat = format ? String(format) : null;
5022
+ err.mexPayload = raw;
5023
+ throw err;
5024
+ }
5025
+ _whaDbg('[DBG] MEX_ARGO decoded ' + JSON.stringify(decoded));
5026
+
5027
+ if (decoded && typeof decoded === 'object') {
5028
+ if (decoded.errors && decoded.errors.length) {
5029
+ throw new Error('mex query ' + queryId + ': ' +
5030
+ decoded.errors.map(e => (e && e.message) || 'unknown error').join(', '));
5031
+ }
5032
+ if (decoded.data !== undefined) {
5033
+ return dataPath ? (decoded.data && decoded.data[dataPath]) : decoded.data;
5034
+ }
5035
+ return dataPath ? decoded[dataPath] : decoded;
5036
+ }
5037
+
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;
5048
+ }
5049
+
5021
5050
  if (format && String(format).toLowerCase() !== 'json') {
5022
- const raw = getNodeContent(resultNode);
5023
5051
  const err = new Error('mex query ' + queryId + ': the server answered in "' +
5024
- format + '" rather than JSON, which this library cannot read' +
5052
+ format + '", which this library cannot read' +
5025
5053
  (raw ? ' (' + raw.length + ' bytes: ' + raw.toString('hex') + ')' : ''));
5026
5054
  err.mexFormat = String(format);
5027
5055
  err.mexPayload = raw || null;
@@ -5094,13 +5122,27 @@ class WhalibmobClient extends EventEmitter {
5094
5122
  }
5095
5123
 
5096
5124
  // Internal: parse newsletter response from w:mex IQ result
5125
+ //
5126
+ // Every newsletter call travels the same transport as the restriction query,
5127
+ // so a server that answers that one in Argo answers these in Argo too. This
5128
+ // used to swallow that and return null, which reads as "no such newsletter"
5129
+ // rather than "this reply is in an encoding nothing here can read" — the
5130
+ // difference between a shrug and something a caller can act on.
5097
5131
  _parseNewsletterResponse(resp) {
5098
5132
  try {
5099
- const resultNode = findChild(resp, 'result');
5100
- if (!resultNode) return null;
5101
- const buf = getNodeContent(resultNode);
5102
- if (!buf) return null;
5103
- const json = JSON.parse(buf.toString('utf8'));
5133
+ const json = this._mexJsonFrom(resp);
5134
+ if (!json) {
5135
+ const resultNode = findChildDeep(resp, 'result');
5136
+ const format = resultNode && resultNode.attrs && resultNode.attrs.format;
5137
+ if (format && String(format).toLowerCase() !== 'json') {
5138
+ _whaDbg('[DBG] NEWSLETTER reply format=' + format + ' — cannot decode');
5139
+ const err = new Error('the server answered in "' + format +
5140
+ '" rather than JSON, which this library cannot read');
5141
+ err.mexFormat = String(format);
5142
+ throw err;
5143
+ }
5144
+ return null;
5145
+ }
5104
5146
  const nl = json && (json.newsletter || (json.data && json.data.xwa2_newsletter));
5105
5147
  if (!nl) return null;
5106
5148
  const meta = nl.metadata || nl;
@@ -5111,7 +5153,8 @@ class WhalibmobClient extends EventEmitter {
5111
5153
  subscriberCount: nl.subscribers_count || meta.subscriber_count || 0,
5112
5154
  creationTime: nl.creation_time || meta.creation_time || 0
5113
5155
  };
5114
- } catch (_) {
5156
+ } catch (err) {
5157
+ if (err && err.mexFormat) throw err;
5115
5158
  return null;
5116
5159
  }
5117
5160
  }
@@ -0,0 +1,240 @@
1
+ 'use strict';
2
+
3
+ // Argo — the binary encoding WhatsApp's GraphQL transport answers in.
4
+ //
5
+ // A w:mex reply can come back as JSON or as Argo; the <result> node says which.
6
+ // Argo is normally decoded against the schema of the query that asked, which a
7
+ // client outside Meta does not have. But a message can set a SelfDescribing
8
+ // flag, and then it carries its own field names and types — which is what
9
+ // WhatsApp sends, and what makes this readable at all.
10
+ //
11
+ // Only the self-describing form is implemented. A schema-typed message is
12
+ // refused rather than guessed at.
13
+ //
14
+ // Layout:
15
+ // [header bitset][block][block]…[core]
16
+ // where each block is a length label followed by that many bytes, and the core
17
+ // is the last one. Values in the core are Labels — zigzag ULEB128 integers —
18
+ // that either carry a small value outright or point into a block.
19
+
20
+ // ─── labels ──────────────────────────────────────────────────────────────────
21
+ //
22
+ // A label is one signed integer doing several jobs at once. Non-negative is a
23
+ // length or a boolean; the three values below are markers; anything lower is a
24
+ // backreference to a value already seen.
25
+ const LABEL_NULL = -1;
26
+ const LABEL_ABSENT = -2;
27
+ const LABEL_ERROR = -3;
28
+ // -4 is the first backreference and means "the first value in this block".
29
+ const BACKREF_BASE = -4;
30
+
31
+ // Self-describing type markers, as label values.
32
+ const MARKER = {
33
+ NULL: -1,
34
+ FALSE: 0,
35
+ TRUE: 1,
36
+ OBJECT: 2,
37
+ LIST: 3,
38
+ STRING: 4,
39
+ BYTES: 5,
40
+ INT: 6,
41
+ FLOAT: 7
42
+ };
43
+
44
+ // Header flag bit positions.
45
+ const FLAG_INLINE_EVERYTHING = 0;
46
+ const FLAG_SELF_DESCRIBING = 1;
47
+
48
+ class Reader {
49
+ constructor(buf) {
50
+ this.buf = buf;
51
+ this.pos = 0;
52
+ }
53
+ get done() { return this.pos >= this.buf.length; }
54
+ byte() {
55
+ if (this.pos >= this.buf.length) throw new Error('argo: ran off the end of the message');
56
+ return this.buf[this.pos++];
57
+ }
58
+ peek() { return this.buf[this.pos]; }
59
+ take(n) {
60
+ if (n < 0 || this.pos + n > this.buf.length) {
61
+ throw new Error('argo: asked for ' + n + ' bytes with ' +
62
+ (this.buf.length - this.pos) + ' left');
63
+ }
64
+ const out = this.buf.slice(this.pos, this.pos + n);
65
+ this.pos += n;
66
+ return out;
67
+ }
68
+ // ULEB128, then zigzag back to signed.
69
+ label() {
70
+ let value = 0n, shift = 0n;
71
+ for (;;) {
72
+ const b = this.byte();
73
+ value |= BigInt(b & 0x7f) << shift;
74
+ if (!(b & 0x80)) break;
75
+ shift += 7n;
76
+ if (shift > 63n) throw new Error('argo: label is too long to be a number');
77
+ }
78
+ const signed = (value >> 1n) ^ -(value & 1n);
79
+ return Number(signed);
80
+ }
81
+ }
82
+
83
+ function readHeader(reader) {
84
+ let bits = 0, shift = 0, more = true;
85
+ while (more) {
86
+ const b = reader.byte();
87
+ bits |= (b >> 1) << shift;
88
+ shift += 7;
89
+ more = (b & 1) === 1;
90
+ }
91
+ return {
92
+ bits,
93
+ selfDescribing: !!(bits >> FLAG_SELF_DESCRIBING & 1),
94
+ inlineEverything: !!(bits >> FLAG_INLINE_EVERYTHING & 1)
95
+ };
96
+ }
97
+
98
+ // A block holds the bulky values — strings, byte arrays, numbers — with the
99
+ // core referring to them in order. Blocks are handed out in the order their
100
+ // types are first needed, which is why they are taken from a queue rather than
101
+ // looked up by name.
102
+ class BlockQueue {
103
+ constructor(blocks, core, inlineEverything) {
104
+ this.blocks = blocks;
105
+ this.core = core;
106
+ this.inline = inlineEverything;
107
+ this.next = 0;
108
+ this.byKey = new Map();
109
+ }
110
+ for(key) {
111
+ if (this.inline) return { reader: this.core, seen: [] };
112
+ if (!this.byKey.has(key)) {
113
+ const buf = this.blocks[this.next++];
114
+ if (buf === undefined) throw new Error('argo: message is missing a block for ' + key);
115
+ this.byKey.set(key, { reader: new Reader(buf), seen: [] });
116
+ }
117
+ return this.byKey.get(key);
118
+ }
119
+ }
120
+
121
+ function decodeArgo(bytes) {
122
+ const buf = Buffer.isBuffer(bytes) ? bytes : Buffer.from(bytes);
123
+ const reader = new Reader(buf);
124
+ const header = readHeader(reader);
125
+
126
+ if (!header.selfDescribing) {
127
+ const err = new Error('argo: this message is encoded against a GraphQL schema ' +
128
+ 'rather than describing itself, so it cannot be read without that schema');
129
+ err.needsSchema = true;
130
+ throw err;
131
+ }
132
+
133
+ const blocks = [];
134
+ if (header.inlineEverything) {
135
+ blocks.push(reader.take(buf.length - reader.pos));
136
+ } else {
137
+ while (!reader.done) {
138
+ const len = reader.label();
139
+ if (len < 0) throw new Error('argo: block length cannot be ' + len);
140
+ blocks.push(reader.take(len));
141
+ }
142
+ }
143
+ if (!blocks.length) throw new Error('argo: message has no core');
144
+
145
+ // The core is the last block, and every other block feeds it.
146
+ const core = new Reader(blocks[blocks.length - 1]);
147
+ const queue = new BlockQueue(blocks.slice(0, -1), core, header.inlineEverything);
148
+
149
+ return readValue(core, queue);
150
+ }
151
+
152
+ // One length-prefixed value out of a block, with backreferences resolved.
153
+ //
154
+ // The same string written twice is written once and pointed at the second time,
155
+ // which is most of why this format is small. A backreference that points at
156
+ // nothing means the message and our reading of it have diverged.
157
+ function readBlockValue(core, queue, key, convert) {
158
+ const block = queue.for(key);
159
+ const label = core.label();
160
+ if (label <= BACKREF_BASE) {
161
+ const idx = -label + BACKREF_BASE;
162
+ if (idx < 0 || idx >= block.seen.length) {
163
+ throw new Error('argo: backreference ' + label + ' points outside ' + key);
164
+ }
165
+ return block.seen[idx];
166
+ }
167
+ if (label < 0) throw new Error('argo: ' + key + ' cannot have label ' + label);
168
+ const value = convert(block.reader.take(label));
169
+ block.seen.push(value);
170
+ return value;
171
+ }
172
+
173
+ function readValue(core, queue) {
174
+ const marker = core.label();
175
+
176
+ switch (marker) {
177
+ case MARKER.NULL: return null;
178
+ case MARKER.FALSE: return false;
179
+ case MARKER.TRUE: return true;
180
+
181
+ case MARKER.OBJECT: {
182
+ const count = core.label();
183
+ if (count < 0) throw new Error('argo: object cannot have ' + count + ' fields');
184
+ const out = {};
185
+ for (let i = 0; i < count; i++) {
186
+ const name = readBlockValue(core, queue, 'String', b => b.toString('utf8'));
187
+ out[name] = readValue(core, queue);
188
+ }
189
+ return out;
190
+ }
191
+
192
+ case MARKER.LIST: {
193
+ const count = core.label();
194
+ if (count < 0) throw new Error('argo: list cannot have ' + count + ' items');
195
+ const out = [];
196
+ for (let i = 0; i < count; i++) out.push(readValue(core, queue));
197
+ return out;
198
+ }
199
+
200
+ case MARKER.STRING: return readBlockValue(core, queue, 'String', b => b.toString('utf8'));
201
+ case MARKER.BYTES: return readBlockValue(core, queue, 'Bytes', b => Buffer.from(b));
202
+
203
+ // An integer lives entirely in its block — the core spends nothing on it.
204
+ case MARKER.INT: {
205
+ const block = queue.for('Varint');
206
+ return block.reader.label();
207
+ }
208
+
209
+ case MARKER.FLOAT: {
210
+ const block = queue.for('Float');
211
+ return block.reader.take(8).readDoubleLE(0);
212
+ }
213
+
214
+ default:
215
+ if (marker === LABEL_ABSENT) return undefined;
216
+ if (marker === LABEL_ERROR) return readValue(core, queue);
217
+ throw new Error('argo: unknown type marker ' + marker);
218
+ }
219
+ }
220
+
221
+ /**
222
+ * Decode an Argo message, or return null when it is not one this can read.
223
+ *
224
+ * Never throws: a caller reaching for this already has a reply it could not
225
+ * parse, and a second failure on top of that is not news. `decodeArgo` is there
226
+ * for when the reason matters.
227
+ */
228
+ function tryDecodeArgo(bytes) {
229
+ try { return decodeArgo(bytes); }
230
+ catch (_) { return null; }
231
+ }
232
+
233
+ module.exports = {
234
+ decodeArgo,
235
+ tryDecodeArgo,
236
+ MARKER,
237
+ LABEL_NULL,
238
+ LABEL_ABSENT,
239
+ LABEL_ERROR
240
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "whalibmob",
3
- "version": "5.10.1",
3
+ "version": "5.10.5",
4
4
  "description": "WhatsApp library for interaction with WhatsApp Mobile API no web",
5
5
  "author": "Kunboruto20",
6
6
  "main": "index.js",