whalibmob 5.5.65 → 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) {
@@ -839,10 +839,22 @@ class MessageSender {
839
839
 
840
840
  // Determine addressing mode for this group (lid = modern groups, pn = legacy)
841
841
  const groupAddressingMode = this._client._groupAddressingMode.get(groupJid) || 'lid';
842
- // For LID-mode groups, sender identity is own LID JID; otherwise own PN JID
843
- const senderIdentity = (groupAddressingMode === 'lid' && this._client._myLid)
844
- ? this._client._myLid
845
- : ownJid;
842
+ // A stanza addressed with addressing_mode='lid' must carry participants in
843
+ // LID space only. Mixing @s.whatsapp.net targets into it is exactly what
844
+ // DeviceManager warns about, and the server answers it with ack 479.
845
+ const useLid = groupAddressingMode === 'lid';
846
+ if (useLid && !this._client._myLid) {
847
+ throw new Error(
848
+ 'Cannot send to ' + groupJid + ': group is LID-addressed but our own LID ' +
849
+ 'is unknown (the server did not supply lid= on <success>). ' +
850
+ 'Reconnect so the LID is re-issued.'
851
+ );
852
+ }
853
+ const myLidUser = useLid ? phoneFromJid(this._client._myLid) : null;
854
+ // For LID-mode groups, sender identity is own LID JID; otherwise own PN JID.
855
+ // Normalised to the bare user so it matches the participant targets below.
856
+ const selfJid = useLid ? `${myLidUser}@lid` : ownJid;
857
+ const senderIdentity = selfJid;
846
858
 
847
859
  const rawSKDM = await this._signal.buildSKDM(groupJid, senderIdentity);
848
860
  const skdmMsg = encodeSenderKeyDistributionMessage(groupJid, rawSKDM);
@@ -856,9 +868,16 @@ class MessageSender {
856
868
  const isLid = jid.endsWith('@lid');
857
869
  const raw = phoneFromJid(jid);
858
870
  if (isLid) {
859
- const pn = this._client._lidToPn && this._client._lidToPn.get(raw);
860
- if (pn) return pn;
861
- // No PN mapping yet track for LID-direct session ensure
871
+ // In a LID-addressed group, resolve LID members through the LID path
872
+ // even when we know their phone number — swapping in the PN would put
873
+ // an @s.whatsapp.net target into a @lid stanza.
874
+ if (!useLid) {
875
+ const pn = this._client._lidToPn && this._client._lidToPn.get(raw);
876
+ if (pn) return pn;
877
+ }
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.
862
881
  lidMembersWithoutPn.push(jid);
863
882
  return null;
864
883
  }
@@ -879,15 +898,31 @@ class MessageSender {
879
898
  this._devMgr.ensureOwnDeviceSessions(ownPhone, this._signal)
880
899
  ]);
881
900
 
882
- const allTargets = [...memberDevices, ...lidDevices, ...ownDevices];
901
+ // Own devices come back as @s.whatsapp.net — re-express them in LID space
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;
883
906
 
884
- const phashTargets = allTargets.includes(ownJid)
885
- ? allTargets
886
- : [ownJid, ...allTargets];
907
+ // Dedupe: a device reached through more than one path must still appear
908
+ // exactly once in <participants>.
909
+ const allTargets = [...new Set([...memberDevices, ...lidDevices, ...ownTargets])];
887
910
 
888
911
  const skStore = this._signal.senderKeyStore;
889
912
  const existingSkdmMap = skStore.getSKDMMap(groupJid);
890
- 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
+ );
891
926
 
892
927
  // Acquire mutex for all group encryption — SKDM fanout and senderKey encrypt
893
928
  // share the same Signal session state and must not run concurrently.
@@ -901,22 +936,34 @@ class MessageSender {
901
936
  skmsgCiphertext = await this._signal.senderKeyEncrypt(groupJid, senderIdentity, plaintext);
902
937
  });
903
938
 
904
- const phash = phashTargets.length > 0 ? computePhash(phashTargets) : null;
905
-
906
939
  const skdmHasPkmsg = skdmEncrypted.some(e => e.type === 'pkmsg');
907
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.
908
946
 
909
947
  const msgContent = [];
910
948
  if (advBytes) {
911
949
  msgContent.push(new BinaryNode('device-identity', {}, advBytes));
912
950
  }
913
951
  if (skdmEncrypted.length > 0) {
914
- 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));
915
957
  }
916
958
  const skmsgAttrs = { type: 'skmsg', v: '2' };
917
959
  if (mediaSubtype) skmsgAttrs.mediatype = mediaSubtype;
918
960
  msgContent.push(new BinaryNode('enc', skmsgAttrs, Buffer.isBuffer(skmsgCiphertext) ? skmsgCiphertext : Buffer.from(skmsgCiphertext)));
919
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.
920
967
  const stanzaAttrs = {
921
968
  to: jidStrToObj(groupJid),
922
969
  id: msgId,
@@ -924,7 +971,6 @@ class MessageSender {
924
971
  addressing_mode: groupAddressingMode,
925
972
  t: String(msNow())
926
973
  };
927
- if (phash) stanzaAttrs.phash = phash;
928
974
  if (options.edit) stanzaAttrs.edit = String(options.edit);
929
975
 
930
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.65",
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",