whalibmob 5.5.66 → 5.5.67

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/cli.js CHANGED
@@ -7,15 +7,39 @@
7
7
  // process.env is fully populated when modules read it at load time.
8
8
  try { require('dotenv').config(); } catch (_) {}
9
9
 
10
- // Suppress internal [DBG] lines — keep console clean for users
10
+ // ─── Wire trace ───────────────────────────────────────────────────────────────
11
+ // Full protocol tracing prints every stanza this client sends and receives.
12
+ // It is opt-in: an interactive session asks once at startup. `--debug` turns it
13
+ // on without asking (for scripts and pipes), `--no-debug` / `--quiet` / `-q` /
14
+ // WA_DEBUG=0 turn it off without asking.
15
+ const TRACE_FORCE_ON = process.argv.includes('--debug') || process.env.WA_DEBUG === '1';
16
+ const TRACE_FORCE_OFF = (
17
+ process.argv.includes('--no-debug') ||
18
+ process.argv.includes('--quiet') ||
19
+ process.argv.includes('-q') ||
20
+ process.env.WA_DEBUG === '0'
21
+ );
22
+ // `--trace-bytes` additionally dumps the raw encoded bytes of every stanza.
23
+ const TRACE_BYTES = process.argv.includes('--trace-bytes');
24
+
25
+ // Consume the trace flags so the command parser below never sees them and
26
+ // reports e.g. `--no-debug` as an unknown command.
27
+ const _TRACE_FLAGS = ['--debug', '--no-debug', '--quiet', '-q', '--trace-bytes'];
28
+ process.argv = process.argv.filter(a => !_TRACE_FLAGS.includes(a));
29
+
30
+ // Console starts clean: internal [DBG] lines are swallowed unless tracing is
31
+ // switched on, which restores this stream.
11
32
  const _origStderrWrite = process.stderr.write.bind(process.stderr);
12
- process.stderr.write = function(chunk, enc, cb) {
13
- if (typeof chunk === 'string' && chunk.startsWith('[DBG]')) {
14
- if (typeof enc === 'function') enc(); else if (typeof cb === 'function') cb();
15
- return true;
16
- }
17
- return _origStderrWrite(chunk, enc, cb);
18
- };
33
+ function installDbgSuppression() {
34
+ process.stderr.write = function(chunk, enc, cb) {
35
+ if (typeof chunk === 'string' && chunk.startsWith('[DBG]')) {
36
+ if (typeof enc === 'function') enc(); else if (typeof cb === 'function') cb();
37
+ return true;
38
+ }
39
+ return _origStderrWrite(chunk, enc, cb);
40
+ };
41
+ }
42
+ installDbgSuppression();
19
43
 
20
44
  const path = require('path');
21
45
  const fs = require('fs');
@@ -37,6 +61,153 @@ const {
37
61
 
38
62
  const { assertMeId, initAuthCreds } = require('./lib/auth-utils');
39
63
 
64
+ // ─── Wire trace implementation ────────────────────────────────────────────────
65
+ // Everything below is CLI-only instrumentation; the library is untouched.
66
+ //
67
+ // Two hooks cover the whole protocol surface:
68
+ // * NoiseSocket.prototype.sendNode — every stanza leaving this client
69
+ // * NoiseSocket.prototype.emit — the 'node' event, every stanza arriving
70
+ // They are patched on the prototype rather than on an instance so the trace
71
+ // survives reconnects and covers frames sent during the handshake, before any
72
+ // client-level event has fired. https.request is wrapped as well, because SMS
73
+ // registration runs over HTTP and never touches the socket.
74
+ let _wireTraceOn = false;
75
+ function enableWireTrace() {
76
+ if (_wireTraceOn) return;
77
+ _wireTraceOn = true;
78
+ // Tracing is the point now — let [DBG] through again.
79
+ process.stderr.write = _origStderrWrite;
80
+ const { NoiseSocket } = require('./lib/noise');
81
+ const { configureLogger } = require('./lib/logger');
82
+ const https = require('https');
83
+
84
+ const C = process.stdout.isTTY
85
+ ? { out:'\x1b[36m', in:'\x1b[32m', http:'\x1b[35m', dim:'\x1b[2m', tag:'\x1b[33m', off:'\x1b[0m' }
86
+ : { out:'', in:'', http:'', dim:'', tag:'', off:'' };
87
+
88
+ // pino writes newline-delimited JSON to stdout. That is the right shape for a
89
+ // log collector and the wrong one for someone watching a live session, so
90
+ // reformat its lines on the way out. Anything that is not a pino record is
91
+ // passed through byte-for-byte.
92
+ const stamp = () => new Date().toISOString().slice(11, 23);
93
+ const trace = (s) => _origStderrWrite(s + '\n');
94
+
95
+ // Route the library's internal [DBG] stream through pino at full verbosity.
96
+ // pino writes newline-delimited JSON straight to fd 1 via sonic-boom, which
97
+ // bypasses process.stdout — so rather than trying to reformat its output we
98
+ // intercept at pino's own logMethod hook, render the record ourselves and let
99
+ // pino emit nothing. The library still logs through pino; only the rendering
100
+ // is ours.
101
+ const LEVEL_NAME = { 10: 'TRACE', 20: 'DEBUG', 30: 'INFO ', 40: 'WARN ', 50: 'ERROR', 60: 'FATAL' };
102
+ configureLogger({
103
+ level: 'trace',
104
+ hooks: {
105
+ logMethod(args, _method, level) {
106
+ const msg = args.map(a => (typeof a === 'string' ? a : JSON.stringify(a))).join(' ');
107
+ trace(`${C.dim}${stamp()} ${LEVEL_NAME[level] || level}${C.off} ${msg}`);
108
+ }
109
+ }
110
+ });
111
+
112
+ const isPrintable = (buf) => buf.length > 0 && buf.every(b => b === 9 || b === 10 || b === 13 || (b >= 32 && b < 127));
113
+
114
+ // JIDs decode to objects, byte fields to Buffers — render both readably.
115
+ function attrValue(v) {
116
+ if (v === null || v === undefined) return '';
117
+ if (Buffer.isBuffer(v)) return isPrintable(v) ? v.toString('utf8') : '0x' + v.toString('hex');
118
+ if (typeof v === 'object') {
119
+ if (v.user !== undefined) {
120
+ const dev = v.device ? ':' + v.device : '';
121
+ return `${v.user || ''}${dev}@${v.server || ''}`;
122
+ }
123
+ return JSON.stringify(v);
124
+ }
125
+ return String(v);
126
+ }
127
+
128
+ function renderContent(content, pad) {
129
+ if (content === null || content === undefined) return null;
130
+ if (Array.isArray(content)) return content.map(n => nodeToXml(n, pad)).join('\n');
131
+ if (Buffer.isBuffer(content)) {
132
+ if (isPrintable(content)) return pad + content.toString('utf8');
133
+ const hex = content.toString('hex');
134
+ const shown = hex.length > 512 ? hex.slice(0, 512) + '…' : hex;
135
+ return `${pad}${C.dim}[${content.length} bytes] ${shown}${C.off}`;
136
+ }
137
+ return pad + String(content);
138
+ }
139
+
140
+ function nodeToXml(node, pad) {
141
+ pad = pad || '';
142
+ if (!node || !node.description) return pad + String(node);
143
+ const attrs = Object.entries(node.attrs || {})
144
+ .map(([k, v]) => ` ${C.tag}${k}${C.off}="${attrValue(v)}"`).join('');
145
+ const tag = node.description;
146
+ const body = renderContent(node.content, pad + ' ');
147
+ if (body === null) return `${pad}<${tag}${attrs}/>`;
148
+ return `${pad}<${tag}${attrs}>\n${body}\n${pad}</${tag}>`;
149
+ }
150
+
151
+ function logStanza(dir, node) {
152
+ const colour = dir === 'OUT' ? C.out : C.in;
153
+ const arrow = dir === 'OUT' ? '──▶ SENT' : '◀── RECV';
154
+ trace(`\n${colour}${stamp()} ${arrow}${C.off}`);
155
+ trace(nodeToXml(node, ' '));
156
+ if (TRACE_BYTES) {
157
+ try {
158
+ const { encodeNode } = require('./lib/BinaryNode');
159
+ const raw = encodeNode(node);
160
+ trace(` ${C.dim}raw ${raw.length}B: ${raw.toString('hex')}${C.off}`);
161
+ } catch (_) {}
162
+ }
163
+ }
164
+
165
+ const origSendNode = NoiseSocket.prototype.sendNode;
166
+ NoiseSocket.prototype.sendNode = function (node) {
167
+ try { logStanza('OUT', node); } catch (_) {}
168
+ return origSendNode.call(this, node);
169
+ };
170
+
171
+ const origEmit = NoiseSocket.prototype.emit;
172
+ NoiseSocket.prototype.emit = function (event, ...args) {
173
+ try {
174
+ if (event === 'node') logStanza('IN', args[0]);
175
+ else if (event === 'open') trace(`\n${C.in}${stamp()} ◀── HANDSHAKE COMPLETE — channel secured${C.off}`);
176
+ else if (event === 'error') trace(`\n${C.in}${stamp()} ◀── SOCKET ERROR: ${args[0] && args[0].message}${C.off}`);
177
+ else if (event === 'close') trace(`\n${C.in}${stamp()} ◀── SOCKET CLOSED${C.off}`);
178
+ } catch (_) {}
179
+ return origEmit.apply(this, [event, ...args]);
180
+ };
181
+
182
+ // Registration (SMS request / code verify) goes over HTTPS, not the socket.
183
+ const origRequest = https.request;
184
+ https.request = function (...args) {
185
+ const opts = typeof args[0] === 'string' ? { href: args[0] } : (args[0] || {});
186
+ const host = opts.hostname || opts.host || opts.href || '?';
187
+ const target = `${opts.method || 'GET'} ${host}${opts.path || ''}`;
188
+ trace(`\n${C.http}${stamp()} ──▶ HTTP ${target}${C.off}`);
189
+ if (opts.headers) {
190
+ for (const [k, v] of Object.entries(opts.headers)) {
191
+ trace(` ${C.tag}${k}${C.off}: ${v}`);
192
+ }
193
+ }
194
+ const req = origRequest.apply(this, args);
195
+ const origWrite = req.write.bind(req);
196
+ req.write = function (chunk, ...rest) {
197
+ try { trace(` ${C.dim}body: ${Buffer.isBuffer(chunk) ? chunk.toString('utf8') : chunk}${C.off}`); } catch (_) {}
198
+ return origWrite(chunk, ...rest);
199
+ };
200
+ req.on('response', (res) => {
201
+ trace(`\n${C.http}${stamp()} ◀── HTTP ${res.statusCode} from ${host}${C.off}`);
202
+ let body = '';
203
+ res.on('data', d => { if (body.length < 8192) body += d.toString('utf8'); });
204
+ res.on('end', () => { if (body) trace(` ${C.dim}${body}${C.off}`); });
205
+ });
206
+ return req;
207
+ };
208
+
209
+ }
210
+
40
211
  // Read the version straight from package.json so it can never drift out of sync
41
212
  // with the published package. npm always ships package.json in the tarball,
42
213
  // regardless of the "files" list, so this resolves for installed users too.
@@ -1444,12 +1615,51 @@ options:
1444
1615
  --method sms | voice | wa_old | email (default: sms)
1445
1616
  --email <address> email address (required when --method email)
1446
1617
 
1618
+ debug:
1619
+ an interactive session asks once whether to trace the protocol.
1620
+ answer y to print every stanza sent and received, n to keep it quiet.
1621
+
1622
+ --debug trace without asking (same as WA_DEBUG=1)
1623
+ --no-debug, -q stay quiet without asking (same as WA_DEBUG=0)
1624
+ --trace-bytes also dump the raw encoded bytes of every stanza
1625
+
1447
1626
  after connecting, type /help for all available commands.
1448
1627
  `.trim();
1449
1628
 
1629
+ function announceTrace() {
1630
+ out('debug full ON — every stanza sent and received will be printed' +
1631
+ (TRACE_BYTES ? ', with raw frame bytes' : '') + '.\n');
1632
+ }
1633
+
1634
+ // Ask once, at startup, whether to trace the protocol. Commands that never
1635
+ // touch the network skip the question entirely. A non-interactive stdin (a
1636
+ // pipe, a script) is treated as "no" rather than hanging on a prompt that
1637
+ // nobody is there to answer.
1638
+ function askDebugMode(cmd) {
1639
+ if (TRACE_FORCE_ON) { enableWireTrace(); announceTrace(); return Promise.resolve(); }
1640
+ if (TRACE_FORCE_OFF) return Promise.resolve();
1641
+ const OFFLINE = ['version', '--version', '-v', 'help', '--help', '-h'];
1642
+ if (cmd && OFFLINE.includes(cmd)) return Promise.resolve();
1643
+ if (!process.stdin.isTTY) return Promise.resolve();
1644
+
1645
+ return new Promise(resolve => {
1646
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
1647
+ rl.question('do you want debug full? [y/N] ', (answer) => {
1648
+ rl.close();
1649
+ if (/^y(es)?$/i.test(String(answer).trim())) {
1650
+ enableWireTrace();
1651
+ announceTrace();
1652
+ }
1653
+ resolve();
1654
+ });
1655
+ });
1656
+ }
1657
+
1450
1658
  async function main() {
1451
1659
  const { cmd, sub, flags, pos } = parseArgs(process.argv);
1452
1660
 
1661
+ await askDebugMode(cmd);
1662
+
1453
1663
  _sessDir = flags.session || defaultSessionDir();
1454
1664
 
1455
1665
  if (!cmd) {
@@ -862,14 +862,6 @@ class MessageSender {
862
862
  // Resolve member phones for session/device queries.
863
863
  // For LID members without a PN mapping, keep the raw LID user part so
864
864
  // bulkEnsureSessionsForLid can still acquire sessions for them.
865
- // "40778226909:2@s.whatsapp.net" → "<ourLid>:2@lid" — keep the device
866
- // suffix, swap identity and domain.
867
- const ownJidToLid = (jid) => {
868
- const base = String(jid).replace(/@(?:s\.whatsapp\.net|lid)$/, '');
869
- const colon = base.indexOf(':');
870
- return myLidUser + (colon >= 0 ? base.slice(colon) : '') + '@lid';
871
- };
872
-
873
865
  const lidMembersWithoutPn = [];
874
866
  const memberPhones = [...new Set(
875
867
  members.map(jid => {
@@ -883,10 +875,10 @@ class MessageSender {
883
875
  const pn = this._client._lidToPn && this._client._lidToPn.get(raw);
884
876
  if (pn) return pn;
885
877
  }
886
- // Our own LID is a member too ensureOwnDeviceSessions already
887
- // covers those devices, so listing it here would encrypt the SKDM
888
- // for ourselves twice and emit duplicate <to> entries.
889
- if (raw !== myLidUser) lidMembersWithoutPn.push(jid);
878
+ // We are a member of the group ourselves, so our own devices come out
879
+ // of this same walk Baileys likewise derives every group target
880
+ // from the participant list and never appends own devices separately.
881
+ lidMembersWithoutPn.push(jid);
890
882
  return null;
891
883
  }
892
884
  return raw;
@@ -907,20 +899,30 @@ class MessageSender {
907
899
  ]);
908
900
 
909
901
  // Own devices come back as @s.whatsapp.net — re-express them in LID space
910
- // so every target in a LID stanza shares one address family.
911
- const ownTargets = useLid ? ownDevices.map(ownJidToLid) : ownDevices;
902
+ // so every target in a LID stanza shares one address family. In LID mode
903
+ // the participant walk above already covered us, so this only fills the gap
904
+ // for legacy pn-addressed groups.
905
+ const ownTargets = useLid ? [] : ownDevices;
912
906
 
913
907
  // Dedupe: a device reached through more than one path must still appear
914
908
  // exactly once in <participants>.
915
909
  const allTargets = [...new Set([...memberDevices, ...lidDevices, ...ownTargets])];
916
910
 
917
- const phashTargets = allTargets.includes(selfJid)
918
- ? allTargets
919
- : [selfJid, ...allTargets];
920
-
921
911
  const skStore = this._signal.senderKeyStore;
922
912
  const existingSkdmMap = skStore.getSKDMMap(groupJid);
923
- const skdmRecipients = allTargets.filter(jid => !existingSkdmMap[jid]);
913
+ // Same exclusions Baileys applies when choosing SenderKey recipients:
914
+ // hosted (bot-side) users and device 99, which is a server placeholder
915
+ // rather than a real endpoint, must never receive a distribution.
916
+ const isSkdmEligible = (jid) => {
917
+ const s = String(jid);
918
+ if (s.endsWith('@hosted') || s.endsWith('@hosted.lid')) return false;
919
+ const base = s.replace(/@.*$/, '');
920
+ const colon = base.indexOf(':');
921
+ return !(colon >= 0 && parseInt(base.slice(colon + 1), 10) === 99);
922
+ };
923
+ const skdmRecipients = allTargets.filter(
924
+ jid => !existingSkdmMap[jid] && isSkdmEligible(jid)
925
+ );
924
926
 
925
927
  // Acquire mutex for all group encryption — SKDM fanout and senderKey encrypt
926
928
  // share the same Signal session state and must not run concurrently.
@@ -934,22 +936,34 @@ class MessageSender {
934
936
  skmsgCiphertext = await this._signal.senderKeyEncrypt(groupJid, senderIdentity, plaintext);
935
937
  });
936
938
 
937
- const phash = phashTargets.length > 0 ? computePhash(phashTargets) : null;
938
-
939
939
  const skdmHasPkmsg = skdmEncrypted.some(e => e.type === 'pkmsg');
940
940
  const advBytes = skdmHasPkmsg ? _buildOrGetAdvIdentity(this._client._store) : null;
941
+ // No device-identity is expected here. ADVSignedDeviceIdentity is issued
942
+ // when a companion device is paired, to prove the companion's identity key
943
+ // was authorised by the primary. This client registers over SMS as the
944
+ // primary device, so its identity key IS the account identity and there is
945
+ // nothing to attach — advBytes stays null and that is correct.
941
946
 
942
947
  const msgContent = [];
943
948
  if (advBytes) {
944
949
  msgContent.push(new BinaryNode('device-identity', {}, advBytes));
945
950
  }
946
951
  if (skdmEncrypted.length > 0) {
947
- msgContent.push(DeviceManager.buildParticipantsNode(skdmEncrypted, null));
952
+ // Baileys applies the same extra attrs — mediatype included — to the
953
+ // SenderKey participant nodes as to the skmsg, so an image sent to a
954
+ // group carries mediatype on both. Passing null here left the SKDM
955
+ // <enc> nodes unlabelled.
956
+ msgContent.push(DeviceManager.buildParticipantsNode(skdmEncrypted, mediaSubtype));
948
957
  }
949
958
  const skmsgAttrs = { type: 'skmsg', v: '2' };
950
959
  if (mediaSubtype) skmsgAttrs.mediatype = mediaSubtype;
951
960
  msgContent.push(new BinaryNode('enc', skmsgAttrs, Buffer.isBuffer(skmsgCiphertext) ? skmsgCiphertext : Buffer.from(skmsgCiphertext)));
952
961
 
962
+ // No phash on group stanzas. Baileys computes a participant hash only on
963
+ // the 1:1 path; a group message carries id/to/type/addressing_mode and
964
+ // nothing else. Sending a hash the server did not ask for — over a
965
+ // participant set it derives itself from the group — is a way to have the
966
+ // stanza rejected outright.
953
967
  const stanzaAttrs = {
954
968
  to: jidStrToObj(groupJid),
955
969
  id: msgId,
@@ -957,7 +971,6 @@ class MessageSender {
957
971
  addressing_mode: groupAddressingMode,
958
972
  t: String(msNow())
959
973
  };
960
- if (phash) stanzaAttrs.phash = phash;
961
974
  if (options.edit) stanzaAttrs.edit = String(options.edit);
962
975
 
963
976
  const msgNode = new BinaryNode('message', stanzaAttrs, msgContent);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "whalibmob",
3
- "version": "5.5.66",
3
+ "version": "5.5.67",
4
4
  "description": "WhatsApp library for interaction with WhatsApp Mobile API no web",
5
5
  "author": "Kunboruto50",
6
6
  "main": "index.js",