@prototypevip/baileys 0.0.5 → 0.0.7
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/lib/AntiBan/antiban.js +922 -0
- package/lib/AntiBan/banRecoveryOrchestrator.js +284 -0
- package/lib/AntiBan/contactGraph.js +236 -0
- package/lib/AntiBan/contentVariator.js +150 -0
- package/lib/AntiBan/credsSnapshot.js +121 -0
- package/lib/AntiBan/deliveryTracker.js +107 -0
- package/lib/AntiBan/deviceFingerprint.js +106 -0
- package/lib/AntiBan/fleetEventStore.js +157 -0
- package/lib/AntiBan/groupOperationGuard.js +116 -0
- package/lib/AntiBan/health.js +207 -0
- package/lib/AntiBan/humanEntropy.js +337 -0
- package/lib/AntiBan/index.js +77 -0
- package/lib/AntiBan/instanceCoordinator.js +175 -0
- package/lib/AntiBan/jidCanonicalizer.js +256 -0
- package/lib/AntiBan/jidCircuitBreaker.js +186 -0
- package/lib/AntiBan/legitimacySignalInjector.js +231 -0
- package/lib/AntiBan/lidFirstResolver.js +174 -0
- package/lib/AntiBan/lidResolver.js +324 -0
- package/lib/AntiBan/messageQueue.js +187 -0
- package/lib/AntiBan/messageRecovery.js +283 -0
- package/lib/AntiBan/messageTypeRegistry.js +369 -0
- package/lib/AntiBan/observability.js +145 -0
- package/lib/AntiBan/patch.js +302 -0
- package/lib/AntiBan/persist.js +87 -0
- package/lib/AntiBan/presenceChoreographer.js +430 -0
- package/lib/AntiBan/presets.js +105 -0
- package/lib/AntiBan/profiles.js +31 -0
- package/lib/AntiBan/proxyRotator.js +306 -0
- package/lib/AntiBan/rateLimiter.js +261 -0
- package/lib/AntiBan/readReceiptVariance.js +88 -0
- package/lib/AntiBan/reconnectThrottle.js +180 -0
- package/lib/AntiBan/replyRatio.js +161 -0
- package/lib/AntiBan/reputationVoucher.js +373 -0
- package/lib/AntiBan/retryReason.js +91 -0
- package/lib/AntiBan/retryTracker.js +172 -0
- package/lib/AntiBan/scheduler.js +111 -0
- package/lib/AntiBan/sessionFingerprint.js +248 -0
- package/lib/AntiBan/sessionStability.js +330 -0
- package/lib/AntiBan/stateAdapter.js +73 -0
- package/lib/AntiBan/stateExport.js +152 -0
- package/lib/AntiBan/stealthConnect.js +130 -0
- package/lib/AntiBan/timelockGuard.js +181 -0
- package/lib/AntiBan/topologyThrottler.js +339 -0
- package/lib/AntiBan/warmup.js +140 -0
- package/lib/AntiBan/webhooks.js +80 -0
- package/lib/AntiBan/wrapper.js +456 -0
- package/lib/Socket/chats.js +29 -4
- package/lib/Socket/groups.js +7 -2
- package/lib/Socket/index.js +68 -3
- package/lib/Socket/messages-send.js +44 -8
- package/lib/Socket/socket.js +2 -2
- package/lib/Store/prototype-store.js +10 -39
- package/lib/index.js +5 -0
- package/package.json +7 -3
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LID Resolver — Maintains bidirectional LID↔PN mapping for contacts
|
|
3
|
+
*
|
|
4
|
+
* WhatsApp migrated to LID (Linked Identity) in 2024. A contact now has two JIDs:
|
|
5
|
+
* - Phone number form: "27825651069@s.whatsapp.net"
|
|
6
|
+
* - LID form: "123456789@lid"
|
|
7
|
+
*
|
|
8
|
+
* Messages can arrive under either form. If an encryption session was established
|
|
9
|
+
* under one form and a message arrives under the other, decryption fails → "Bad MAC".
|
|
10
|
+
*
|
|
11
|
+
* This utility:
|
|
12
|
+
* - Learns LID↔PN mappings from message events
|
|
13
|
+
* - Normalizes JIDs to a canonical form (phone number by default)
|
|
14
|
+
* - Provides lookup for cross-form resolution
|
|
15
|
+
* - Optionally persists state across restarts
|
|
16
|
+
*
|
|
17
|
+
* This is a standalone utility — can be used independently or via JidCanonicalizer.
|
|
18
|
+
*/
|
|
19
|
+
const DEFAULT_CONFIG = {
|
|
20
|
+
canonical: 'pn',
|
|
21
|
+
maxEntries: 10_000,
|
|
22
|
+
};
|
|
23
|
+
export class LidResolver {
|
|
24
|
+
config;
|
|
25
|
+
persistence;
|
|
26
|
+
// Bidirectional maps: lid→pn and pn→lid
|
|
27
|
+
lidToPn = new Map();
|
|
28
|
+
pnToLid = new Map(); // pn → lid (for quick reverse lookup)
|
|
29
|
+
stats = {
|
|
30
|
+
learnedFromEvents: 0,
|
|
31
|
+
lookupsServed: 0,
|
|
32
|
+
lookupMisses: 0,
|
|
33
|
+
};
|
|
34
|
+
constructor(config = {}) {
|
|
35
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
36
|
+
this.persistence = config.persistence;
|
|
37
|
+
// Auto-hydrate if persistence provided
|
|
38
|
+
if (this.persistence?.load) {
|
|
39
|
+
void this.hydrate();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Learn from a message event. Idempotent.
|
|
44
|
+
* Accepts partial mappings — will use whatever fields are available.
|
|
45
|
+
*/
|
|
46
|
+
learn(mapping) {
|
|
47
|
+
let lid = mapping.lid ? this.normalizeJid(mapping.lid) : undefined;
|
|
48
|
+
let pn = mapping.pn ? this.normalizeJid(mapping.pn) : undefined;
|
|
49
|
+
const phone = mapping.phone;
|
|
50
|
+
// Validate we have both lid and pn (or can derive pn from phone)
|
|
51
|
+
if (!lid || (!pn && !phone)) {
|
|
52
|
+
return; // Insufficient data
|
|
53
|
+
}
|
|
54
|
+
// Derive pn from phone if not provided
|
|
55
|
+
if (!pn && phone) {
|
|
56
|
+
pn = `${phone}@s.whatsapp.net`;
|
|
57
|
+
}
|
|
58
|
+
// Validate forms
|
|
59
|
+
if (!lid || !pn)
|
|
60
|
+
return;
|
|
61
|
+
if (!lid.endsWith('@lid'))
|
|
62
|
+
return;
|
|
63
|
+
if (!pn.endsWith('@s.whatsapp.net'))
|
|
64
|
+
return;
|
|
65
|
+
// Check if we already know this mapping
|
|
66
|
+
const existing = this.lidToPn.get(lid);
|
|
67
|
+
if (existing) {
|
|
68
|
+
// Already learned — just increment seen count
|
|
69
|
+
existing.seenCount++;
|
|
70
|
+
existing.learnedAt = Date.now(); // Update access time for LRU
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
// Learn new mapping
|
|
74
|
+
const extractedPhone = phone || pn.split('@')[0];
|
|
75
|
+
const newMapping = {
|
|
76
|
+
lid,
|
|
77
|
+
pn,
|
|
78
|
+
phone: extractedPhone,
|
|
79
|
+
learnedAt: Date.now(),
|
|
80
|
+
seenCount: 1,
|
|
81
|
+
};
|
|
82
|
+
// Check if we need to evict (LRU)
|
|
83
|
+
if (this.lidToPn.size >= this.config.maxEntries) {
|
|
84
|
+
this.evictLRU();
|
|
85
|
+
}
|
|
86
|
+
this.lidToPn.set(lid, newMapping);
|
|
87
|
+
this.pnToLid.set(pn, lid);
|
|
88
|
+
this.stats.learnedFromEvents++;
|
|
89
|
+
// Async flush to persistence (don't await)
|
|
90
|
+
if (this.persistence?.save) {
|
|
91
|
+
void this.flush();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Given any form (LID or PN), return the canonical form.
|
|
96
|
+
* Falls back to input if unknown (no throw).
|
|
97
|
+
*/
|
|
98
|
+
resolveCanonical(jid) {
|
|
99
|
+
const normalized = this.normalizeJid(jid);
|
|
100
|
+
if (this.config.canonical === 'pn') {
|
|
101
|
+
// Want PN form
|
|
102
|
+
if (normalized.endsWith('@lid')) {
|
|
103
|
+
const mapping = this.lidToPn.get(normalized);
|
|
104
|
+
if (mapping) {
|
|
105
|
+
this.stats.lookupsServed++;
|
|
106
|
+
mapping.learnedAt = Date.now(); // Update LRU access time
|
|
107
|
+
return mapping.pn;
|
|
108
|
+
}
|
|
109
|
+
this.stats.lookupMisses++;
|
|
110
|
+
return jid; // Fallback to original input
|
|
111
|
+
}
|
|
112
|
+
// Already PN form
|
|
113
|
+
this.stats.lookupsServed++;
|
|
114
|
+
return normalized;
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
// Want LID form
|
|
118
|
+
if (normalized.endsWith('@s.whatsapp.net')) {
|
|
119
|
+
const lid = this.pnToLid.get(normalized);
|
|
120
|
+
if (lid) {
|
|
121
|
+
this.stats.lookupsServed++;
|
|
122
|
+
const mapping = this.lidToPn.get(lid);
|
|
123
|
+
if (mapping) {
|
|
124
|
+
mapping.learnedAt = Date.now(); // Update LRU access time
|
|
125
|
+
}
|
|
126
|
+
return lid;
|
|
127
|
+
}
|
|
128
|
+
this.stats.lookupMisses++;
|
|
129
|
+
return jid; // Fallback to original input
|
|
130
|
+
}
|
|
131
|
+
// Already LID form
|
|
132
|
+
this.stats.lookupsServed++;
|
|
133
|
+
return normalized;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Lookup partner form. Returns null if unknown.
|
|
138
|
+
*/
|
|
139
|
+
getLid(pn) {
|
|
140
|
+
const normalized = this.normalizeJid(pn);
|
|
141
|
+
const lid = this.pnToLid.get(normalized);
|
|
142
|
+
if (lid) {
|
|
143
|
+
const mapping = this.lidToPn.get(lid);
|
|
144
|
+
if (mapping) {
|
|
145
|
+
mapping.learnedAt = Date.now(); // Update LRU access time
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return lid || null;
|
|
149
|
+
}
|
|
150
|
+
getPn(lid) {
|
|
151
|
+
const normalized = this.normalizeJid(lid);
|
|
152
|
+
const mapping = this.lidToPn.get(normalized);
|
|
153
|
+
if (mapping) {
|
|
154
|
+
mapping.learnedAt = Date.now(); // Update LRU access time
|
|
155
|
+
return mapping.pn;
|
|
156
|
+
}
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Full mapping for inspection
|
|
161
|
+
*/
|
|
162
|
+
getMapping(jid) {
|
|
163
|
+
const normalized = this.normalizeJid(jid);
|
|
164
|
+
// Try as LID first
|
|
165
|
+
const byLid = this.lidToPn.get(normalized);
|
|
166
|
+
if (byLid) {
|
|
167
|
+
byLid.learnedAt = Date.now(); // Update LRU access time
|
|
168
|
+
return byLid;
|
|
169
|
+
}
|
|
170
|
+
// Try as PN
|
|
171
|
+
const lid = this.pnToLid.get(normalized);
|
|
172
|
+
if (lid) {
|
|
173
|
+
const mapping = this.lidToPn.get(lid);
|
|
174
|
+
if (mapping) {
|
|
175
|
+
mapping.learnedAt = Date.now(); // Update LRU access time
|
|
176
|
+
return mapping;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Learn LID↔PN mappings from group metadata participants.
|
|
183
|
+
* Call this after fetchGroupMetadata() to pre-populate the map.
|
|
184
|
+
* Supports both {id: '@lid', phoneNumber: '@s.whatsapp.net'} and
|
|
185
|
+
* {id: '@s.whatsapp.net', lid: '@lid'} participant formats (v7 + v6 shapes).
|
|
186
|
+
*
|
|
187
|
+
* @param participants - Group metadata participants array from Baileys
|
|
188
|
+
* @returns Number of new mappings learned
|
|
189
|
+
*/
|
|
190
|
+
learnFromGroupMetadata(participants) {
|
|
191
|
+
let learned = 0;
|
|
192
|
+
for (const p of participants) {
|
|
193
|
+
const domain = p.id.split('@')[1] || '';
|
|
194
|
+
if (domain === 'lid' && (p.phoneNumber || p.phone || p.number)) {
|
|
195
|
+
const pn = p.phoneNumber || (p.phone ? `${p.phone}@s.whatsapp.net` : null) || (p.number ? `${p.number}@s.whatsapp.net` : null);
|
|
196
|
+
if (pn) {
|
|
197
|
+
const prevSize = this.lidToPn.size;
|
|
198
|
+
this.learn({ lid: p.id, pn });
|
|
199
|
+
if (this.lidToPn.size > prevSize)
|
|
200
|
+
learned++;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
else if (domain === 's.whatsapp.net' && p.lid) {
|
|
204
|
+
const lid = p.lid.endsWith('@lid') ? p.lid : `${p.lid}@lid`;
|
|
205
|
+
const prevSize = this.lidToPn.size;
|
|
206
|
+
this.learn({ lid, pn: p.id });
|
|
207
|
+
if (this.lidToPn.size > prevSize)
|
|
208
|
+
learned++;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return learned;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Seed from persistence (called automatically in constructor if persistence provided)
|
|
215
|
+
*/
|
|
216
|
+
async hydrate() {
|
|
217
|
+
if (!this.persistence?.load)
|
|
218
|
+
return;
|
|
219
|
+
try {
|
|
220
|
+
const stored = await this.persistence.load();
|
|
221
|
+
if (!stored || typeof stored !== 'object')
|
|
222
|
+
return;
|
|
223
|
+
// Restore mappings
|
|
224
|
+
for (const [lid, serialized] of Object.entries(stored)) {
|
|
225
|
+
if (typeof serialized === 'string') {
|
|
226
|
+
// Old format: lid → pn string
|
|
227
|
+
const pn = serialized;
|
|
228
|
+
const phone = pn.split('@')[0];
|
|
229
|
+
const mapping = {
|
|
230
|
+
lid,
|
|
231
|
+
pn,
|
|
232
|
+
phone,
|
|
233
|
+
learnedAt: Date.now(),
|
|
234
|
+
seenCount: 1,
|
|
235
|
+
};
|
|
236
|
+
this.lidToPn.set(lid, mapping);
|
|
237
|
+
this.pnToLid.set(pn, lid);
|
|
238
|
+
}
|
|
239
|
+
else if (typeof serialized === 'object' && serialized !== null) {
|
|
240
|
+
// New format: lid → LidMapping object
|
|
241
|
+
const mapping = serialized;
|
|
242
|
+
this.lidToPn.set(lid, mapping);
|
|
243
|
+
this.pnToLid.set(mapping.pn, lid);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
catch (error) {
|
|
248
|
+
// Silently fail hydration — don't crash on corrupt persistence
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Flush current map to persistence
|
|
253
|
+
*/
|
|
254
|
+
async flush() {
|
|
255
|
+
if (!this.persistence?.save)
|
|
256
|
+
return;
|
|
257
|
+
try {
|
|
258
|
+
const toStore = {};
|
|
259
|
+
for (const [lid, mapping] of this.lidToPn.entries()) {
|
|
260
|
+
toStore[lid] = mapping;
|
|
261
|
+
}
|
|
262
|
+
await this.persistence.save(toStore);
|
|
263
|
+
}
|
|
264
|
+
catch (error) {
|
|
265
|
+
// Silently fail flush — don't crash
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
getStats() {
|
|
269
|
+
return {
|
|
270
|
+
totalMappings: this.lidToPn.size,
|
|
271
|
+
learnedFromEvents: this.stats.learnedFromEvents,
|
|
272
|
+
lookupsServed: this.stats.lookupsServed,
|
|
273
|
+
lookupMisses: this.stats.lookupMisses,
|
|
274
|
+
canonicalForm: this.config.canonical,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Clear everything
|
|
279
|
+
*/
|
|
280
|
+
reset() {
|
|
281
|
+
this.lidToPn.clear();
|
|
282
|
+
this.pnToLid.clear();
|
|
283
|
+
this.stats = {
|
|
284
|
+
learnedFromEvents: 0,
|
|
285
|
+
lookupsServed: 0,
|
|
286
|
+
lookupMisses: 0,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
destroy() {
|
|
290
|
+
this.reset();
|
|
291
|
+
// Flush one final time
|
|
292
|
+
if (this.persistence?.save) {
|
|
293
|
+
void this.flush();
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
// Private helpers
|
|
297
|
+
/**
|
|
298
|
+
* Normalize JID: strip device suffix `:N`
|
|
299
|
+
*/
|
|
300
|
+
normalizeJid(jid) {
|
|
301
|
+
// Strip device suffix e.g. "123:45@s.whatsapp.net" → "123@s.whatsapp.net"
|
|
302
|
+
return jid.replace(/:\d+@/, '@');
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Evict least recently accessed mapping (LRU)
|
|
306
|
+
*/
|
|
307
|
+
evictLRU() {
|
|
308
|
+
let oldestLid = null;
|
|
309
|
+
let oldestTime = Infinity;
|
|
310
|
+
for (const [lid, mapping] of this.lidToPn.entries()) {
|
|
311
|
+
if (mapping.learnedAt < oldestTime) {
|
|
312
|
+
oldestTime = mapping.learnedAt;
|
|
313
|
+
oldestLid = lid;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
if (oldestLid) {
|
|
317
|
+
const mapping = this.lidToPn.get(oldestLid);
|
|
318
|
+
if (mapping) {
|
|
319
|
+
this.pnToLid.delete(mapping.pn);
|
|
320
|
+
}
|
|
321
|
+
this.lidToPn.delete(oldestLid);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persistent Message Queue — Messages survive crashes and restarts
|
|
3
|
+
*
|
|
4
|
+
* Instead of fire-and-forget, queue messages and let the anti-ban
|
|
5
|
+
* system drain them at a safe pace. Failed messages auto-retry.
|
|
6
|
+
*/
|
|
7
|
+
import { EventEmitter } from 'events';
|
|
8
|
+
const DEFAULT_CONFIG = {
|
|
9
|
+
maxAttempts: 3,
|
|
10
|
+
retryBaseDelayMs: 30000,
|
|
11
|
+
maxQueueSize: 1000,
|
|
12
|
+
priorityOrder: true,
|
|
13
|
+
};
|
|
14
|
+
export class MessageQueue extends EventEmitter {
|
|
15
|
+
config;
|
|
16
|
+
queue = [];
|
|
17
|
+
processing = false;
|
|
18
|
+
sendFn = null;
|
|
19
|
+
drainTimer = null;
|
|
20
|
+
idCounter = 0;
|
|
21
|
+
constructor(config = {}) {
|
|
22
|
+
super();
|
|
23
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Set the send function (called for each message when drained)
|
|
27
|
+
* This should be the anti-ban wrapped sendMessage
|
|
28
|
+
*/
|
|
29
|
+
setSendFunction(fn) {
|
|
30
|
+
this.sendFn = fn;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Add a message to the queue
|
|
34
|
+
*/
|
|
35
|
+
add(recipient, content, options) {
|
|
36
|
+
if (this.queue.length >= this.config.maxQueueSize) {
|
|
37
|
+
throw new Error(`Queue full (${this.config.maxQueueSize} messages)`);
|
|
38
|
+
}
|
|
39
|
+
const id = `msg_${Date.now()}_${++this.idCounter}`;
|
|
40
|
+
const message = {
|
|
41
|
+
id,
|
|
42
|
+
recipient,
|
|
43
|
+
content,
|
|
44
|
+
priority: options?.priority || 'normal',
|
|
45
|
+
addedAt: Date.now(),
|
|
46
|
+
attempts: 0,
|
|
47
|
+
maxAttempts: this.config.maxAttempts,
|
|
48
|
+
scheduledFor: options?.scheduledFor?.getTime(),
|
|
49
|
+
metadata: options?.metadata,
|
|
50
|
+
};
|
|
51
|
+
this.queue.push(message);
|
|
52
|
+
this.sortQueue();
|
|
53
|
+
this.emit('added', message);
|
|
54
|
+
return id;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Add multiple messages (e.g., broadcast to many recipients)
|
|
58
|
+
*/
|
|
59
|
+
addBulk(recipients, content, options) {
|
|
60
|
+
return recipients.map(r => this.add(r, content, options));
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Start processing the queue
|
|
64
|
+
*/
|
|
65
|
+
start(intervalMs = 1000) {
|
|
66
|
+
if (this.drainTimer)
|
|
67
|
+
return;
|
|
68
|
+
this.drainTimer = setInterval(() => this.processNext(), intervalMs);
|
|
69
|
+
this.emit('started');
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Stop processing
|
|
73
|
+
*/
|
|
74
|
+
stop() {
|
|
75
|
+
if (this.drainTimer) {
|
|
76
|
+
clearInterval(this.drainTimer);
|
|
77
|
+
this.drainTimer = null;
|
|
78
|
+
}
|
|
79
|
+
this.emit('stopped');
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Clean up all timers and resources.
|
|
83
|
+
* Call this when disposing of the queue.
|
|
84
|
+
*/
|
|
85
|
+
destroy() {
|
|
86
|
+
this.stop();
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Process the next message in the queue
|
|
90
|
+
*/
|
|
91
|
+
async processNext() {
|
|
92
|
+
if (this.processing || !this.sendFn)
|
|
93
|
+
return;
|
|
94
|
+
const now = Date.now();
|
|
95
|
+
const message = this.queue.find(m => (!m.scheduledFor || m.scheduledFor <= now));
|
|
96
|
+
if (!message)
|
|
97
|
+
return;
|
|
98
|
+
this.processing = true;
|
|
99
|
+
try {
|
|
100
|
+
message.attempts++;
|
|
101
|
+
await this.sendFn(message.recipient, message.content);
|
|
102
|
+
// Success — remove from queue
|
|
103
|
+
this.queue = this.queue.filter(m => m.id !== message.id);
|
|
104
|
+
this.emit('sent', message);
|
|
105
|
+
}
|
|
106
|
+
catch (err) {
|
|
107
|
+
message.lastError = err.message;
|
|
108
|
+
if (err.message?.includes('baileys-antiban')) {
|
|
109
|
+
// Anti-ban blocked it — don't count as attempt, try later
|
|
110
|
+
message.attempts--;
|
|
111
|
+
this.emit('delayed', message, err.message);
|
|
112
|
+
}
|
|
113
|
+
else if (message.attempts >= message.maxAttempts) {
|
|
114
|
+
// Max retries reached — move to dead letter
|
|
115
|
+
this.queue = this.queue.filter(m => m.id !== message.id);
|
|
116
|
+
this.emit('failed', message, err.message);
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
// Schedule retry with exponential backoff
|
|
120
|
+
const backoff = this.config.retryBaseDelayMs * Math.pow(2, message.attempts - 1);
|
|
121
|
+
message.scheduledFor = Date.now() + backoff;
|
|
122
|
+
this.emit('retry', message, message.attempts, backoff);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
finally {
|
|
126
|
+
this.processing = false;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Get queue stats
|
|
131
|
+
*/
|
|
132
|
+
getStats() {
|
|
133
|
+
const now = Date.now();
|
|
134
|
+
return {
|
|
135
|
+
total: this.queue.length,
|
|
136
|
+
pending: this.queue.filter(m => !m.scheduledFor || m.scheduledFor <= now).length,
|
|
137
|
+
scheduled: this.queue.filter(m => m.scheduledFor && m.scheduledFor > now).length,
|
|
138
|
+
byPriority: {
|
|
139
|
+
high: this.queue.filter(m => m.priority === 'high').length,
|
|
140
|
+
normal: this.queue.filter(m => m.priority === 'normal').length,
|
|
141
|
+
low: this.queue.filter(m => m.priority === 'low').length,
|
|
142
|
+
},
|
|
143
|
+
processing: this.processing,
|
|
144
|
+
isRunning: this.drainTimer !== null,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Clear all messages
|
|
149
|
+
*/
|
|
150
|
+
clear() {
|
|
151
|
+
const count = this.queue.length;
|
|
152
|
+
this.queue = [];
|
|
153
|
+
this.emit('cleared', count);
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Remove a specific message
|
|
157
|
+
*/
|
|
158
|
+
remove(id) {
|
|
159
|
+
const before = this.queue.length;
|
|
160
|
+
this.queue = this.queue.filter(m => m.id !== id);
|
|
161
|
+
return this.queue.length < before;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Export queue for persistence
|
|
165
|
+
*/
|
|
166
|
+
export() {
|
|
167
|
+
return [...this.queue];
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Import queue (e.g., after restart)
|
|
171
|
+
*/
|
|
172
|
+
import(messages) {
|
|
173
|
+
this.queue = [...messages];
|
|
174
|
+
this.sortQueue();
|
|
175
|
+
}
|
|
176
|
+
sortQueue() {
|
|
177
|
+
if (!this.config.priorityOrder)
|
|
178
|
+
return;
|
|
179
|
+
const priorityWeight = { high: 0, normal: 1, low: 2 };
|
|
180
|
+
this.queue.sort((a, b) => {
|
|
181
|
+
const pDiff = priorityWeight[a.priority] - priorityWeight[b.priority];
|
|
182
|
+
if (pDiff !== 0)
|
|
183
|
+
return pDiff;
|
|
184
|
+
return a.addedAt - b.addedAt; // FIFO within same priority
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|