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 +218 -8
- package/lib/messages/MessageSender.js +62 -16
- 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,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
|
-
//
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
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
|
-
|
|
860
|
-
|
|
861
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
885
|
-
|
|
886
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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);
|