@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.
@@ -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;
@@ -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
- for await (const block of stream) {
203
- hasher.update(block);
204
- encFileWriteStream.write(block);
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
  };
@@ -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
- logger?.warn({ name, version: patchVersion, error: err.message }, 'failed to decode patch, skipping');
362
- continue;
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 = Object.assign(data.contactUpdates[contact.id], trimUndefined(contact));
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
- const groupUpdate = data.groupUpdates[id] || {};
529
- if (!data.groupUpdates[id]) {
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:
@@ -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
- * @param reason the string reason given, eg. "conflict"
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
- const statusCode = +(node.attrs.code || CODE_MAP[reason] || DisconnectReason.badSession);
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
+ };