whalibmob 5.10.0 → 5.10.1

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 +30 -0
  2. package/cli.js +25 -1
  3. package/lib/Client.js +123 -23
  4. package/package.json +2 -2
package/README.md CHANGED
@@ -1449,6 +1449,21 @@ 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`.
1457
+ >
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.
1461
+ >
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.
1466
+
1452
1467
  ### What Is Automatic vs What You Need to Do
1453
1468
 
1454
1469
  **Everything in the "Automatic" column requires zero code from you.**
@@ -3247,6 +3262,21 @@ For the numbers without the countdown:
3247
3262
  wa> /restriction --once
3248
3263
  ```
3249
3264
 
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:
3267
+
3268
+ ```sh
3269
+ wa> /restriction
3270
+ checking account restriction...
3271
+ error: mex query 23983697327930364: the server answered in "argo" rather than JSON, which this library cannot read (10 bytes: 0402300c000002010303)
3272
+
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.
3275
+
3276
+ 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.
3278
+ ```
3279
+
3250
3280
  An account that is fine says so and returns immediately:
3251
3281
 
3252
3282
  ```sh
package/cli.js CHANGED
@@ -1482,7 +1482,31 @@ async function handleLine(line) {
1482
1482
  requireConn();
1483
1483
  const watch = !p.includes('--once');
1484
1484
  out('checking account restriction...');
1485
- const st = await _client.fetchReachoutTimelock();
1485
+ let st;
1486
+ try {
1487
+ st = await _client.fetchReachoutTimelock();
1488
+ } catch (e) {
1489
+ fail(e.message);
1490
+ if (e.mexFormat) {
1491
+ 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.');
1494
+ out('');
1495
+ 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.');
1497
+ out(' Leave the session connected and watch for:');
1498
+ out(' ACCOUNT RESTRICTED — <reason>, HH:MM:SS remaining');
1499
+ 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.');
1503
+ } else {
1504
+ out('');
1505
+ out(' If the reply is sketched above, paste that line when reporting this —');
1506
+ out(' it is the server\'s actual answer. Run with --debug for the raw stanza.');
1507
+ }
1508
+ break;
1509
+ }
1486
1510
  hr();
1487
1511
  if (!st.active) {
1488
1512
  kv('status', 'not restricted — you can start new chats');
package/lib/Client.js CHANGED
@@ -177,6 +177,39 @@ function findChildDeep(node, desc) {
177
177
  return null;
178
178
  }
179
179
 
180
+ // A one-line sketch of a stanza, for an error message that has to be useful to
181
+ // somebody who cannot see the wire. Shows the tag tree and a short preview of
182
+ // any content, so an unexpected reply can be reported rather than guessed at.
183
+ function describeNodeBriefly(node, depth) {
184
+ if (!node) return '(nothing)';
185
+ depth = depth || 0;
186
+ const attrs = Object.entries(node.attrs || {})
187
+ .map(([k, v]) => k + '=' + String(v))
188
+ .join(' ');
189
+ let out = '<' + node.description + (attrs ? ' ' + attrs : '');
190
+
191
+ if (Array.isArray(node.content)) {
192
+ if (depth >= 3) return out + ' …>';
193
+ const kids = node.content
194
+ .filter(c => c && c.description)
195
+ .map(c => describeNodeBriefly(c, depth + 1));
196
+ return out + '>' + kids.join('');
197
+ }
198
+
199
+ const buf = getNodeContent(node);
200
+ if (buf && buf.length) {
201
+ const text = buf.toString('utf8');
202
+ // Printable content is far more use than hex; fall back to hex when it is
203
+ // not text at all.
204
+ const printable = /^[\x09\x0a\x0d\x20-\x7e]*$/.test(text);
205
+ const shown = printable
206
+ ? JSON.stringify(text.length > 120 ? text.slice(0, 120) + '…' : text)
207
+ : '0x' + buf.slice(0, 40).toString('hex') + (buf.length > 40 ? '…' : '');
208
+ return out + '>' + buf.length + 'B ' + shown;
209
+ }
210
+ return out + '/>';
211
+ }
212
+
180
213
  function getNodeContent(node) {
181
214
  if (!node) return null;
182
215
  if (Buffer.isBuffer(node.content)) return node.content;
@@ -4926,37 +4959,104 @@ class WhalibmobClient extends EventEmitter {
4926
4959
  * caller concludes the account is fine.
4927
4960
  */
4928
4961
  async _mexQuery(queryId, variables, dataPath) {
4929
- const resp = await this._sendIq(new BinaryNode('iq', {
4930
- id: this._genMsgId(),
4931
- to: 's.whatsapp.net',
4932
- type: 'get',
4933
- xmlns: 'w:mex'
4934
- }, [
4935
- new BinaryNode('query', { query_id: String(queryId) },
4936
- Buffer.from(JSON.stringify({ variables: variables || {} }), 'utf8'))
4937
- ]));
4962
+ const body = Buffer.from(JSON.stringify({ variables: variables || {} }), 'utf8');
4963
+
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
+ }
4938
4990
 
4939
4991
  if (!resp) throw new Error('mex query ' + queryId + ': no reply from server (IQ timed out)');
4940
- if (resp.attrs && resp.attrs.type === 'error') {
4941
- const errNode = findChild(resp, 'error');
4942
- const code = errNode && errNode.attrs && errNode.attrs.code;
4992
+
4993
+ // An <error> child is the refusal whether or not the iq is typed as one.
4994
+ const errNode = (resp.attrs && resp.attrs.type === 'error')
4995
+ ? findChildDeep(resp, 'error') : findChild(resp, 'error');
4996
+ if (errNode) {
4997
+ const a = errNode.attrs || {};
4998
+ const code = a.code ? String(a.code) : null;
4999
+ const text = a.text ? String(a.text) : null;
4943
5000
  throw new Error('mex query ' + queryId + ': server rejected the request' +
4944
- (code ? ' (' + code + ')' : ''));
5001
+ (code ? ' (' + code + (text ? ' ' + text : '') + ')' : ''));
4945
5002
  }
4946
5003
 
4947
- const resultNode = findChild(resp, 'result');
4948
- const buf = resultNode ? getNodeContent(resultNode) : null;
4949
- if (!buf) throw new Error('mex query ' + queryId + ': reply carried no result');
5004
+ // The JSON normally sits in <result>, but not every build wraps it the same
5005
+ // way, so take it from wherever in the reply it turns up rather than from
5006
+ // one fixed path.
5007
+ const json = this._mexJsonFrom(resp);
5008
+ if (json) {
5009
+ if (json.errors && json.errors.length) {
5010
+ throw new Error('mex query ' + queryId + ': ' +
5011
+ json.errors.map(e => (e && e.message) || 'unknown error').join(', '));
5012
+ }
5013
+ return dataPath ? (json.data && json.data[dataPath]) : json.data;
5014
+ }
4950
5015
 
4951
- let json;
4952
- try { json = JSON.parse(buf.toString('utf8')); }
4953
- catch (_) { throw new Error('mex query ' + queryId + ': reply was not JSON'); }
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.
5019
+ const resultNode = findChildDeep(resp, 'result');
5020
+ const format = resultNode && resultNode.attrs && resultNode.attrs.format;
5021
+ if (format && String(format).toLowerCase() !== 'json') {
5022
+ const raw = getNodeContent(resultNode);
5023
+ const err = new Error('mex query ' + queryId + ': the server answered in "' +
5024
+ format + '" rather than JSON, which this library cannot read' +
5025
+ (raw ? ' (' + raw.length + ' bytes: ' + raw.toString('hex') + ')' : ''));
5026
+ err.mexFormat = String(format);
5027
+ err.mexPayload = raw || null;
5028
+ _whaDbg('[DBG] MEX_REPLY_FORMAT ' + format + ' — cannot decode');
5029
+ throw err;
5030
+ }
5031
+
5032
+ _whaDbg('[DBG] MEX_REPLY_UNPARSED ' + JSON.stringify(describeNodeBriefly(resp)));
5033
+ throw new Error('mex query ' + queryId + ': the reply carried no JSON — ' +
5034
+ describeNodeBriefly(resp));
5035
+ }
4954
5036
 
4955
- if (json.errors && json.errors.length) {
4956
- throw new Error('mex query ' + queryId + ': ' +
4957
- json.errors.map(e => (e && e.message) || 'unknown error').join(', '));
5037
+ // Walk the reply for the first node whose content parses as a JSON object.
5038
+ //
5039
+ // Content arrives as bytes for a long payload and as a plain string when the
5040
+ // encoder had a token for it, so both are tried.
5041
+ _mexJsonFrom(node) {
5042
+ if (!node) return null;
5043
+ const raw = getNodeContent(node);
5044
+ if (raw && raw.length) {
5045
+ const text = raw.toString('utf8').trim();
5046
+ if (text.startsWith('{') || text.startsWith('[')) {
5047
+ try {
5048
+ const parsed = JSON.parse(text);
5049
+ if (parsed && typeof parsed === 'object') return parsed;
5050
+ } catch (_) { /* not this node */ }
5051
+ }
4958
5052
  }
4959
- return dataPath ? (json.data && json.data[dataPath]) : json.data;
5053
+ if (Array.isArray(node.content)) {
5054
+ for (const child of node.content) {
5055
+ const found = this._mexJsonFrom(child);
5056
+ if (found) return found;
5057
+ }
5058
+ }
5059
+ return null;
4960
5060
  }
4961
5061
 
4962
5062
  /**
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "whalibmob",
3
- "version": "5.10.0",
3
+ "version": "5.10.1",
4
4
  "description": "WhatsApp library for interaction with WhatsApp Mobile API no web",
5
- "author": "Kunboruto50",
5
+ "author": "Kunboruto20",
6
6
  "main": "index.js",
7
7
  "bin": {
8
8
  "wa": "./cli.js"