whalibmob 5.5.66 → 5.5.68
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 +230 -8
- package/lib/Registration.js +2 -2
- package/lib/Store.js +8 -0
- package/lib/messages/MessageSender.js +36 -23
- package/package.json +1 -1
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
|
-
//
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
if (typeof
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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,165 @@ 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
|
+
// Traffic arrives while readline owns the bottom line, so writing straight
|
|
94
|
+
// out overwrites the prompt and leaves the two interleaved. Clear the line
|
|
95
|
+
// first and redraw the prompt after, the same way the CLI's own event
|
|
96
|
+
// handlers do.
|
|
97
|
+
const trace = (s) => {
|
|
98
|
+
const live = _rl && process.stdout.isTTY;
|
|
99
|
+
if (live) {
|
|
100
|
+
readline.cursorTo(process.stdout, 0);
|
|
101
|
+
readline.clearLine(process.stdout, 0);
|
|
102
|
+
}
|
|
103
|
+
_origStderrWrite(s + '\n');
|
|
104
|
+
if (live) _rl.prompt(true);
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
// Route the library's internal [DBG] stream through pino at full verbosity.
|
|
108
|
+
// pino writes newline-delimited JSON straight to fd 1 via sonic-boom, which
|
|
109
|
+
// bypasses process.stdout — so rather than trying to reformat its output we
|
|
110
|
+
// intercept at pino's own logMethod hook, render the record ourselves and let
|
|
111
|
+
// pino emit nothing. The library still logs through pino; only the rendering
|
|
112
|
+
// is ours.
|
|
113
|
+
const LEVEL_NAME = { 10: 'TRACE', 20: 'DEBUG', 30: 'INFO ', 40: 'WARN ', 50: 'ERROR', 60: 'FATAL' };
|
|
114
|
+
configureLogger({
|
|
115
|
+
level: 'trace',
|
|
116
|
+
hooks: {
|
|
117
|
+
logMethod(args, _method, level) {
|
|
118
|
+
const msg = args.map(a => (typeof a === 'string' ? a : JSON.stringify(a))).join(' ');
|
|
119
|
+
trace(`${C.dim}${stamp()} ${LEVEL_NAME[level] || level}${C.off} ${msg}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const isPrintable = (buf) => buf.length > 0 && buf.every(b => b === 9 || b === 10 || b === 13 || (b >= 32 && b < 127));
|
|
125
|
+
|
|
126
|
+
// JIDs decode to objects, byte fields to Buffers — render both readably.
|
|
127
|
+
function attrValue(v) {
|
|
128
|
+
if (v === null || v === undefined) return '';
|
|
129
|
+
if (Buffer.isBuffer(v)) return isPrintable(v) ? v.toString('utf8') : '0x' + v.toString('hex');
|
|
130
|
+
if (typeof v === 'object') {
|
|
131
|
+
if (v.user !== undefined) {
|
|
132
|
+
const dev = v.device ? ':' + v.device : '';
|
|
133
|
+
return `${v.user || ''}${dev}@${v.server || ''}`;
|
|
134
|
+
}
|
|
135
|
+
return JSON.stringify(v);
|
|
136
|
+
}
|
|
137
|
+
return String(v);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function renderContent(content, pad) {
|
|
141
|
+
if (content === null || content === undefined) return null;
|
|
142
|
+
if (Array.isArray(content)) return content.map(n => nodeToXml(n, pad)).join('\n');
|
|
143
|
+
if (Buffer.isBuffer(content)) {
|
|
144
|
+
if (isPrintable(content)) return pad + content.toString('utf8');
|
|
145
|
+
const hex = content.toString('hex');
|
|
146
|
+
const shown = hex.length > 512 ? hex.slice(0, 512) + '…' : hex;
|
|
147
|
+
return `${pad}${C.dim}[${content.length} bytes] ${shown}${C.off}`;
|
|
148
|
+
}
|
|
149
|
+
return pad + String(content);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function nodeToXml(node, pad) {
|
|
153
|
+
pad = pad || '';
|
|
154
|
+
if (!node || !node.description) return pad + String(node);
|
|
155
|
+
const attrs = Object.entries(node.attrs || {})
|
|
156
|
+
.map(([k, v]) => ` ${C.tag}${k}${C.off}="${attrValue(v)}"`).join('');
|
|
157
|
+
const tag = node.description;
|
|
158
|
+
const body = renderContent(node.content, pad + ' ');
|
|
159
|
+
if (body === null) return `${pad}<${tag}${attrs}/>`;
|
|
160
|
+
return `${pad}<${tag}${attrs}>\n${body}\n${pad}</${tag}>`;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function logStanza(dir, node) {
|
|
164
|
+
const colour = dir === 'OUT' ? C.out : C.in;
|
|
165
|
+
const arrow = dir === 'OUT' ? '──▶ SENT' : '◀── RECV';
|
|
166
|
+
trace(`\n${colour}${stamp()} ${arrow}${C.off}`);
|
|
167
|
+
trace(nodeToXml(node, ' '));
|
|
168
|
+
if (TRACE_BYTES) {
|
|
169
|
+
try {
|
|
170
|
+
const { encodeNode } = require('./lib/BinaryNode');
|
|
171
|
+
const raw = encodeNode(node);
|
|
172
|
+
trace(` ${C.dim}raw ${raw.length}B: ${raw.toString('hex')}${C.off}`);
|
|
173
|
+
} catch (_) {}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const origSendNode = NoiseSocket.prototype.sendNode;
|
|
178
|
+
NoiseSocket.prototype.sendNode = function (node) {
|
|
179
|
+
try { logStanza('OUT', node); } catch (_) {}
|
|
180
|
+
return origSendNode.call(this, node);
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const origEmit = NoiseSocket.prototype.emit;
|
|
184
|
+
NoiseSocket.prototype.emit = function (event, ...args) {
|
|
185
|
+
try {
|
|
186
|
+
if (event === 'node') logStanza('IN', args[0]);
|
|
187
|
+
else if (event === 'open') trace(`\n${C.in}${stamp()} ◀── HANDSHAKE COMPLETE — channel secured${C.off}`);
|
|
188
|
+
else if (event === 'error') trace(`\n${C.in}${stamp()} ◀── SOCKET ERROR: ${args[0] && args[0].message}${C.off}`);
|
|
189
|
+
else if (event === 'close') trace(`\n${C.in}${stamp()} ◀── SOCKET CLOSED${C.off}`);
|
|
190
|
+
} catch (_) {}
|
|
191
|
+
return origEmit.apply(this, [event, ...args]);
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
// Registration (SMS request / code verify) goes over HTTPS, not the socket.
|
|
195
|
+
const origRequest = https.request;
|
|
196
|
+
https.request = function (...args) {
|
|
197
|
+
const opts = typeof args[0] === 'string' ? { href: args[0] } : (args[0] || {});
|
|
198
|
+
const host = opts.hostname || opts.host || opts.href || '?';
|
|
199
|
+
const target = `${opts.method || 'GET'} ${host}${opts.path || ''}`;
|
|
200
|
+
trace(`\n${C.http}${stamp()} ──▶ HTTP ${target}${C.off}`);
|
|
201
|
+
if (opts.headers) {
|
|
202
|
+
for (const [k, v] of Object.entries(opts.headers)) {
|
|
203
|
+
trace(` ${C.tag}${k}${C.off}: ${v}`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
const req = origRequest.apply(this, args);
|
|
207
|
+
const origWrite = req.write.bind(req);
|
|
208
|
+
req.write = function (chunk, ...rest) {
|
|
209
|
+
try { trace(` ${C.dim}body: ${Buffer.isBuffer(chunk) ? chunk.toString('utf8') : chunk}${C.off}`); } catch (_) {}
|
|
210
|
+
return origWrite(chunk, ...rest);
|
|
211
|
+
};
|
|
212
|
+
req.on('response', (res) => {
|
|
213
|
+
trace(`\n${C.http}${stamp()} ◀── HTTP ${res.statusCode} from ${host}${C.off}`);
|
|
214
|
+
let body = '';
|
|
215
|
+
res.on('data', d => { if (body.length < 8192) body += d.toString('utf8'); });
|
|
216
|
+
res.on('end', () => { if (body) trace(` ${C.dim}${body}${C.off}`); });
|
|
217
|
+
});
|
|
218
|
+
return req;
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
}
|
|
222
|
+
|
|
40
223
|
// Read the version straight from package.json so it can never drift out of sync
|
|
41
224
|
// with the published package. npm always ships package.json in the tarball,
|
|
42
225
|
// regardless of the "files" list, so this resolves for installed users too.
|
|
@@ -1444,12 +1627,51 @@ options:
|
|
|
1444
1627
|
--method sms | voice | wa_old | email (default: sms)
|
|
1445
1628
|
--email <address> email address (required when --method email)
|
|
1446
1629
|
|
|
1630
|
+
debug:
|
|
1631
|
+
an interactive session asks once whether to trace the protocol.
|
|
1632
|
+
answer y to print every stanza sent and received, n to keep it quiet.
|
|
1633
|
+
|
|
1634
|
+
--debug trace without asking (same as WA_DEBUG=1)
|
|
1635
|
+
--no-debug, -q stay quiet without asking (same as WA_DEBUG=0)
|
|
1636
|
+
--trace-bytes also dump the raw encoded bytes of every stanza
|
|
1637
|
+
|
|
1447
1638
|
after connecting, type /help for all available commands.
|
|
1448
1639
|
`.trim();
|
|
1449
1640
|
|
|
1641
|
+
function announceTrace() {
|
|
1642
|
+
out('debug full ON — every stanza sent and received will be printed' +
|
|
1643
|
+
(TRACE_BYTES ? ', with raw frame bytes' : '') + '.\n');
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
// Ask once, at startup, whether to trace the protocol. Commands that never
|
|
1647
|
+
// touch the network skip the question entirely. A non-interactive stdin (a
|
|
1648
|
+
// pipe, a script) is treated as "no" rather than hanging on a prompt that
|
|
1649
|
+
// nobody is there to answer.
|
|
1650
|
+
function askDebugMode(cmd) {
|
|
1651
|
+
if (TRACE_FORCE_ON) { enableWireTrace(); announceTrace(); return Promise.resolve(); }
|
|
1652
|
+
if (TRACE_FORCE_OFF) return Promise.resolve();
|
|
1653
|
+
const OFFLINE = ['version', '--version', '-v', 'help', '--help', '-h'];
|
|
1654
|
+
if (cmd && OFFLINE.includes(cmd)) return Promise.resolve();
|
|
1655
|
+
if (!process.stdin.isTTY) return Promise.resolve();
|
|
1656
|
+
|
|
1657
|
+
return new Promise(resolve => {
|
|
1658
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
1659
|
+
rl.question('do you want debug full? [y/N] ', (answer) => {
|
|
1660
|
+
rl.close();
|
|
1661
|
+
if (/^y(es)?$/i.test(String(answer).trim())) {
|
|
1662
|
+
enableWireTrace();
|
|
1663
|
+
announceTrace();
|
|
1664
|
+
}
|
|
1665
|
+
resolve();
|
|
1666
|
+
});
|
|
1667
|
+
});
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1450
1670
|
async function main() {
|
|
1451
1671
|
const { cmd, sub, flags, pos } = parseArgs(process.argv);
|
|
1452
1672
|
|
|
1673
|
+
await askDebugMode(cmd);
|
|
1674
|
+
|
|
1453
1675
|
_sessDir = flags.session || defaultSessionDir();
|
|
1454
1676
|
|
|
1455
1677
|
if (!cmd) {
|
package/lib/Registration.js
CHANGED
|
@@ -424,7 +424,7 @@ function getRequestVerificationCodeParameters(store, method, meta, device, attem
|
|
|
424
424
|
'manage_call_permission', 'false',
|
|
425
425
|
'clicked_education_link', 'false',
|
|
426
426
|
'aid', '',
|
|
427
|
-
'push_code', ''
|
|
427
|
+
'push_code', store.pushCode || ''
|
|
428
428
|
];
|
|
429
429
|
}
|
|
430
430
|
|
|
@@ -434,7 +434,7 @@ function getRequestVerificationCodeParameters(store, method, meta, device, attem
|
|
|
434
434
|
'sim_mcc', meta.mcc,
|
|
435
435
|
'sim_mnc', meta.mnc,
|
|
436
436
|
'jailbroken', '0',
|
|
437
|
-
'push_code', '',
|
|
437
|
+
'push_code', store.pushCode || '',
|
|
438
438
|
'cellular_strength', '1'
|
|
439
439
|
];
|
|
440
440
|
}
|
package/lib/Store.js
CHANGED
|
@@ -64,6 +64,11 @@ function createNewStore(phoneNumber) {
|
|
|
64
64
|
// Registration.js): advertising id (UUID) and the 20-byte backup token.
|
|
65
65
|
const advertisingId = uuidv4();
|
|
66
66
|
const backupToken = crypto.randomBytes(20);
|
|
67
|
+
// Push-notification token sent as `push_code` on /code. The server validates
|
|
68
|
+
// its shape and rejects an empty value with bad_param/bad_format, so it has
|
|
69
|
+
// to be a real 32-hex-character token. Generated once and persisted, because
|
|
70
|
+
// the value must stay the same across the /code and /register pair.
|
|
71
|
+
const pushCode = crypto.randomBytes(16).toString('hex');
|
|
67
72
|
|
|
68
73
|
const device = getDeviceConfig();
|
|
69
74
|
const version = device.os === 'android' ? ANDROID_VERSION_FALLBACK : IOS_VERSION_FALLBACK;
|
|
@@ -86,6 +91,7 @@ function createNewStore(phoneNumber) {
|
|
|
86
91
|
identityId,
|
|
87
92
|
advertisingId,
|
|
88
93
|
backupToken,
|
|
94
|
+
pushCode,
|
|
89
95
|
registered: false,
|
|
90
96
|
codePending: false, // true after /code request, cleared on successful /register
|
|
91
97
|
name: 'User',
|
|
@@ -140,6 +146,7 @@ function storeToJson(store) {
|
|
|
140
146
|
identityId: store.identityId.toString('base64'),
|
|
141
147
|
advertisingId: store.advertisingId || null,
|
|
142
148
|
backupToken: store.backupToken ? store.backupToken.toString('base64') : null,
|
|
149
|
+
pushCode: store.pushCode || null,
|
|
143
150
|
registered: !!store.registered,
|
|
144
151
|
codePending: store.codePending || false,
|
|
145
152
|
name,
|
|
@@ -182,6 +189,7 @@ function storeFromJson(obj) {
|
|
|
182
189
|
// enriched /code request always has stable values.
|
|
183
190
|
advertisingId: obj.advertisingId || uuidv4(),
|
|
184
191
|
backupToken: obj.backupToken ? Buffer.from(obj.backupToken, 'base64') : crypto.randomBytes(20),
|
|
192
|
+
pushCode: obj.pushCode || crypto.randomBytes(16).toString('hex'),
|
|
185
193
|
registered: !!obj.registered,
|
|
186
194
|
codePending: obj.codePending || false,
|
|
187
195
|
name,
|
|
@@ -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
|
-
//
|
|
887
|
-
//
|
|
888
|
-
//
|
|
889
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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);
|