@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,373 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ReputationVoucher — High-trust accounts vouch for new numbers
|
|
3
|
+
*
|
|
4
|
+
* Establishes genuine conversation history between trusted accounts and new numbers
|
|
5
|
+
* before those new numbers contact customers. Reduces warmup time and makes new
|
|
6
|
+
* accounts appear established with real bidirectional message history.
|
|
7
|
+
*
|
|
8
|
+
* Key design principles:
|
|
9
|
+
* - Dedicated sacrificial vouch accounts (separate from business accounts)
|
|
10
|
+
* - Max 5 vouches per week per vouching account (not per day)
|
|
11
|
+
* - Targets must complete 3 qualifying events before vouching
|
|
12
|
+
* - Strike system: 3 failed vouches = 90-day suspension for voucher
|
|
13
|
+
* - Blast radius containment: vouching accounts isolated from main business
|
|
14
|
+
*
|
|
15
|
+
* Usage:
|
|
16
|
+
* const rv = new ReputationVoucher();
|
|
17
|
+
*
|
|
18
|
+
* // Register a trusted account
|
|
19
|
+
* rv.registerVoucher({
|
|
20
|
+
* jid: '27123456789@s.whatsapp.net',
|
|
21
|
+
* trustScore: 85,
|
|
22
|
+
* accountAgeDays: 240
|
|
23
|
+
* });
|
|
24
|
+
*
|
|
25
|
+
* // Queue a new number for vouching
|
|
26
|
+
* rv.queueTarget({ jid: '27987654321@s.whatsapp.net' });
|
|
27
|
+
*
|
|
28
|
+
* // Record qualifying events (auction completion, payment, etc)
|
|
29
|
+
* rv.recordQualifyingEvent('27987654321@s.whatsapp.net');
|
|
30
|
+
* rv.recordQualifyingEvent('27987654321@s.whatsapp.net');
|
|
31
|
+
* rv.recordQualifyingEvent('27987654321@s.whatsapp.net');
|
|
32
|
+
*
|
|
33
|
+
* // Check if target qualifies
|
|
34
|
+
* const check = rv.targetQualifies('27987654321@s.whatsapp.net');
|
|
35
|
+
* if (check.qualified) {
|
|
36
|
+
* const voucher = rv.getAvailableVoucher();
|
|
37
|
+
* if (voucher) {
|
|
38
|
+
* // Plan the conversation
|
|
39
|
+
* const conversation = rv.planVouchConversation(voucher.jid, '27987654321@s.whatsapp.net');
|
|
40
|
+
*
|
|
41
|
+
* // Execute sends (caller must have separate socket for voucher account)
|
|
42
|
+
* // ... send conversation.messages ...
|
|
43
|
+
*
|
|
44
|
+
* // After target replies
|
|
45
|
+
* rv.recordVouchOutcome('27987654321@s.whatsapp.net', true);
|
|
46
|
+
*
|
|
47
|
+
* // Calculate warmup credit
|
|
48
|
+
* const daysCredit = rv.calculateWarmupCredit('27987654321@s.whatsapp.net');
|
|
49
|
+
* // Skip daysCredit days of warmup for this target
|
|
50
|
+
* }
|
|
51
|
+
* }
|
|
52
|
+
*/
|
|
53
|
+
const DEFAULT_CONFIG = {
|
|
54
|
+
maxVouchesPerWeek: 5,
|
|
55
|
+
qualifyingEventsRequired: 3,
|
|
56
|
+
strikesForSuspension: 3,
|
|
57
|
+
suspensionDurationMs: 90 * 24 * 60 * 60 * 1000, // 90 days
|
|
58
|
+
minVoucherTrustScore: 60,
|
|
59
|
+
minVoucherAgeDays: 180, // 6 months
|
|
60
|
+
warmupMessages: [
|
|
61
|
+
"Hey, just checking this is the right number?",
|
|
62
|
+
"Hi! Got your details from the group",
|
|
63
|
+
"Morning! Are you available this week?",
|
|
64
|
+
"Hey there, hope this is a good time to connect",
|
|
65
|
+
"Hi, just wanted to reach out",
|
|
66
|
+
],
|
|
67
|
+
};
|
|
68
|
+
export class ReputationVoucher {
|
|
69
|
+
config;
|
|
70
|
+
vouchers = new Map();
|
|
71
|
+
targets = new Map();
|
|
72
|
+
conversations = [];
|
|
73
|
+
constructor(config) {
|
|
74
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Register a vouching account (high-trust, established number)
|
|
78
|
+
*/
|
|
79
|
+
registerVoucher(account) {
|
|
80
|
+
// Validate account meets minimum requirements
|
|
81
|
+
if (account.trustScore < this.config.minVoucherTrustScore) {
|
|
82
|
+
throw new Error(`Voucher trust score ${account.trustScore} below minimum ${this.config.minVoucherTrustScore}`);
|
|
83
|
+
}
|
|
84
|
+
if (account.accountAgeDays < this.config.minVoucherAgeDays) {
|
|
85
|
+
throw new Error(`Voucher account age ${account.accountAgeDays} days below minimum ${this.config.minVoucherAgeDays} days`);
|
|
86
|
+
}
|
|
87
|
+
const existing = this.vouchers.get(account.jid);
|
|
88
|
+
if (existing) {
|
|
89
|
+
// Update existing voucher (preserve counters)
|
|
90
|
+
this.vouchers.set(account.jid, {
|
|
91
|
+
...account,
|
|
92
|
+
vouchesThisWeek: existing.vouchesThisWeek,
|
|
93
|
+
totalVouches: existing.totalVouches,
|
|
94
|
+
failedVouches: existing.failedVouches,
|
|
95
|
+
strikes: existing.strikes,
|
|
96
|
+
suspendedUntil: existing.suspendedUntil,
|
|
97
|
+
lastVouchAt: existing.lastVouchAt,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
// New voucher
|
|
102
|
+
this.vouchers.set(account.jid, {
|
|
103
|
+
...account,
|
|
104
|
+
vouchesThisWeek: 0,
|
|
105
|
+
totalVouches: 0,
|
|
106
|
+
failedVouches: 0,
|
|
107
|
+
strikes: 0,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Queue a target for vouching
|
|
113
|
+
*/
|
|
114
|
+
queueTarget(target) {
|
|
115
|
+
const existing = this.targets.get(target.jid);
|
|
116
|
+
if (existing && existing.status !== 'failed') {
|
|
117
|
+
// Already queued or in progress
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
this.targets.set(target.jid, {
|
|
121
|
+
...target,
|
|
122
|
+
qualifyingEvents: target.qualifyingEvents || 0,
|
|
123
|
+
requestedAt: Date.now(),
|
|
124
|
+
status: 'pending',
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Check if a target qualifies for vouching
|
|
129
|
+
*/
|
|
130
|
+
targetQualifies(jid) {
|
|
131
|
+
const target = this.targets.get(jid);
|
|
132
|
+
if (!target) {
|
|
133
|
+
return { qualified: false, reason: 'Target not queued', eventsNeeded: this.config.qualifyingEventsRequired };
|
|
134
|
+
}
|
|
135
|
+
const events = target.qualifyingEvents || 0;
|
|
136
|
+
if (events < this.config.qualifyingEventsRequired) {
|
|
137
|
+
return {
|
|
138
|
+
qualified: false,
|
|
139
|
+
reason: `Not enough qualifying events (${events}/${this.config.qualifyingEventsRequired})`,
|
|
140
|
+
eventsNeeded: this.config.qualifyingEventsRequired - events,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
if (target.status === 'completed') {
|
|
144
|
+
return { qualified: false, reason: 'Already vouched' };
|
|
145
|
+
}
|
|
146
|
+
if (target.status === 'failed') {
|
|
147
|
+
return { qualified: false, reason: 'Previous vouch failed' };
|
|
148
|
+
}
|
|
149
|
+
return { qualified: true };
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Record a qualifying event for a target (e.g., auction completion, payment cleared)
|
|
153
|
+
*/
|
|
154
|
+
recordQualifyingEvent(jid) {
|
|
155
|
+
const target = this.targets.get(jid);
|
|
156
|
+
if (!target) {
|
|
157
|
+
// Auto-queue with first event
|
|
158
|
+
this.queueTarget({ jid, qualifyingEvents: 1 });
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
target.qualifyingEvents = (target.qualifyingEvents || 0) + 1;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Get next available voucher for a target (respects limits + suspension)
|
|
165
|
+
*/
|
|
166
|
+
getAvailableVoucher() {
|
|
167
|
+
const now = Date.now();
|
|
168
|
+
const weekMs = 7 * 24 * 60 * 60 * 1000;
|
|
169
|
+
// Find eligible vouchers
|
|
170
|
+
const eligible = [];
|
|
171
|
+
for (const voucher of this.vouchers.values()) {
|
|
172
|
+
// Check suspension
|
|
173
|
+
if (voucher.suspendedUntil && voucher.suspendedUntil > now) {
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
// Reset weekly counter if 7 days have passed since last vouch
|
|
177
|
+
if (voucher.lastVouchAt && now - voucher.lastVouchAt > weekMs) {
|
|
178
|
+
voucher.vouchesThisWeek = 0;
|
|
179
|
+
}
|
|
180
|
+
// Check weekly limit
|
|
181
|
+
if (voucher.vouchesThisWeek >= this.config.maxVouchesPerWeek) {
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
eligible.push(voucher);
|
|
185
|
+
}
|
|
186
|
+
if (eligible.length === 0) {
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
// Sort by:
|
|
190
|
+
// 1. Lowest vouchesThisWeek (spread load)
|
|
191
|
+
// 2. Highest trustScore (best vouchers first)
|
|
192
|
+
// 3. Lowest totalVouches (rotate usage)
|
|
193
|
+
eligible.sort((a, b) => {
|
|
194
|
+
if (a.vouchesThisWeek !== b.vouchesThisWeek) {
|
|
195
|
+
return a.vouchesThisWeek - b.vouchesThisWeek;
|
|
196
|
+
}
|
|
197
|
+
if (a.trustScore !== b.trustScore) {
|
|
198
|
+
return b.trustScore - a.trustScore;
|
|
199
|
+
}
|
|
200
|
+
return a.totalVouches - b.totalVouches;
|
|
201
|
+
});
|
|
202
|
+
return eligible[0];
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Plan a vouch conversation.
|
|
206
|
+
* Returns a conversation plan — caller executes the sends.
|
|
207
|
+
*/
|
|
208
|
+
planVouchConversation(voucherJid, targetJid) {
|
|
209
|
+
const voucher = this.vouchers.get(voucherJid);
|
|
210
|
+
if (!voucher) {
|
|
211
|
+
throw new Error(`Voucher ${voucherJid} not registered`);
|
|
212
|
+
}
|
|
213
|
+
const target = this.targets.get(targetJid);
|
|
214
|
+
if (!target) {
|
|
215
|
+
throw new Error(`Target ${targetJid} not queued`);
|
|
216
|
+
}
|
|
217
|
+
const check = this.targetQualifies(targetJid);
|
|
218
|
+
if (!check.qualified) {
|
|
219
|
+
throw new Error(`Target ${targetJid} not qualified: ${check.reason}`);
|
|
220
|
+
}
|
|
221
|
+
// Update voucher counters
|
|
222
|
+
voucher.vouchesThisWeek++;
|
|
223
|
+
voucher.totalVouches++;
|
|
224
|
+
voucher.lastVouchAt = Date.now();
|
|
225
|
+
// Update target status
|
|
226
|
+
target.status = 'active';
|
|
227
|
+
target.vouchedAt = Date.now();
|
|
228
|
+
target.vouchedBy = voucherJid;
|
|
229
|
+
// Pick 2-3 warmup messages randomly
|
|
230
|
+
const messageCount = 2 + Math.floor(Math.random() * 2); // 2-3 messages
|
|
231
|
+
const selectedMessages = [];
|
|
232
|
+
const pool = [...this.config.warmupMessages];
|
|
233
|
+
for (let i = 0; i < messageCount && pool.length > 0; i++) {
|
|
234
|
+
const idx = Math.floor(Math.random() * pool.length);
|
|
235
|
+
selectedMessages.push(pool[idx]);
|
|
236
|
+
pool.splice(idx, 1); // Remove to avoid duplicates
|
|
237
|
+
}
|
|
238
|
+
// Create conversation plan
|
|
239
|
+
const conversation = {
|
|
240
|
+
targetJid,
|
|
241
|
+
voucherJid,
|
|
242
|
+
messages: selectedMessages.map((text, idx) => ({
|
|
243
|
+
direction: 'outbound',
|
|
244
|
+
timestamp: Date.now() + idx * 5000, // Stagger by 5s
|
|
245
|
+
text,
|
|
246
|
+
})),
|
|
247
|
+
startedAt: Date.now(),
|
|
248
|
+
success: false, // Will be updated when recordVouchOutcome is called
|
|
249
|
+
};
|
|
250
|
+
this.conversations.push(conversation);
|
|
251
|
+
return conversation;
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Record outcome of a vouch.
|
|
255
|
+
* success = true if target replied within reasonable time (e.g., 7 days)
|
|
256
|
+
* success = false if target got banned within 7 days of vouch
|
|
257
|
+
*/
|
|
258
|
+
recordVouchOutcome(targetJid, success) {
|
|
259
|
+
const target = this.targets.get(targetJid);
|
|
260
|
+
if (!target || !target.vouchedBy) {
|
|
261
|
+
throw new Error(`Target ${targetJid} not vouched`);
|
|
262
|
+
}
|
|
263
|
+
const voucher = this.vouchers.get(target.vouchedBy);
|
|
264
|
+
if (!voucher) {
|
|
265
|
+
throw new Error(`Voucher ${target.vouchedBy} not found`);
|
|
266
|
+
}
|
|
267
|
+
// Find the conversation
|
|
268
|
+
const conversation = this.conversations.find((c) => c.targetJid === targetJid && c.voucherJid === target.vouchedBy);
|
|
269
|
+
if (conversation) {
|
|
270
|
+
conversation.success = success;
|
|
271
|
+
conversation.completedAt = Date.now();
|
|
272
|
+
}
|
|
273
|
+
if (success) {
|
|
274
|
+
target.status = 'completed';
|
|
275
|
+
}
|
|
276
|
+
else {
|
|
277
|
+
// Failed vouch — apply strike to voucher
|
|
278
|
+
target.status = 'failed';
|
|
279
|
+
voucher.failedVouches++;
|
|
280
|
+
voucher.strikes++;
|
|
281
|
+
// Check if suspension threshold reached
|
|
282
|
+
if (voucher.strikes >= this.config.strikesForSuspension) {
|
|
283
|
+
voucher.suspendedUntil = Date.now() + this.config.suspensionDurationMs;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Calculate warmup credit for a successfully vouched target.
|
|
289
|
+
* Returns number of warmup days that can be skipped (0-3).
|
|
290
|
+
*
|
|
291
|
+
* Credit logic:
|
|
292
|
+
* - 1 reply = 1 day credit
|
|
293
|
+
* - 2+ replies = 2 days credit
|
|
294
|
+
* - Reply + 3+ days elapsed = 3 days credit (max)
|
|
295
|
+
*/
|
|
296
|
+
calculateWarmupCredit(targetJid) {
|
|
297
|
+
const target = this.targets.get(targetJid);
|
|
298
|
+
if (!target || target.status !== 'completed') {
|
|
299
|
+
return 0;
|
|
300
|
+
}
|
|
301
|
+
const conversation = this.conversations.find((c) => c.targetJid === targetJid && c.success);
|
|
302
|
+
if (!conversation) {
|
|
303
|
+
return 0;
|
|
304
|
+
}
|
|
305
|
+
// Count inbound messages (replies from target)
|
|
306
|
+
const inboundCount = conversation.messages.filter((m) => m.direction === 'inbound').length;
|
|
307
|
+
if (inboundCount === 0) {
|
|
308
|
+
return 0;
|
|
309
|
+
}
|
|
310
|
+
if (inboundCount === 1) {
|
|
311
|
+
return 1;
|
|
312
|
+
}
|
|
313
|
+
if (inboundCount >= 2) {
|
|
314
|
+
// Check if 3+ days elapsed
|
|
315
|
+
const daysElapsed = (Date.now() - conversation.startedAt) / (24 * 60 * 60 * 1000);
|
|
316
|
+
if (daysElapsed >= 3) {
|
|
317
|
+
return 3; // Max credit
|
|
318
|
+
}
|
|
319
|
+
return 2;
|
|
320
|
+
}
|
|
321
|
+
return 0;
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Get stats for a specific voucher
|
|
325
|
+
*/
|
|
326
|
+
getVoucherStats(jid) {
|
|
327
|
+
const voucher = this.vouchers.get(jid);
|
|
328
|
+
return voucher ? { ...voucher } : null;
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Get all pending targets (awaiting vouching)
|
|
332
|
+
*/
|
|
333
|
+
getPendingTargets() {
|
|
334
|
+
return Array.from(this.targets.values()).filter((t) => t.status === 'pending');
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Get vouch history
|
|
338
|
+
*/
|
|
339
|
+
getVouchHistory() {
|
|
340
|
+
return [...this.conversations];
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Export state for persistence
|
|
344
|
+
*/
|
|
345
|
+
exportState() {
|
|
346
|
+
return {
|
|
347
|
+
version: 1,
|
|
348
|
+
exportedAt: Date.now(),
|
|
349
|
+
vouchers: Array.from(this.vouchers.values()),
|
|
350
|
+
targets: Array.from(this.targets.values()),
|
|
351
|
+
conversations: [...this.conversations],
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Import state from persistence
|
|
356
|
+
*/
|
|
357
|
+
importState(state) {
|
|
358
|
+
// Clear existing state
|
|
359
|
+
this.vouchers.clear();
|
|
360
|
+
this.targets.clear();
|
|
361
|
+
this.conversations = [];
|
|
362
|
+
// Import vouchers
|
|
363
|
+
for (const voucher of state.vouchers) {
|
|
364
|
+
this.vouchers.set(voucher.jid, { ...voucher });
|
|
365
|
+
}
|
|
366
|
+
// Import targets
|
|
367
|
+
for (const target of state.targets) {
|
|
368
|
+
this.targets.set(target.jid, { ...target });
|
|
369
|
+
}
|
|
370
|
+
// Import conversations
|
|
371
|
+
this.conversations = [...state.conversations];
|
|
372
|
+
}
|
|
373
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TypedMessageRetryReason — Typed enum for WhatsApp's retry reason codes
|
|
3
|
+
*
|
|
4
|
+
* Based on protocol research from whatsapp-rust and Baileys source.
|
|
5
|
+
* These codes appear in message retry events when encryption fails.
|
|
6
|
+
*
|
|
7
|
+
* Common scenarios:
|
|
8
|
+
* - SignalErrorBadMac (7) — Most common, indicates encryption session mismatch
|
|
9
|
+
* - SignalErrorNoSession (5) — Peer hasn't established session yet
|
|
10
|
+
* - SignalErrorInvalidKeyId (3) — Peer's prekey rotated
|
|
11
|
+
* - MessageExpired (8) — Message too old to decrypt
|
|
12
|
+
*
|
|
13
|
+
* @author Kobus Wentzel <kobie@pop.co.za>
|
|
14
|
+
* @license MIT
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* WhatsApp message retry reason codes.
|
|
18
|
+
* Based on Signal protocol error codes + WhatsApp extensions.
|
|
19
|
+
*/
|
|
20
|
+
export var MessageRetryReason;
|
|
21
|
+
(function (MessageRetryReason) {
|
|
22
|
+
MessageRetryReason[MessageRetryReason["UnknownError"] = 0] = "UnknownError";
|
|
23
|
+
MessageRetryReason[MessageRetryReason["GenericError"] = 1] = "GenericError";
|
|
24
|
+
MessageRetryReason[MessageRetryReason["SignalErrorInvalidKeyId"] = 3] = "SignalErrorInvalidKeyId";
|
|
25
|
+
MessageRetryReason[MessageRetryReason["SignalErrorInvalidMessage"] = 4] = "SignalErrorInvalidMessage";
|
|
26
|
+
MessageRetryReason[MessageRetryReason["SignalErrorNoSession"] = 5] = "SignalErrorNoSession";
|
|
27
|
+
MessageRetryReason[MessageRetryReason["SignalErrorBadMac"] = 7] = "SignalErrorBadMac";
|
|
28
|
+
MessageRetryReason[MessageRetryReason["MessageExpired"] = 8] = "MessageExpired";
|
|
29
|
+
MessageRetryReason[MessageRetryReason["DecryptionError"] = 9] = "DecryptionError";
|
|
30
|
+
})(MessageRetryReason || (MessageRetryReason = {}));
|
|
31
|
+
/**
|
|
32
|
+
* Set of retry reasons that indicate MAC verification failure.
|
|
33
|
+
* These are the most common causes of "Bad MAC" errors in Baileys.
|
|
34
|
+
*/
|
|
35
|
+
export const MAC_ERROR_CODES = new Set([
|
|
36
|
+
MessageRetryReason.SignalErrorBadMac,
|
|
37
|
+
MessageRetryReason.SignalErrorInvalidMessage,
|
|
38
|
+
MessageRetryReason.SignalErrorNoSession,
|
|
39
|
+
MessageRetryReason.SignalErrorInvalidKeyId,
|
|
40
|
+
]);
|
|
41
|
+
/**
|
|
42
|
+
* Parse a retry reason code from various input formats.
|
|
43
|
+
* Returns UnknownError if code is not recognized.
|
|
44
|
+
*/
|
|
45
|
+
export function parseRetryReason(code) {
|
|
46
|
+
if (code === undefined || code === null) {
|
|
47
|
+
return MessageRetryReason.UnknownError;
|
|
48
|
+
}
|
|
49
|
+
const n = typeof code === 'string' ? parseInt(code, 10) : code;
|
|
50
|
+
if (isNaN(n)) {
|
|
51
|
+
return MessageRetryReason.UnknownError;
|
|
52
|
+
}
|
|
53
|
+
// Check if the number is a valid enum value
|
|
54
|
+
if (Object.values(MessageRetryReason).includes(n)) {
|
|
55
|
+
return n;
|
|
56
|
+
}
|
|
57
|
+
return MessageRetryReason.UnknownError;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Check if a retry reason indicates a MAC error.
|
|
61
|
+
* MAC errors are typically caused by encryption session mismatches,
|
|
62
|
+
* often due to LID/PN race conditions.
|
|
63
|
+
*/
|
|
64
|
+
export function isMacError(reason) {
|
|
65
|
+
return MAC_ERROR_CODES.has(reason);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Get a human-readable description of a retry reason.
|
|
69
|
+
*/
|
|
70
|
+
export function getRetryReasonDescription(reason) {
|
|
71
|
+
switch (reason) {
|
|
72
|
+
case MessageRetryReason.UnknownError:
|
|
73
|
+
return 'Unknown error';
|
|
74
|
+
case MessageRetryReason.GenericError:
|
|
75
|
+
return 'Generic error';
|
|
76
|
+
case MessageRetryReason.SignalErrorInvalidKeyId:
|
|
77
|
+
return 'Invalid key ID — peer prekey rotated';
|
|
78
|
+
case MessageRetryReason.SignalErrorInvalidMessage:
|
|
79
|
+
return 'Invalid message format';
|
|
80
|
+
case MessageRetryReason.SignalErrorNoSession:
|
|
81
|
+
return 'No session — peer not initialized';
|
|
82
|
+
case MessageRetryReason.SignalErrorBadMac:
|
|
83
|
+
return 'Bad MAC — encryption session mismatch';
|
|
84
|
+
case MessageRetryReason.MessageExpired:
|
|
85
|
+
return 'Message expired — too old to decrypt';
|
|
86
|
+
case MessageRetryReason.DecryptionError:
|
|
87
|
+
return 'Decryption failed';
|
|
88
|
+
default:
|
|
89
|
+
return `Unknown reason code ${reason}`;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RetryReasonTracker — Track message retry reasons and detect retry spirals
|
|
3
|
+
*
|
|
4
|
+
* Inspired by whatsapp-rust's protocol/retry.rs module which defines 13 typed
|
|
5
|
+
* RetryReason codes with MAX_RETRY=5 and optimized key-include behavior.
|
|
6
|
+
*
|
|
7
|
+
* In the middleware layer, we can't control key inclusion (that's transport-level),
|
|
8
|
+
* but we CAN observe retry patterns from messages.update events and classify them.
|
|
9
|
+
* High retry rates per reason = ban signal precursor.
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* const tracker = new RetryReasonTracker({ enabled: true, maxRetries: 5 });
|
|
13
|
+
*
|
|
14
|
+
* // In messages.update handler:
|
|
15
|
+
* tracker.onMessageUpdate(update);
|
|
16
|
+
*
|
|
17
|
+
* // Check for spirals:
|
|
18
|
+
* if (tracker.isSpiraling(msgId)) {
|
|
19
|
+
* console.warn('Message stuck in retry spiral, dropping');
|
|
20
|
+
* }
|
|
21
|
+
*
|
|
22
|
+
* // On successful send:
|
|
23
|
+
* tracker.clear(msgId);
|
|
24
|
+
*
|
|
25
|
+
* // Get stats:
|
|
26
|
+
* const stats = tracker.getStats();
|
|
27
|
+
*/
|
|
28
|
+
const DEFAULT_CONFIG = {
|
|
29
|
+
enabled: false,
|
|
30
|
+
maxRetries: 5,
|
|
31
|
+
spiralThreshold: 3,
|
|
32
|
+
onSpiral: () => { },
|
|
33
|
+
};
|
|
34
|
+
export class RetryReasonTracker {
|
|
35
|
+
config;
|
|
36
|
+
retries = new Map();
|
|
37
|
+
totalRetries = 0;
|
|
38
|
+
reasonCounts = {
|
|
39
|
+
no_session: 0,
|
|
40
|
+
invalid_key: 0,
|
|
41
|
+
bad_mac: 0,
|
|
42
|
+
decryption_failure: 0,
|
|
43
|
+
server_error_463: 0,
|
|
44
|
+
server_error_429: 0,
|
|
45
|
+
timeout: 0,
|
|
46
|
+
no_route: 0,
|
|
47
|
+
node_malformed: 0,
|
|
48
|
+
unknown: 0,
|
|
49
|
+
};
|
|
50
|
+
spiralsDetected = 0;
|
|
51
|
+
constructor(config) {
|
|
52
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Call when a messages.update event arrives with a status/error.
|
|
56
|
+
* Classifies and records the retry.
|
|
57
|
+
*/
|
|
58
|
+
onMessageUpdate(update) {
|
|
59
|
+
if (!this.config.enabled)
|
|
60
|
+
return;
|
|
61
|
+
const msgId = update.key?.id;
|
|
62
|
+
if (!msgId)
|
|
63
|
+
return;
|
|
64
|
+
// Only track error statuses (status 0 = error in Baileys WAMessageStatus)
|
|
65
|
+
if (update.status !== 0 && !update.error)
|
|
66
|
+
return;
|
|
67
|
+
// rc10 may surface error info in update.update (wrapped message); check all forms
|
|
68
|
+
const reason = this.classify(update.error || update.update || update);
|
|
69
|
+
this.recordRetry(msgId, reason);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Classify an arbitrary error object into a RetryReason
|
|
73
|
+
*/
|
|
74
|
+
classify(err) {
|
|
75
|
+
if (!err)
|
|
76
|
+
return 'unknown';
|
|
77
|
+
// Check for HTTP status codes
|
|
78
|
+
const statusCode = err.output?.statusCode || err.statusCode || err.status;
|
|
79
|
+
if (statusCode === 463)
|
|
80
|
+
return 'server_error_463';
|
|
81
|
+
if (statusCode === 429)
|
|
82
|
+
return 'server_error_429';
|
|
83
|
+
// Extract error message/text for pattern matching
|
|
84
|
+
const errorMsg = (err.message || err.text || String(err)).toLowerCase();
|
|
85
|
+
// Pattern matching for known errors
|
|
86
|
+
if (errorMsg.includes('bad mac'))
|
|
87
|
+
return 'bad_mac';
|
|
88
|
+
if (errorMsg.includes('no session') || errorMsg.includes('session not found'))
|
|
89
|
+
return 'no_session';
|
|
90
|
+
if (errorMsg.includes('invalid key') || errorMsg.includes('key error'))
|
|
91
|
+
return 'invalid_key';
|
|
92
|
+
if (errorMsg.includes('decryption') || errorMsg.includes('decrypt'))
|
|
93
|
+
return 'decryption_failure';
|
|
94
|
+
if (errorMsg.includes('timeout') || errorMsg.includes('timed out'))
|
|
95
|
+
return 'timeout';
|
|
96
|
+
if (errorMsg.includes('no route') || errorMsg.includes('unreachable') || errorMsg.includes('offline'))
|
|
97
|
+
return 'no_route';
|
|
98
|
+
if (errorMsg.includes('malformed') || errorMsg.includes('invalid node'))
|
|
99
|
+
return 'node_malformed';
|
|
100
|
+
return 'unknown';
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Record a retry for a message
|
|
104
|
+
*/
|
|
105
|
+
recordRetry(msgId, reason) {
|
|
106
|
+
const now = Date.now();
|
|
107
|
+
let record = this.retries.get(msgId);
|
|
108
|
+
if (!record) {
|
|
109
|
+
record = {
|
|
110
|
+
msgId,
|
|
111
|
+
count: 0,
|
|
112
|
+
reasons: [],
|
|
113
|
+
firstRetry: now,
|
|
114
|
+
lastRetry: now,
|
|
115
|
+
};
|
|
116
|
+
this.retries.set(msgId, record);
|
|
117
|
+
}
|
|
118
|
+
record.count++;
|
|
119
|
+
record.reasons.push(reason);
|
|
120
|
+
record.lastRetry = now;
|
|
121
|
+
this.totalRetries++;
|
|
122
|
+
this.reasonCounts[reason]++;
|
|
123
|
+
// Check for spiral
|
|
124
|
+
if (record.count >= this.config.spiralThreshold) {
|
|
125
|
+
this.spiralsDetected++;
|
|
126
|
+
this.config.onSpiral(msgId, reason);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Should we warn the user this message is spiraling?
|
|
131
|
+
*/
|
|
132
|
+
isSpiraling(msgId) {
|
|
133
|
+
const record = this.retries.get(msgId);
|
|
134
|
+
return record ? record.count >= this.config.spiralThreshold : false;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Reset counters for a specific message (call on successful delivery)
|
|
138
|
+
*/
|
|
139
|
+
clear(msgId) {
|
|
140
|
+
this.retries.delete(msgId);
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Get current stats
|
|
144
|
+
*/
|
|
145
|
+
getStats() {
|
|
146
|
+
return {
|
|
147
|
+
totalRetries: this.totalRetries,
|
|
148
|
+
byReason: { ...this.reasonCounts },
|
|
149
|
+
spiralsDetected: this.spiralsDetected,
|
|
150
|
+
activeRetries: this.retries.size,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Clean up old retry records (>5 minutes old)
|
|
155
|
+
*/
|
|
156
|
+
cleanup() {
|
|
157
|
+
const now = Date.now();
|
|
158
|
+
const maxAge = 5 * 60 * 1000; // 5 minutes
|
|
159
|
+
for (const [msgId, record] of this.retries.entries()) {
|
|
160
|
+
if (now - record.lastRetry > maxAge) {
|
|
161
|
+
this.retries.delete(msgId);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Destroy and clean up
|
|
167
|
+
*/
|
|
168
|
+
destroy() {
|
|
169
|
+
this.retries.clear();
|
|
170
|
+
this.cleanup();
|
|
171
|
+
}
|
|
172
|
+
}
|