@violetix/baileys 0.3.18 → 0.5.0
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/README.md +174 -0
- package/lib/Defaults/index.js +4 -2
- package/lib/Socket/chats.js +3 -1
- package/lib/Socket/groups.js +8 -3
- package/lib/Socket/index.d.ts +7 -0
- package/lib/Socket/messages-recv.js +3 -3
- package/lib/Socket/messages-send.js +140 -10
- package/lib/Socket/socket.d.ts +6 -0
- package/lib/Socket/socket.js +92 -30
- package/lib/Store/make-in-memory-store.js +32 -24
- package/lib/Utils/a2ui.d.ts +50 -0
- package/lib/Utils/a2ui.js +168 -0
- package/lib/Utils/anchor-guard.d.ts +71 -0
- package/lib/Utils/anchor-guard.js +404 -0
- package/lib/Utils/business.js +25 -11
- package/lib/Utils/chat-utils.js +5 -3
- package/lib/Utils/event-buffer.js +4 -5
- package/lib/Utils/generics.js +21 -6
- package/lib/Utils/ignore-key.d.ts +51 -0
- package/lib/Utils/ignore-key.js +176 -0
- package/lib/Utils/index.d.ts +3 -0
- package/lib/Utils/index.js +3 -0
- package/lib/Utils/messages-media.d.ts +2 -0
- package/lib/Utils/messages-media.js +25 -2
- package/lib/Utils/messages.d.ts +1 -0
- package/lib/Utils/messages.js +74 -1
- package/lib/Utils/process-message.js +4 -1
- package/lib/Utils/rich-message-utils.js +1 -1
- package/lib/Utils/use-multi-file-auth-state.d.ts +2 -1
- package/lib/Utils/use-multi-file-auth-state.js +27 -6
- package/lib/Utils/use-single-file-auth-state.d.ts +1 -1
- package/lib/Utils/use-single-file-auth-state.js +12 -3
- package/package.json +2 -2
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
// Anti force-close guard: detects payloads that crash WhatsApp clients — combining/invisible
|
|
2
|
+
// char floods, mention bombs, oversized native-flow/list/carousel payloads, broken
|
|
3
|
+
// buttonParamsJson, over-deep or circular message structures — then deletes them and optionally
|
|
4
|
+
// blocks the sender.
|
|
5
|
+
//
|
|
6
|
+
// Ported from @rennzsync/baileys' anchor-guard, itself a port of
|
|
7
|
+
// @rexxhayanasi/elaina-anchorguard (MIT).
|
|
8
|
+
import { META_AI_JID } from '../WABinary/index.js';
|
|
9
|
+
export const ANCHORGUARD_DEFAULTS = {
|
|
10
|
+
maxText: 65536,
|
|
11
|
+
maxCaption: 32768,
|
|
12
|
+
maxMentions: 1500,
|
|
13
|
+
maxGroupMentions: 200,
|
|
14
|
+
maxButtons: 100,
|
|
15
|
+
maxSections: 100,
|
|
16
|
+
maxRows: 300,
|
|
17
|
+
maxCards: 60,
|
|
18
|
+
maxDepth: 12,
|
|
19
|
+
maxNodes: 20000,
|
|
20
|
+
maxBytes: 3 * 1024 * 1024,
|
|
21
|
+
maxParamsJson: 262144,
|
|
22
|
+
maxInvisibleRun: 500,
|
|
23
|
+
maxCombiningRun: 100,
|
|
24
|
+
maxNewlines: 20000
|
|
25
|
+
};
|
|
26
|
+
// Escaped on purpose: literal invisible/combining characters make the source unreadable
|
|
27
|
+
// and easy to corrupt on the next edit.
|
|
28
|
+
const INVISIBLE = /[\u200b-\u200f\u202a-\u202e\u2060-\u2064\u206a-\u206f\ufeff\ufff9-\ufffb]/;
|
|
29
|
+
const COMBINING = /[\u0300-\u036f\u0483-\u0489\u0591-\u05bd\u0610-\u061a\u064b-\u065f\u0670\u06d6-\u06dc\u0e31\u0e34-\u0e3a\u0e47-\u0e4e\uaa70\uaaf6\uabed\ufe20-\ufe2f]/;
|
|
30
|
+
/** Longest consecutive run of characters matching `matcher`. */
|
|
31
|
+
const longestRun = (text, matcher) => {
|
|
32
|
+
let run = 0;
|
|
33
|
+
let best = 0;
|
|
34
|
+
for (const ch of text) {
|
|
35
|
+
if (matcher.test(ch)) {
|
|
36
|
+
run += 1;
|
|
37
|
+
if (run > best) {
|
|
38
|
+
best = run;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
run = 0;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return best;
|
|
46
|
+
};
|
|
47
|
+
const countChar = (text, ch) => {
|
|
48
|
+
let n = 0;
|
|
49
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
50
|
+
if (text[i] === ch) {
|
|
51
|
+
n += 1;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return n;
|
|
55
|
+
};
|
|
56
|
+
const WRAPPERS = [
|
|
57
|
+
'ephemeralMessage',
|
|
58
|
+
'viewOnceMessage',
|
|
59
|
+
'viewOnceMessageV2',
|
|
60
|
+
'viewOnceMessageV2Extension',
|
|
61
|
+
'documentWithCaptionMessage',
|
|
62
|
+
'editedMessage',
|
|
63
|
+
'deviceSentMessage',
|
|
64
|
+
'botInvokeMessage'
|
|
65
|
+
];
|
|
66
|
+
const unwrapContent = (message) => {
|
|
67
|
+
let current = message;
|
|
68
|
+
let depth = 0;
|
|
69
|
+
while (current && typeof current === 'object' && depth < 64) {
|
|
70
|
+
const wrapper = WRAPPERS.find(w => current[w]?.message);
|
|
71
|
+
if (!wrapper) {
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
current = current[wrapper].message;
|
|
75
|
+
depth += 1;
|
|
76
|
+
}
|
|
77
|
+
return { content: current, wrapDepth: depth };
|
|
78
|
+
};
|
|
79
|
+
/**
|
|
80
|
+
* Inspects a message content object and reports why it looks like a crash payload.
|
|
81
|
+
* Pure — no I/O, no mutation of the input.
|
|
82
|
+
*/
|
|
83
|
+
export const detectBug = (message, options = {}) => {
|
|
84
|
+
const limits = { ...ANCHORGUARD_DEFAULTS, ...options };
|
|
85
|
+
const reasons = [];
|
|
86
|
+
if (!message || typeof message !== 'object') {
|
|
87
|
+
return { flagged: false, reasons };
|
|
88
|
+
}
|
|
89
|
+
const { content, wrapDepth } = unwrapContent(message);
|
|
90
|
+
if (wrapDepth >= 6) {
|
|
91
|
+
reasons.push(`wrapper nesting ${wrapDepth}`);
|
|
92
|
+
}
|
|
93
|
+
let nodes = 0;
|
|
94
|
+
const seen = new WeakSet();
|
|
95
|
+
// One traversal. The mention checks used to be a second full walk of the same tree, which
|
|
96
|
+
// doubled the cost of the guard on the path it is meant to protect: every inbound message.
|
|
97
|
+
const walk = (value, depth) => {
|
|
98
|
+
if (reasons.length > 40) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
nodes += 1;
|
|
102
|
+
if (nodes > limits.maxNodes) {
|
|
103
|
+
reasons.push(`node count > ${limits.maxNodes}`);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (depth > limits.maxDepth) {
|
|
107
|
+
reasons.push(`structure depth > ${limits.maxDepth}`);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (typeof value === 'string') {
|
|
111
|
+
if (value.length > limits.maxText) {
|
|
112
|
+
reasons.push(`string length ${value.length} > ${limits.maxText}`);
|
|
113
|
+
}
|
|
114
|
+
if (countChar(value, '\n') > limits.maxNewlines) {
|
|
115
|
+
reasons.push('excessive newlines');
|
|
116
|
+
}
|
|
117
|
+
const invisibleRun = longestRun(value, INVISIBLE);
|
|
118
|
+
if (invisibleRun > limits.maxInvisibleRun) {
|
|
119
|
+
reasons.push(`invisible-char run ${invisibleRun}`);
|
|
120
|
+
}
|
|
121
|
+
const combiningRun = longestRun(value, COMBINING);
|
|
122
|
+
if (combiningRun > limits.maxCombiningRun) {
|
|
123
|
+
reasons.push(`combining-char run ${combiningRun}`);
|
|
124
|
+
}
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (Array.isArray(value)) {
|
|
128
|
+
for (const item of value) {
|
|
129
|
+
walk(item, depth + 1);
|
|
130
|
+
}
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (value && typeof value === 'object') {
|
|
134
|
+
if (seen.has(value)) {
|
|
135
|
+
reasons.push('circular structure');
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
seen.add(value);
|
|
139
|
+
for (const key of Object.keys(value)) {
|
|
140
|
+
const child = value[key];
|
|
141
|
+
if (key === 'contextInfo' && child && typeof child === 'object') {
|
|
142
|
+
if (Array.isArray(child.mentionedJid) && child.mentionedJid.length > limits.maxMentions) {
|
|
143
|
+
reasons.push(`mentionedJid ${child.mentionedJid.length} > ${limits.maxMentions}`);
|
|
144
|
+
}
|
|
145
|
+
if (Array.isArray(child.groupMentions) && child.groupMentions.length > limits.maxGroupMentions) {
|
|
146
|
+
reasons.push(`groupMentions ${child.groupMentions.length} > ${limits.maxGroupMentions}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
walk(child, depth + 1);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
walk(content, 0);
|
|
154
|
+
const inspectInteractive = (node) => {
|
|
155
|
+
if (!node || typeof node !== 'object') {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
const nativeFlow = node.nativeFlowMessage || node.interactiveMessage?.nativeFlowMessage;
|
|
159
|
+
if (nativeFlow?.buttons) {
|
|
160
|
+
if (nativeFlow.buttons.length > limits.maxButtons) {
|
|
161
|
+
reasons.push(`nativeFlow buttons ${nativeFlow.buttons.length} > ${limits.maxButtons}`);
|
|
162
|
+
}
|
|
163
|
+
for (const button of nativeFlow.buttons) {
|
|
164
|
+
const params = button?.buttonParamsJson;
|
|
165
|
+
if (typeof params !== 'string') {
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (params.length > limits.maxParamsJson) {
|
|
169
|
+
reasons.push(`buttonParamsJson length ${params.length}`);
|
|
170
|
+
}
|
|
171
|
+
else if (params.length) {
|
|
172
|
+
try {
|
|
173
|
+
JSON.parse(params);
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
reasons.push('buttonParamsJson invalid');
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
const list = node.listMessage || node.interactiveMessage?.carouselMessage;
|
|
182
|
+
if (list?.sections && list.sections.length > limits.maxSections) {
|
|
183
|
+
reasons.push(`list sections ${list.sections.length} > ${limits.maxSections}`);
|
|
184
|
+
}
|
|
185
|
+
if (Array.isArray(list?.sections)) {
|
|
186
|
+
let rows = 0;
|
|
187
|
+
for (const section of list.sections) {
|
|
188
|
+
rows += Array.isArray(section?.rows) ? section.rows.length : 0;
|
|
189
|
+
}
|
|
190
|
+
if (rows > limits.maxRows) {
|
|
191
|
+
reasons.push(`list rows ${rows} > ${limits.maxRows}`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
const cards = node.interactiveMessage?.carouselMessage?.cards;
|
|
195
|
+
if (Array.isArray(cards) && cards.length > limits.maxCards) {
|
|
196
|
+
reasons.push(`carousel cards ${cards.length} > ${limits.maxCards}`);
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
inspectInteractive(content);
|
|
200
|
+
if (typeof options.byteLength === 'number' && options.byteLength > limits.maxBytes) {
|
|
201
|
+
reasons.push(`encoded size ${options.byteLength} > ${limits.maxBytes}`);
|
|
202
|
+
}
|
|
203
|
+
if (options.proto?.Message) {
|
|
204
|
+
try {
|
|
205
|
+
const encoded = options.proto.Message.encode(options.proto.Message.fromObject(message)).finish();
|
|
206
|
+
if (encoded.length > limits.maxBytes) {
|
|
207
|
+
reasons.push(`encoded size ${encoded.length} > ${limits.maxBytes}`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
reasons.push(`proto encode failed: ${error.message}`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return { flagged: reasons.length > 0, reasons };
|
|
215
|
+
};
|
|
216
|
+
const META_AI_USER = META_AI_JID.split('@')[0];
|
|
217
|
+
const isMetaAiJid = (jid) => typeof jid === 'string' && jid.startsWith(META_AI_USER);
|
|
218
|
+
/** Bounded so a bot in busy groups cannot grow the burst log without limit. */
|
|
219
|
+
const MAX_BURST_SENDERS = 5000;
|
|
220
|
+
/**
|
|
221
|
+
* Attaches the guard to a socket: flags inbound crash payloads, optionally blocks the sender,
|
|
222
|
+
* and rejects outgoing messages that would crash the recipient.
|
|
223
|
+
*/
|
|
224
|
+
export const createAnchorGuard = (sock, options = {}) => {
|
|
225
|
+
const config = {
|
|
226
|
+
autoDelete: true,
|
|
227
|
+
deleteMode: 'auto',
|
|
228
|
+
guardIncoming: true,
|
|
229
|
+
// Off by default: this one replaces `sock.sendMessage` with a wrapper, which a host that
|
|
230
|
+
// also wraps `sendMessage` needs to know about. Opt in explicitly.
|
|
231
|
+
guardOutgoing: false,
|
|
232
|
+
blockOnBug: false,
|
|
233
|
+
selfOnly: false,
|
|
234
|
+
thresholds: {},
|
|
235
|
+
onDetect: null,
|
|
236
|
+
proto: null,
|
|
237
|
+
/** messages from one sender within burstWindowMs before it counts as a burst; 0 = off */
|
|
238
|
+
burstThreshold: 0,
|
|
239
|
+
burstWindowMs: 4000,
|
|
240
|
+
/** remove the sender from the group on burst (needs admin) */
|
|
241
|
+
kickOnBurst: false,
|
|
242
|
+
/** flag/kick when a burst-flagged inviter adds this account to a group */
|
|
243
|
+
guardGroupAdds: false,
|
|
244
|
+
/** guard Meta AI's own numbers instead of exempting them */
|
|
245
|
+
metaAiNumbers: false,
|
|
246
|
+
logger: sock?.logger,
|
|
247
|
+
...options
|
|
248
|
+
};
|
|
249
|
+
const ownJid = config.ownJid || sock?.user?.id;
|
|
250
|
+
const detectOptions = { ...config.thresholds, proto: config.proto };
|
|
251
|
+
const burstLog = new Map();
|
|
252
|
+
const removeMessage = async (jid, key) => {
|
|
253
|
+
if (!config.autoDelete) {
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
const revoke = config.deleteMode === 'everyone' || (config.deleteMode === 'auto' && key?.fromMe);
|
|
257
|
+
try {
|
|
258
|
+
if (revoke) {
|
|
259
|
+
await sock.sendMessage(jid, { delete: key });
|
|
260
|
+
}
|
|
261
|
+
else if (typeof sock.chatModify === 'function') {
|
|
262
|
+
await sock.chatModify({ deleteForMe: { key, timestamp: Date.now(), deleteMedia: false } }, jid);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
catch (error) {
|
|
266
|
+
config.logger?.warn?.({ error: error.message }, 'anchorguard delete failed');
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
const maybeBlock = async (jid, key) => {
|
|
270
|
+
if (!config.blockOnBug || key?.fromMe || typeof sock.updateBlockStatus !== 'function') {
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
try {
|
|
274
|
+
await sock.updateBlockStatus(jid, 'block');
|
|
275
|
+
}
|
|
276
|
+
catch (error) {
|
|
277
|
+
config.logger?.warn?.({ error: error.message }, 'anchorguard block failed');
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
const maybeKickBurst = async (groupJid, sender) => {
|
|
281
|
+
if (!config.kickOnBurst || !groupJid?.endsWith('@g.us') || typeof sock.groupParticipantsUpdate !== 'function') {
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
try {
|
|
285
|
+
await sock.groupParticipantsUpdate(groupJid, [sender], 'remove');
|
|
286
|
+
}
|
|
287
|
+
catch (error) {
|
|
288
|
+
config.logger?.warn?.({ error: error.message }, 'anchorguard kick-on-burst failed');
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
const checkBurst = (sender) => {
|
|
292
|
+
if (!config.burstThreshold || !sender) {
|
|
293
|
+
return false;
|
|
294
|
+
}
|
|
295
|
+
const now = Date.now();
|
|
296
|
+
const stamps = (burstLog.get(sender) || []).filter(t => now - t < config.burstWindowMs);
|
|
297
|
+
stamps.push(now);
|
|
298
|
+
burstLog.set(sender, stamps);
|
|
299
|
+
// Senders whose window has fully expired are dropped, so the map tracks active senders
|
|
300
|
+
// only. Without this it keeps one entry per sender seen for the lifetime of the socket.
|
|
301
|
+
if (burstLog.size > MAX_BURST_SENDERS) {
|
|
302
|
+
for (const [key, times] of burstLog) {
|
|
303
|
+
if (!times.some(t => now - t < config.burstWindowMs)) {
|
|
304
|
+
burstLog.delete(key);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return stamps.length > config.burstThreshold;
|
|
309
|
+
};
|
|
310
|
+
const handleUpsert = async ({ messages }) => {
|
|
311
|
+
if (!config.guardIncoming || !Array.isArray(messages)) {
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
for (const msg of messages) {
|
|
315
|
+
const content = msg?.message;
|
|
316
|
+
if (!content) {
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
const jid = msg.key?.remoteJid;
|
|
320
|
+
const sender = msg.key?.participant || jid;
|
|
321
|
+
if (config.selfOnly && jid !== ownJid && !msg.key?.fromMe) {
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
if (!config.metaAiNumbers && isMetaAiJid(sender)) {
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
const result = detectBug(content, detectOptions);
|
|
328
|
+
const burst = checkBurst(sender);
|
|
329
|
+
if (!result.flagged && !burst) {
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
const reasons = burst
|
|
333
|
+
? [...result.reasons, `burst > ${config.burstThreshold}/${config.burstWindowMs}ms`]
|
|
334
|
+
: result.reasons;
|
|
335
|
+
config.logger?.warn?.({ jid, sender, reasons }, 'anchorguard flagged incoming message');
|
|
336
|
+
await config.onDetect?.({ direction: 'incoming', message: msg, jid, sender, reasons, burst });
|
|
337
|
+
if (result.flagged) {
|
|
338
|
+
await removeMessage(jid, msg.key);
|
|
339
|
+
}
|
|
340
|
+
await maybeBlock(sender, msg.key);
|
|
341
|
+
if (burst) {
|
|
342
|
+
await maybeKickBurst(jid, sender);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
const handleGroupParticipantsUpdate = async (update) => {
|
|
347
|
+
if (!config.guardGroupAdds || update?.action !== 'add') {
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
const inviter = update?.author;
|
|
351
|
+
if (!inviter || (!config.metaAiNumbers && isMetaAiJid(inviter))) {
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
if (checkBurst(inviter)) {
|
|
355
|
+
config.logger?.warn?.({ group: update.id, inviter }, 'anchorguard flagged burst group-add');
|
|
356
|
+
await config.onDetect?.({ direction: 'group-add', jid: update.id, sender: inviter, reasons: ['burst group-add'], burst: true });
|
|
357
|
+
await maybeKickBurst(update.id, inviter);
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
if (config.guardIncoming && sock?.ev?.on) {
|
|
361
|
+
sock.ev.on('messages.upsert', handleUpsert);
|
|
362
|
+
}
|
|
363
|
+
if (config.guardGroupAdds && sock?.ev?.on) {
|
|
364
|
+
sock.ev.on('group-participants.update', handleGroupParticipantsUpdate);
|
|
365
|
+
}
|
|
366
|
+
let originalSend = null;
|
|
367
|
+
let patchedSend = null;
|
|
368
|
+
if (config.guardOutgoing && typeof sock?.sendMessage === 'function') {
|
|
369
|
+
// Keep the unbound reference so `stop()` restores the exact function that was there.
|
|
370
|
+
// Restoring a bound copy instead stacks a `bind` layer per guard/stop cycle and makes
|
|
371
|
+
// the socket's own method unrecognisable to a host comparing identities.
|
|
372
|
+
originalSend = sock.sendMessage;
|
|
373
|
+
const callOriginal = originalSend.bind(sock);
|
|
374
|
+
patchedSend = async (jid, content, sendOptions) => {
|
|
375
|
+
const probe = detectBug(content, detectOptions);
|
|
376
|
+
if (probe.flagged) {
|
|
377
|
+
config.logger?.warn?.({ jid, reasons: probe.reasons }, 'anchorguard blocked outgoing message');
|
|
378
|
+
await config.onDetect?.({ direction: 'outgoing', jid, content, reasons: probe.reasons });
|
|
379
|
+
throw new Error(`anchorguard blocked outgoing message: ${probe.reasons.join('; ')}`);
|
|
380
|
+
}
|
|
381
|
+
return callOriginal(jid, content, sendOptions);
|
|
382
|
+
};
|
|
383
|
+
sock.sendMessage = patchedSend;
|
|
384
|
+
}
|
|
385
|
+
return {
|
|
386
|
+
detect: message => detectBug(message, detectOptions),
|
|
387
|
+
stop: () => {
|
|
388
|
+
if (config.guardIncoming && sock?.ev?.off) {
|
|
389
|
+
sock.ev.off('messages.upsert', handleUpsert);
|
|
390
|
+
}
|
|
391
|
+
if (config.guardGroupAdds && sock?.ev?.off) {
|
|
392
|
+
sock.ev.off('group-participants.update', handleGroupParticipantsUpdate);
|
|
393
|
+
}
|
|
394
|
+
// Only unwind our own wrapper. Restoring unconditionally would throw away a wrapper
|
|
395
|
+
// the host installed after this guard did.
|
|
396
|
+
if (patchedSend && sock.sendMessage === patchedSend) {
|
|
397
|
+
sock.sendMessage = originalSend;
|
|
398
|
+
}
|
|
399
|
+
burstLog.clear();
|
|
400
|
+
}
|
|
401
|
+
};
|
|
402
|
+
};
|
|
403
|
+
// kept for drop-in parity with @rexxhayanasi/elaina-anchorguard's export name
|
|
404
|
+
export const createAntiBugGuard = createAnchorGuard;
|
package/lib/Utils/business.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Boom } from '@hapi/boom';
|
|
2
2
|
import { createHash } from 'crypto';
|
|
3
|
+
import { once } from 'events';
|
|
3
4
|
import { createWriteStream, promises as fs } from 'fs';
|
|
4
5
|
import { tmpdir } from 'os';
|
|
5
6
|
import { join } from 'path';
|
|
@@ -199,18 +200,31 @@ export const uploadingNecessaryImages = async (images, waUploadToServer, timeout
|
|
|
199
200
|
const hasher = createHash('sha256');
|
|
200
201
|
const filePath = join(tmpdir(), 'img' + generateMessageIDV2());
|
|
201
202
|
const encFileWriteStream = createWriteStream(filePath);
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
203
|
+
try {
|
|
204
|
+
for await (const block of stream) {
|
|
205
|
+
hasher.update(block);
|
|
206
|
+
// Respect backpressure instead of queueing the whole image in memory.
|
|
207
|
+
if (!encFileWriteStream.write(block)) {
|
|
208
|
+
await once(encFileWriteStream, 'drain');
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
// The upload reads the path, so the writes have to be on disk first. Without this the
|
|
212
|
+
// upload could read a partial file while the hash covers the whole image, and the
|
|
213
|
+
// server rejects the mismatch.
|
|
214
|
+
await new Promise((resolve, reject) => encFileWriteStream.end(err => (err ? reject(err) : resolve())));
|
|
215
|
+
const sha = hasher.digest('base64');
|
|
216
|
+
const { directPath } = await waUploadToServer(filePath, {
|
|
217
|
+
mediaType: 'product-catalog-image',
|
|
218
|
+
fileEncSha256B64: sha,
|
|
219
|
+
timeoutMs
|
|
220
|
+
});
|
|
221
|
+
return { url: getUrlFromDirectPath(directPath) };
|
|
222
|
+
}
|
|
223
|
+
finally {
|
|
224
|
+
// Also runs when the upload throws, which used to leak the temp file.
|
|
225
|
+
encFileWriteStream.destroy();
|
|
226
|
+
await fs.unlink(filePath).catch(() => { });
|
|
205
227
|
}
|
|
206
|
-
const sha = hasher.digest('base64');
|
|
207
|
-
const { directPath } = await waUploadToServer(filePath, {
|
|
208
|
-
mediaType: 'product-catalog-image',
|
|
209
|
-
fileEncSha256B64: sha,
|
|
210
|
-
timeoutMs
|
|
211
|
-
});
|
|
212
|
-
await fs.unlink(filePath).catch(err => console.log('Error deleting temp file ', err));
|
|
213
|
-
return { url: getUrlFromDirectPath(directPath) };
|
|
214
228
|
}));
|
|
215
229
|
return results;
|
|
216
230
|
};
|
package/lib/Utils/chat-utils.js
CHANGED
|
@@ -344,7 +344,6 @@ export const decodePatches = async (name, syncds, initial, getAppStateSyncKey, o
|
|
|
344
344
|
syncd.mutations?.push(...ref.mutations);
|
|
345
345
|
}
|
|
346
346
|
const patchVersion = toNumber(version.version);
|
|
347
|
-
newState.version = patchVersion;
|
|
348
347
|
const shouldMutate = typeof minimumVersionNumber === 'undefined' || patchVersion > minimumVersionNumber;
|
|
349
348
|
let decodeResult;
|
|
350
349
|
try {
|
|
@@ -358,9 +357,12 @@ export const decodePatches = async (name, syncds, initial, getAppStateSyncKey, o
|
|
|
358
357
|
catch (err) {
|
|
359
358
|
if (isMissingKeyError(err))
|
|
360
359
|
throw err;
|
|
361
|
-
|
|
362
|
-
|
|
360
|
+
// stop here: advancing the version past an undecoded patch loses its mutations for good;
|
|
361
|
+
// the next sync resumes from the last good version
|
|
362
|
+
logger?.warn({ name, version: patchVersion, error: err.message }, 'failed to decode patch, stopping');
|
|
363
|
+
break;
|
|
363
364
|
}
|
|
365
|
+
newState.version = patchVersion;
|
|
364
366
|
newState.hash = decodeResult.hash;
|
|
365
367
|
newState.indexValueMap = decodeResult.indexValueMap;
|
|
366
368
|
if (validateMacs) {
|
|
@@ -400,7 +400,8 @@ eventData, logger) {
|
|
|
400
400
|
data.contactUpserts[contact.id] = upsert;
|
|
401
401
|
}
|
|
402
402
|
if (data.contactUpdates[contact.id]) {
|
|
403
|
-
upsert
|
|
403
|
+
// merge into the stored upsert itself; the old code merged into a temp and lost the update
|
|
404
|
+
Object.assign(upsert, trimUndefined(data.contactUpdates[contact.id]), trimUndefined(contact));
|
|
404
405
|
delete data.contactUpdates[contact.id];
|
|
405
406
|
}
|
|
406
407
|
}
|
|
@@ -525,10 +526,8 @@ eventData, logger) {
|
|
|
525
526
|
const groupUpdates = eventData;
|
|
526
527
|
for (const update of groupUpdates) {
|
|
527
528
|
const id = update.id;
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
data.groupUpdates[id] = Object.assign(groupUpdate, update);
|
|
531
|
-
}
|
|
529
|
+
// merge every update in the window, not just the first
|
|
530
|
+
data.groupUpdates[id] = Object.assign(data.groupUpdates[id] || {}, update);
|
|
532
531
|
}
|
|
533
532
|
break;
|
|
534
533
|
default:
|
package/lib/Utils/generics.js
CHANGED
|
@@ -263,17 +263,32 @@ export const getStatusFromReceiptType = (type) => {
|
|
|
263
263
|
}
|
|
264
264
|
return status;
|
|
265
265
|
};
|
|
266
|
-
const CODE_MAP = {
|
|
267
|
-
conflict: DisconnectReason.connectionReplaced
|
|
268
|
-
};
|
|
269
266
|
/**
|
|
270
|
-
* Stream errors generally provide a reason, map that to a baileys DisconnectReason
|
|
271
|
-
*
|
|
267
|
+
* Stream errors generally provide a reason, map that to a baileys DisconnectReason.
|
|
268
|
+
* Mirrors zapo-js (src/transport/stream/parse.ts): only conflict means the session is gone;
|
|
269
|
+
* ack / xml-not-well-formed / unknown reasons are transient and must stay reconnectable.
|
|
270
|
+
* The old fallback was badSession (500), which made bots wipe healthy sessions.
|
|
272
271
|
*/
|
|
273
272
|
export const getErrorCodeFromStreamError = (node) => {
|
|
274
273
|
const [reasonNode] = getAllBinaryNodeChildren(node);
|
|
275
274
|
let reason = reasonNode?.tag || 'unknown';
|
|
276
|
-
|
|
275
|
+
let statusCode;
|
|
276
|
+
if (reason === 'conflict') {
|
|
277
|
+
// type="replaced": another client took over; any other type (device_removed): unlinked
|
|
278
|
+
statusCode = reasonNode.attrs?.type === 'replaced' || !reasonNode.attrs?.type
|
|
279
|
+
? DisconnectReason.connectionReplaced
|
|
280
|
+
: DisconnectReason.loggedOut;
|
|
281
|
+
}
|
|
282
|
+
else if (node.attrs.code) {
|
|
283
|
+
statusCode = +node.attrs.code;
|
|
284
|
+
// 516 = server-forced logout
|
|
285
|
+
if (statusCode === 516) {
|
|
286
|
+
statusCode = DisconnectReason.loggedOut;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
statusCode = DisconnectReason.connectionClosed;
|
|
291
|
+
}
|
|
277
292
|
if (statusCode === DisconnectReason.restartRequired) {
|
|
278
293
|
reason = 'restart required';
|
|
279
294
|
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { BinaryNode } from '../WABinary/index.js';
|
|
2
|
+
export declare const IGNORE_KEY_KINDS: readonly ['message', 'receipt', 'notification', 'presence', 'chatstate', 'call'];
|
|
3
|
+
export type WaIgnoreKeyKind = (typeof IGNORE_KEY_KINDS)[number];
|
|
4
|
+
export type WaIgnoreKeyContext = {
|
|
5
|
+
/** Stanza tag. */
|
|
6
|
+
kind: WaIgnoreKeyKind;
|
|
7
|
+
/** Deviceless chat JID; userless `from` values are kept verbatim. */
|
|
8
|
+
remoteJid: string | null;
|
|
9
|
+
/** Resolved against the account's own PN and LID. */
|
|
10
|
+
fromMe: boolean;
|
|
11
|
+
/** Stanza id. */
|
|
12
|
+
id: string | undefined;
|
|
13
|
+
/** Author in groups / broadcasts; `null` otherwise. */
|
|
14
|
+
participant: string | null;
|
|
15
|
+
};
|
|
16
|
+
export type WaIgnoreKey = {
|
|
17
|
+
/** Chat JID; array entries OR. Alt `sender_pn` / `sender_lid` attrs are matched too. */
|
|
18
|
+
remoteJid?: string | readonly string[];
|
|
19
|
+
/** Whether the stanza was sent by this account. */
|
|
20
|
+
fromMe?: boolean;
|
|
21
|
+
/** Stanza id. */
|
|
22
|
+
id?: string;
|
|
23
|
+
/** Author in groups / broadcasts. Alt forms are matched too. */
|
|
24
|
+
participant?: string;
|
|
25
|
+
/** Restrict to specific tags. Default: all six. */
|
|
26
|
+
only?: readonly WaIgnoreKeyKind[];
|
|
27
|
+
};
|
|
28
|
+
export type WaIgnoreKeyPredicate = (ctx: WaIgnoreKeyContext) => boolean;
|
|
29
|
+
export type WaIgnoreKeyInput = WaIgnoreKey | WaIgnoreKeyPredicate;
|
|
30
|
+
export type WaIgnoreKeyEntry = {
|
|
31
|
+
remoteJid?: string[];
|
|
32
|
+
participant?: string;
|
|
33
|
+
fromMe?: boolean;
|
|
34
|
+
id?: string;
|
|
35
|
+
only?: readonly WaIgnoreKeyKind[];
|
|
36
|
+
predicate?: WaIgnoreKeyPredicate;
|
|
37
|
+
};
|
|
38
|
+
export declare const isIgnorableKind: (tag: string) => boolean;
|
|
39
|
+
export declare const normalizeIgnoreKey: (input: WaIgnoreKeyInput) => WaIgnoreKeyEntry;
|
|
40
|
+
export declare const buildIgnoreContext: (node: BinaryNode, meId?: string, meLid?: string) => WaIgnoreKeyContext;
|
|
41
|
+
export declare const matchesIgnoreKey: (entry: WaIgnoreKeyEntry, ctx: WaIgnoreKeyContext, node: BinaryNode) => boolean;
|
|
42
|
+
export declare const ignoredStanzaNeedsAck: (tag: string) => boolean;
|
|
43
|
+
export declare const makeIgnoreKeyRegistry: (deps: {
|
|
44
|
+
getMeId: () => string | undefined;
|
|
45
|
+
getMeLid: () => string | undefined;
|
|
46
|
+
logger?: any;
|
|
47
|
+
}) => {
|
|
48
|
+
register: (input: WaIgnoreKeyInput) => () => void;
|
|
49
|
+
shouldDrop: (node: BinaryNode) => boolean;
|
|
50
|
+
readonly size: number;
|
|
51
|
+
};
|